Every lesson so far in this domain covered one piece in isolation — a service, a database, private networking, a worker. This lesson assembles all of them into one actual, realistic project, the way a real small product's infrastructure genuinely looks.
3 min read
Project: "taskflow"
├── web (Next.js frontend — public domain: taskflow.app)
├── api (Express API — private only, reached via api.railway.internal)
├── worker (processes background jobs from a queue — no public domain at all)
└── postgres (managed database — private only)
This shape — a public frontend, a private API behind it, a worker handling anything that doesn't need to block a user's request, and a database underneath — is genuinely common for a real small product, and every piece of it maps directly onto a concept this domain already covered individually.
api has no public domain, deliberatelyThe frontend (web) is the only service that genuinely needs to be reachable from the public internet — it's the thing actual users' browsers talk to. api only ever needs to be reached by web, over the private network (lesson 4), which means it never gets a public domain generated for it at all. This isn't a missed configuration step; it's the deliberate, default-private posture lesson 4 established, applied correctly: a service with no legitimate external caller simply has no public attack surface, by design.
DATABASE_URL = ${{Postgres.DATABASE_URL}} # same reference variable (lesson 4),
# used in BOTH api and worker's config
Both api and worker reference the same Postgres service's connection string via a reference variable — they're two independent services, but they share the same underlying data, which is exactly the right shape when a worker's job is "finish something the API started" (send a confirmation email after an API call created an order, say) rather than owning genuinely separate data of its own.
postgres first — other services need its reference variable to exist before they can use it.api, configured with DATABASE_URL referencing postgres.worker, configured identically for database access, plus whatever queue-connection variable it needs.web, configured with api's private hostname as its backend URL.web only, then add the custom domain (lesson 8) on top of it.A service referencing another service's variable before that other service exists fails immediately, since there's nothing yet to reference — Railway doesn't reorder deploys to resolve this automatically. Deploying in dependency order (database, then the services that depend on it, then the public-facing one last) avoids a category of "why can't my API connect to the database" confusion that's really just a sequencing mistake, not a configuration bug.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why does the api service in the realistic example have no public domain generated for it?
2. Why is deployment order important when standing up a multi-service project with reference variables?
3. Why do both the api and worker services in the example reference the same Postgres connection string?
Railway