# ───────────────────────────────────────────────────────────────────────────── # Tracely on Railway — variable reference (copy values into each service's Variables tab). # Reference syntax ${{ServiceName.VAR}} links to another service; service names below assume the # default template names: Postgres (pgvector), Redis, ClickHouse, MinIO, backend, worker, frontend. # Verify each source variable name in that plugin's own Variables tab — template versions differ. # ───────────────────────────────────────────────────────────────────────────── # ===== SHARED by backend + worker ===== # Set these as Project → Shared Variables and reference ${{shared.NAME}} from both services, # OR just paste them into both the `backend` and `worker` services. TRACELY_ENV=railway # Postgres (pgvector template). Construct both driver URLs from the template's parts (we can't prefix # the template's own DATABASE_URL with a driver). pgvector listens on 5432. DATABASE_URL=postgresql+asyncpg://${{Postgres.PGUSER}}:${{Postgres.PGPASSWORD}}@${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/${{Postgres.PGDATABASE}} ALEMBIC_DATABASE_URL=postgresql+psycopg://${{Postgres.PGUSER}}:${{Postgres.PGPASSWORD}}@${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/${{Postgres.PGDATABASE}} # ClickHouse template (HTTP interface on 8123, private). The migrate step creates the `tracely` DB. CLICKHOUSE_HOST=${{ClickHouse.RAILWAY_PRIVATE_DOMAIN}} CLICKHOUSE_PORT=8123 CLICKHOUSE_USER=${{ClickHouse.CLICKHOUSE_USER}} CLICKHOUSE_PASSWORD=${{ClickHouse.CLICKHOUSE_PASSWORD}} CLICKHOUSE_DATABASE=tracely CLICKHOUSE_ASYNC_INSERT=1 # Redis template. Prefer the private URL; if the template only exposes REDIS_URL as a public proxy, # construct the private one: redis://default:${{Redis.REDIS_PASSWORD}}@${{Redis.RAILWAY_PRIVATE_DOMAIN}}:6379 REDIS_URL=${{Redis.REDIS_PRIVATE_URL}} # MinIO template (S3 API on 9000, private). The backend pre-deploy step auto-creates the bucket. S3_ENDPOINT_URL=http://${{MinIO.RAILWAY_PRIVATE_DOMAIN}}:9000 S3_REGION=us-east-1 S3_ACCESS_KEY_ID=${{MinIO.MINIO_ROOT_USER}} S3_SECRET_ACCESS_KEY=${{MinIO.MINIO_ROOT_PASSWORD}} S3_BUCKET=tracely-events S3_EVENT_PREFIX=events/ # Auth: self-host => local (email/password). SaaS => clerk. AUTH_MODE=local # REQUIRED for AUTH_MODE=local. Generate: openssl rand -hex 32 (Railway can also generate a secret) SESSION_SECRET= SESSION_TTL_SECONDS=604800 # AUTH_MODE=clerk only: # CLERK_ISSUER=https://your-app.clerk.accounts.dev # CLERK_SECRET_KEY=sk_... # FRONTEND_ORIGIN=https://${{frontend.RAILWAY_PUBLIC_DOMAIN}} # REQUIRED to store any secret Tracely holds for you: each workspace's OpenRouter key # (Settings → OpenRouter key — what powers the LLM judges) and scenario endpoint bearer tokens. # Unset, those forms refuse to save rather than persisting plaintext. Same value on backend AND # worker (the API encrypts, the worker decrypts). Generate: openssl rand -hex 32. # Rotating it orphans already-stored secrets — each workspace re-enters theirs. SECRETS_ENCRYPTION_KEY= # Links in invite emails and monitor alerts point here (backend + worker). APP_BASE_URL=https://${{frontend.RAILWAY_PUBLIC_DOMAIN}} # Optional — transactional email for team invites (resend.com; domain must be verified there). # Without it, invites still work as copyable links — they're just not emailed. # RESEND_API_KEY=re_... # EMAIL_FROM=Tracely # Optional server-wide LLM keys — single-tenant convenience only. In normal use every workspace # brings its OWN OpenRouter key in Settings; these are never applied inside a workspace scope. # OPENAI_API_KEY=sk-... # embeddings for failure clustering # OPENROUTER_API_KEY=sk-or-... # ── Hosted cloud only (set on backend AND worker; leave unset when self-hosting) ── # BILLING_ENABLED turns on the monthly trace quota (free 20k / pro 1M, tunable), the per-org # workspace/seat caps, and the Stripe endpoints; the quota is COUNTED in the worker and ENFORCED # in the API, so a backend-only flag silently never enforces. Everything is billed per # ORGANIZATION: usage sums across the account's workspaces, so extra workspaces mint no quota. # ALLOW_PUBLIC_SIGNUP=true is for hosted cloud (each signup gets its own personal org); left # unset, registration is first-user-only and everyone else joins by invite. REQUIRE_PROJECT_LLM_KEY makes the server-wide LLM keys inapplicable # everywhere — every AI feature then runs exclusively on each workspace's own OpenRouter key. # Stripe: create the Pro product+price in the dashboard, add a webhook endpoint at # https:///api/billing/webhook (events: checkout.session.completed, # customer.subscription.updated, customer.subscription.deleted), then fill these in. # Before enabling, mark your own account: UPDATE organizations SET plan='unlimited' WHERE slug='…'; # BILLING_ENABLED=true # ALLOW_PUBLIC_SIGNUP=true # REQUIRE_PROJECT_LLM_KEY=true # FREE_TRACE_LIMIT=20000 # PRO_TRACE_LIMIT=1000000 # FREE_WORKSPACE_LIMIT=3 # PRO_WORKSPACE_LIMIT=10 # FREE_SEAT_LIMIT=3 # PRO_SEAT_LIMIT=10 # MAX_ORGANIZATIONS_PER_USER=1 # an org is a quota pool; scale by workspaces inside it # STRIPE_SECRET_KEY=sk_live_... # STRIPE_WEBHOOK_SECRET=whsec_... # STRIPE_PRICE_PRO=price_... # ===== backend service ONLY ===== # Fixed port so the frontend can reach the backend privately at a known port. PORT=8000 # Where server-side jobs call the product's own API. The default (http://backend:8000) is a # docker-compose service name and does NOT resolve on Railway. On the backend it can just be # loopback — the demo seeder runs as a child of the API process, in this container. INTERNAL_API_URL=http://localhost:8000 # ===== worker service ONLY ===== # The worker drives scenario gates through the product's API, so it needs the backend's PRIVATE # address (not loopback — nothing listens on :8000 inside the worker container). INTERNAL_API_URL=http://${{backend.RAILWAY_PRIVATE_DOMAIN}}:8000 # Real concurrency for production. Default (unset) is --pool=solo --concurrency=1 — fine to start; # switch to prefork when ingest volume grows. `threads` if a numba/UMAP fork bug ever resurfaces. # CELERY_POOL=prefork # CELERY_CONCURRENCY=4 # ===== frontend service ONLY ===== PORT=3000 # Server-side fetches reach the backend over the private network (no egress cost). TRACELY_API=http://${{backend.RAILWAY_PRIVATE_DOMAIN}}:${{backend.PORT}} # *.railway.internal is dual-stack (A + AAAA) but uvicorn binds IPv4 only (see README "IPv6 binding"), # so a Node fetch that picks the AAAA record hangs until undici's 10s UND_ERR_CONNECT_TIMEOUT. Pin it. NODE_OPTIONS=--dns-result-order=ipv4first # Must MATCH the backend AUTH_MODE. Also add it as a *Build* variable (it's inlined into the client # bundle at `next build` — see the Dockerfile.railway ARGs). NEXT_PUBLIC_AUTH_MODE=local # AUTH_MODE=clerk only (set NEXT_PUBLIC_* ones as Build variables too): # NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_... # CLERK_SECRET_KEY=sk_... # NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in # NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up