Use Node 24 or newer and the pnpm version in the starter's packageManager field. The starter is a private notes app with PostgreSQL, Platform's app host, RPC, shared query state, routing, a form, Vite, React and Tailwind with stock shadcn components.
- Run
pnpm create @shivaedev/app my-app, thencd my-app. The command writes the starter with exact published Platform versions, keeps its self-contained pnpm catalog, installs dependencies, and generates a git-ignored.env.localwith a fresh authentication secret andmy_app_dev/my_app_testdatabase URLs. Use an absent or empty folder; existing files are never overwritten. - Run
pnpm devand openhttp://127.0.0.1:5173. It first runspnpm db:setupto prepare both databases through@shivaedev/local-postgres; have local PostgreSQL 18.6 on port 55432 or Docker available. The server and background worker watch TypeScript, Vite serves React, andpnpm migrateapplies migrations, including Better Auth's tables, before either process starts. Follow the sign-in link, create an account, then add a note. The worker counts each note's words; the count appears without refreshing. Sign out and sign back in to see your saved notes.
Run pnpm test to load TEST_DATABASE_URL from .env.local and test the app against its dedicated local test database. Run pnpm db:setup separately when you want to prepare databases without starting development. See local-postgres machine setup for the shared local service requirements.
The command initializes Git when it is available and the target is outside an existing repository, then runs pnpm run setup to install dependencies and Quality's pre-commit hook before staging the generated files and installed lockfile. The first commit runs the hook's lint and typecheck gates; a failed gate leaves it uncommitted. Pass --no-git to skip repository creation. Setup installs a hook only when the app has its own .git, so it leaves an enclosing repository's hooks alone. After cloning the generated repository, run pnpm run setup to install dependencies and the hook for that checkout. Run pnpm ready for the full local gate: lint, typecheck, build and tests. Run pnpm lint to check the shared Quality policy and pnpm format to apply its fixes and formatting. The starter's biome.json extends @shivaedev/quality/biome/preset.json; no Quality config or baseline is needed. The included .github/workflows/ci.yml runs the same checks through pnpm run ci against a PostgreSQL service on pushes and pull requests.
The starter passes queue definitions to effect-pg-boss's NoteJobs.layer({ connectionString, jobs: [CountWords] }) to create its queues and dead-letter queues before serving requests, without starting a consumer. The separate worker registers the handler.
Change the shared operations in src/notes.ts, the table and services in src/server, and the screen in src/client. The CSS declaration is local; no vite/client types are needed. pnpm build && pnpm migrate && pnpm start serves the built app on port 3000; run pnpm worker in a second process to handle jobs.
The notes route loads an authenticated RPC query; the create command is also protected. Missing or revoked sessions return typed Unauthorized failures that the UI presents as a sign-in prompt. Each account sees its own notes. SessionBoundary replaces the query registry and drafts when the session changes. Better Auth's HTTP endpoints share the app host at /api/auth/*; Vite proxies both auth and RPC to that host during development.
The starter uses strict application checking with skipLibCheck: true, matching Quality's application configuration. Better Auth's published declarations also reference runtimes this Node app does not use. Auth options live in src/server/authOptions.ts; the numbered migrations in src/server/migrations/ call Better Auth's programmatic migration API, so no separate auth CLI step is needed. With PUBLIC_URL unset, auth supports the documented 127.0.0.1 HTTP development and built-app URLs.
Define a payload Schema with defineQueue in src/server/noteJobs.ts, and register its handler in src/server/noteWorker.ts; the worker uses the host's appDatabase Layer.
Register the queue definition in the request runtime's jobs Layer, then call jobs.enqueue(queue, payload) after the command's database transaction completes. Keep enqueueing independent of application SQL transactions and handle queue failures so a queue error cannot fail a note write. The starter logs failures and returns the saved note, leaving its word count pending when enqueueing fails.
After the worker writes, call invalidateOnCommit with the contract's invalidation keys; PostgreSQL delivers the hint to the server and the mounted live subscription refetches the query.
Enable Railway's per-service tracing setting separately for the web and worker services to supply the standard OpenTelemetry exporter variables. Both export their Effect spans through Effect's OTLP/HTTP tracer, as protobuf by default or JSON with OTEL_EXPORTER_OTLP_PROTOCOL=http/json. For another collector, set OTEL_EXPORTER_OTLP_ENDPOINT to its base URL (the exporter appends /v1/traces) or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to its complete traces URL, which takes precedence. Set OTEL_EXPORTER_OTLP_HEADERS to comma-separated key=value headers with URL-encoded values, OTEL_SERVICE_NAME to the service identity, and optional OTEL_RESOURCE_ATTRIBUTES to comma-separated resource attributes. Without an endpoint, no exporter runs. eBPF auto-instrumentation is not part of the framework path.
- Push the generated repository to GitHub. Install the Railway CLI (5.42.1 or newer), run
railway login, and create an empty project withrailway initor link an empty project withrailway link. Run these commands from the generated app's root, afterpnpm run setuphas installed its pinnedrailwayTypeScript SDK. - In the Railway dashboard, add an empty service named
web, keeping its source disconnected. Generate its public domain in Networking settings. In the selected environment's shared variables, createBETTER_AUTH_SECRETwith the output ofopenssl rand -base64 32andPUBLIC_URLwith that full HTTPS URL. Create both shared values before the first plan/apply; web references them throughctx.sharedin the configuration. - Run
railway config plan. The CLI evaluates.railway/railway.ts, reusesweb, and previews aPostgresdatabase andworkerservice. Review that plan, then runrailway config applyand confirm it. The configuration uses the linked project's name, connects both services'DATABASE_URLtoPostgres, and gives web references to the shared authentication secret and public URL. It setsHOST=0.0.0.0andTRUSTED_PROXY=100.64.0.0/10onweb; Railway suppliesPORT. The worker needs no public domain. Keep PostgreSQL's persistent volume and credentials in Railway. - Connect both services to the GitHub repository in their source settings, keeping the service root at the repository root, then deploy both. Railpack installs dependencies and runs
pnpm build. Web runspnpm migratein its pre-deploy container to apply the numbered notes and Better Auth migrations and prepare pg-boss storage and queues. A failed migration stops the new web deployment while the old version keeps serving. Neither service migrates at boot. The worker uses migration mode"wait"until the schema is current, since Railway may start the services in any order. The web service startspnpm start, and Railway checksGET /healthwith a 120-second timeout before routing traffic. The worker startspnpm workerwithout an HTTP health check. - Open the application domain, sign up and create a note. Confirm the word count appears, then sign out and back in to check the saved note. Change the secret or public URL in the environment's shared variables and redeploy web when either changes. Keep these values in shared variables rather than service-specific overrides; later plan/apply runs retain the references without putting the values in source.
After editing .railway/railway.ts, run railway config plan and railway config apply again. Railway deploys do not apply the TypeScript file automatically. Keep the file as the complete configuration for this project's environment; review any resource deletions in the plan before applying. See Railway Infrastructure as Code for the CLI workflow.
The server and worker use the same PostgreSQL database. Local pnpm db:setup is for development and must not run against the hosted database.
The worker and pnpm migrate need only DATABASE_URL, shared with web. Set these variables on web:
| Variable | Value |
|---|---|
DATABASE_URL |
Your PostgreSQL connection URL. |
BETTER_AUTH_SECRET |
A random secret of at least 32 characters, kept in the host's secret settings. |
PUBLIC_URL |
Your public app URL, such as https://app.example. Its origin configures Better Auth's base URL, trusted origins and RPC session Origin policy. HTTPS enables secure cookies. Leave it unset for loopback HTTP development. |
PORT |
The port supplied by your host; defaults to 3000. |
MAX_BODY_SIZE |
Maximum RPC request body size in bytes; defaults to 1048576 (1 MiB). |
HOST |
0.0.0.0 to listen on the container's network interface; defaults to loopback. |
TRUSTED_PROXY |
A comma-separated list of proxy peer addresses or CIDR ranges, such as 10.0.0.0/24,2001:db8::/32. IPv4, IPv6 and IPv4-mapped IPv6 peers are supported. The same peer matching controls forwarded RPC authority and Better Auth client IPs for rate limiting. Without it, the RPC guard retains its loopback default and auth ignores forwarded IP headers; production auth warns and uses one shared rate-limit bucket per path. |
For Railway, use TRUSTED_PROXY=100.64.0.0/10, the observed source range of its edge proxy. Railway does not promise this range, and it also trusts other services in the same Railway project; see Railway's networking specs. A matching proxy peer must forward X-Forwarded-Host and X-Forwarded-Proto (http or https) for the public URL; any browser Origin must match that authority. It must also overwrite X-Forwarded-For with the client IP or append the actual client IP to the incoming chain. Better Auth resolves the first untrusted address from the right, skipping the configured trusted hops. Auth ignores X-Forwarded-For from peers outside TRUSTED_PROXY.
Use GET /health for the web service healthcheck. It queries PostgreSQL on each request, returns 200 only when the query succeeds within two seconds, and returns 503 when the database is unavailable or the probe times out. Both responses have an empty body, require no authentication and disable caching. Better Auth’s /api/auth/ok only checks the auth process.
Web’s pre-deploy step owns migrations. The .railway/railway.ts web configuration sets preDeploy: "pnpm migrate" to run migrations in a separate pre-deploy container before the new web version starts. A failed migration exits nonzero and stops that deployment; Railway keeps the previous version serving. The command also prepares pg-boss storage and producer queues; web and worker disable pg-boss’s schema migration at boot. The migration command needs only DATABASE_URL, and rerunning it preserves applied migrations and application rows.
Neither web nor worker runs migrations at boot. Web uses migrations.mode: "check" after its pre-deploy migration command to require the numbered migrations in its loader to be recorded before initializing authentication or HTTP. This prevents web from initializing Better Auth while its user, session, account and verification tables are still missing. Every service without that pre-deploy command uses migrations.mode: "wait"; the starter worker waits for the shared ledger before initializing jobs, because deploy platforms may start services in any order. It polls every second and fails with the schema check’s MigrationError once its five-minute polling budget is exceeded while still behind. Configure migrations.pollInterval and migrations.waitTimeout in databaseOptions when the deployment needs different durations. For local production runs, run pnpm migrate before pnpm start or pnpm worker; pnpm dev performs this step before starting watchers.
The worker logs Worker waiting for schema before checking its required migrations, Worker schema current after confirming that they are recorded, then Worker ready to take jobs after pg-boss registers its handlers. These lines use Effect’s logger and include no connection details or secrets.