Configuration
This is the most involved step — but only because it’s the most important one. Get this right and everything else just works. Refract splits configuration into two layers: a.env.development file at the repo root (injected by Docker Compose at the container boundary), and typed TypeScript config modules under apps/backend/src/configuration/ that resolve everything into a clean ConfigType object. The rest of the app never touches process.env directly — it receives a fully resolved tools object. Clean, predictable, testable.
How the pattern works
One TypeScript file per environment lives underapps/backend/src/configuration/:
ConfigType object. Values come from process.env only inside that file — nowhere else in the codebase. Docker Compose injects variables from .env.development at the container boundary.
The result: the rest of your application receives a fully resolved, typed tools object. No scattered process.env calls, no runtime surprises, no “works on my machine” moments.
What to set for local dev
| Section | Local dev value | Notes |
|---|---|---|
frontend.url | http://localhost:4000 | Set FRONTEND_URL — Vite listens on 4000 |
backend.url | http://localhost:3000 | Set BACKEND_URL — Express serves API and GraphQL on 3000 |
backend.port | 3000 | Parsed from BACKEND_URL in development.ts |
logger | Pino, pretty: true | Already checked in — no changes needed. → Pino |
mailer | client: 'local' | Logs rendered emails to console, no credentials needed. → Local mailer |
queue | client: 'bullmq' + connection.url from REDIS_URL | Default stack: BullMQ on the same Redis as cache (redis://memory:6379 in Compose). Optional swap to SQS: SQS. |
analytics | client: 'local' | Already checked in — logs events to console. → Local analytics |
metrics | StatsD, mock: true | No StatsD server needed locally — mock silently no-ops all calls. → StatsD |
paymentProcessor | client: 'stripe' | Mandatory — always Stripe in local dev. Test keys go in .env.development. → Stripe |
cache | Redis | Compose sets REDIS_URL=redis://memory:6379 — shared with BullMQ. → Redis |
rds | PGHOST=rds | Use the Docker service name rds, not localhost. → Sequelize |
Your .env.development file
Open the .env.development file you created in Step 2 and fill in the following values. Everything with a ... needs a real value from you:
A note on PGHOST=rds
When your backend container connects to Postgres, it communicates over the Docker Compose network rather than directly from your computer. That’s why the Postgres service is referred to as rds in your compose.yml file—use rds as the hostname here. If you accidentally use localhost, the backend container won’t be able to locate Postgres, which will result in a connection error.
PGHOST=rds inside Docker, PGHOST=localhost if you ever connect with a local Postgres client like TablePlus or psql from your machine.Swapping pluggable tool implementations
Every tool in the table above is pluggable — you swap providers by changing theclient value in the relevant config file and updating dependencies. No loader code changes are needed.
The swap pattern is the same for every tool:
- Update the
clientvalue indevelopment.ts,production.ts, andtest.ts. - In
apps/backend/package.json, add the newtooling-*workspace package and remove the old one. - Run
make pnpm-lockfile-updatethenmake pnpm-install. - Follow the setup steps in the relevant tooling doc (environment variables, Compose services, etc.).
You’ve got the hardest part behind you. Everything from here is a command away. → Next: Step 4 — First Run