Skip to content

Railway Infrastructure Overview ​

Projects ​

Current Railway projects:

  1. agri-backend
  2. agri-frontend
  3. conversational-server

Environments ​

Redis Instances ​

  • BullMq dashboard
  • Redis Insight

Preview Environments ​

Every PR targeting main automatically spawns a Railway PR environment cloned from dev. Preview services build, deploy, and tear down without manual intervention.

URL pattern ​

Preview services get predictable hostnames derived from the service name and the PR's Railway environment name (pr-<N>):

  • Frontend: https://tell-ia-solutionsfrontend-tell-ia-solutions-pr-<N>.up.railway.app
  • Backend: https://tell-ia-solutionsbackend-tell-ia-solutions-pr-<N>.up.railway.app
  • Admin: https://tell-ia-solutionsadmin-tell-ia-solutions-pr-<N>.up.railway.app

The Railway GitHub bot posts a deployment table comment with the live URLs on every preview-eligible PR.

Per-PR database ​

Every PR gets its own MongoDB database on the shared Atlas dev cluster. The MONGODB_URI on the dev backend service uses a RAILWAY_ENVIRONMENT_NAME reference so each preview env resolves to its own DB name at boot:

text
mongodb+srv://<creds>@dev.mwlbfjl.mongodb.net/${{RAILWAY_ENVIRONMENT_NAME}}?retryWrites=true&w=majority&appName=dev

For previews, Railway resolves RAILWAY_ENVIRONMENT_NAME to pr-<N>, so the backend connects to a database called pr-<N> (one per PR). The dev environment itself resolves it to dev, so the long-running dev backend keeps using the dev database.

Seeding the preview database (manual for now) ​

Previously, .github/workflows/preview-seed-snapshot.yml (daily mongodump) and .github/workflows/preview-db-lifecycle.yml (per-PR restore + cleanup) handled this automatically. Both have been removed β€” the automation was unreliable and is being redesigned. Until a replacement lands, preview DBs are seeded by hand by whoever needs the preview.

Seed the pr-<N> database from a fresh production dump using the existing dev script:

bash
TARGET_DB="pr-<N>" \
TARGET_MONGODB_URI="<dev-cluster-uri>" \
PRODUCTION_MONGODB_URI="<source-uri>" \
./apps/agri-backend/dev/scripts/create-user-db.sh --dump

Drop the --dump flag on subsequent runs to reuse /tmp/tellia-mongodb.

Atlas Search indexes ​

The backend creates 5 Atlas Search / Vector Search indexes per database on first boot (aggknowledges, attachmentchunks, drafts, parseresultembeddings, parseresults). These are cluster-level metadata, not stored alongside collection data, so they survive dropDatabase() and have to be cleaned up explicitly.

The dev cluster is a Flex-tier cluster with a hard cap of 10 search/vector indexes per cluster. With 5 indexes consumed per active preview, you can run at most two concurrent fully-isolated preview backends before the next preview crashes on boot with:

text
MongoServerError: The maximum number of FTS indexes has been reached
for this instance size.

If you ever see this in a preview backend's logs, check the Atlas β†’ dev β†’ Search page for orphan indexes attached to closed PRs.

Cleanup on PR close (manual for now) ​

The automated drop job has been removed alongside the seeding workflow. Until a replacement lands, the dev who created the preview is on the hook for cleanup.

After closing a PR, drop the search indexes first (so the collection references still resolve), then the database:

bash
mongosh "<dev-cluster-uri>" --quiet --eval "
  const target = db.getSiblingDB('pr-<N>');
  target.getCollectionNames().forEach(coll => {
    try {
      target[coll]
        .aggregate([{ \$listSearchIndexes: {} }])
        .toArray()
        .forEach(idx => target[coll].dropSearchIndex(idx.name));
    } catch (e) { /* no search indexes on this collection */ }
  });
  target.dropDatabase();
"

Skipping this leaks 5 search indexes per closed PR into the Flex cluster's FTS quota. See the next section for the symptoms.

CORS and Firebase ​

The dev backend's CORS_ORIGINS includes a regex entry that auto-matches every preview hostname, so per-PR origins don't need to be added manually:

text
https://${{tell-ia-solutions:frontend.RAILWAY_PUBLIC_DOMAIN}},https://${{tell-ia-solutions:admin.RAILWAY_PUBLIC_DOMAIN}},regex:^https://tell-ia-solutions(frontend|admin)-[a-z0-9-]+\.up\.railway\.app$

The same value covers both HTTP and WebSocket origins (see apps/agri-backend/src/server.config.ts).

On the frontend side, VITE_API_URL is set with a Railway reference variable so each PR's frontend calls its own preview backend:

text
VITE_API_URL=https://${{backend.RAILWAY_PUBLIC_DOMAIN}}/api

Firebase Authentication's Authorized Domains list is currently shared across all environments. Until per-environment Firebase projects land, signing in on a preview just works β€” but every preview's auth state hits the same user pool.

Promotion PRs ​

PRs targeting staging or production (e.g. main β†’ staging, staging β†’ production) also spawn preview environments because Railway can't filter by base branch. They use the same pr-<N> DB naming convention, so they never touch staging or production data β€” but the preview backend will still fail to find a seeded pr-<N> DB unless someone seeds it by hand. Promotion PRs rarely need a working preview; skip the seeding step for them.

Custom database override (rare) ​

When a PR needs a database that diverges from the default pr-<N> name (e.g. testing a destructive migration with a clean-room DB that survives subsequent reseeds), override MONGODB_URI on that specific PR's Railway env to point at a different DB path:

text
mongodb+srv://<creds>@dev.mwlbfjl.mongodb.net/dev-pr-<N>-experimental?retryWrites=true&w=majority&appName=dev

You need to:

  1. Seed the override DB manually from your laptop:

    bash
    TARGET_DB=dev-pr-<N>-experimental \
    PRODUCTION_MONGODB_URI="<source-uri>" \
    TARGET_MONGODB_URI="<dev-cluster-uri>" \
    ./apps/agri-backend/dev/scripts/create-user-db.sh --dump
  2. Manually drop the DB and its Atlas Search indexes after closing the PR (same mongosh snippet as the Cleanup on PR close section, with the override DB name swapped in).

Bumping the Atlas cluster tier is the long-term answer if custom overrides become common β€” Flex's 10-index cap is the binding constraint.