Serve the web app and orchestrator from one Railway service - #236
Merged
Conversation
The orchestrator was the only thing deployed, so the SvelteKit app had
nowhere to run: the UI, auth, and the /api/hooks/[slug] webhook endpoint
were unreachable, and nothing could enqueue a run over HTTP. Railway's
assigned domain answered 502 because no process was listening.
server.mjs supervises both halves under one start command. They run as
two child processes rather than in one Node process because their
shutdown paths are incompatible: adapter-node drains in-flight requests
and deliberately never calls process.exit, while the orchestrator exits
as soon as its workers stop. Sharing a process, the orchestrator's exit
would cut off responses the web server was still draining. Splitting
them also stops a throw in a workflow step from taking the HTTP server
down. The cost is ~50MB of RSS for the second process.
ORIGIN is derived from RAILWAY_PUBLIC_DOMAIN when it isn't set
explicitly. SvelteKit rejects form posts whose Origin doesn't match, and
adapter-node can't infer it from behind Railway's proxy, so without this
every login and form action 403s. Deriving it beats another hand-set
variable that has to be updated whenever the domain changes.
adapter-node's SHUTDOWN_TIMEOUT defaults to 30s, which outlasts
Railway's ~30s SIGKILL and the supervisor's own grace period, so it is
pinned to 20s unless overridden.
Also fixes the landing page, which tested `data.session` — a leftover
from Supabase. The root layout returns `{ user }`, so that branch was
never taken and a signed-in user was still told to sign in.
Verified locally end to end: POST /api/hooks/smoke-hook returned 202 and
the orchestrator in the same container drained the run to completion,
and SIGTERM stopped both children cleanly with the supervisor exiting 0.
railway.web.toml is removed; it described a second service that no
longer reflects how this deploys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThreatCrush Security Scan17 finding(s) HIGH/CRITICAL: 6 | MEDIUM: 10 | LOW: 1
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The
meshhookRailway service ran onlyworkers/orchestrator.mjs, so the SvelteKit app had nowhere to live. The UI, auth, and the/api/hooks/[slug]webhook endpoint were unreachable, and nothing could enqueue a run over HTTP — Railway's assigned domain answered 502 because no process was listening.server.mjsnow supervises both halves under one start command.Why two child processes, not one process
Both halves install their own
SIGTERM/SIGINThandlers, and they are incompatible in a shared process:process.exit— it emitssveltekit:shutdownand lets the loop empty.Combined, the orchestrator's
process.exit(0)would cut off responses the web server was still draining, on every redeploy. Separate processes keep each half's already-tested shutdown path intact, and stop a throw in a workflow step from taking the HTTP server down. The cost is ~50MB RSS for the second Node process.ORIGIN
Derived from
RAILWAY_PUBLIC_DOMAINwhen not set explicitly. SvelteKit rejects form posts whoseOrigindoesn't match its own, and adapter-node can't infer that from behind Railway's proxy — without it every login and form action 403s. Deriving it beats another hand-set variable that goes stale when the domain changes.SHUTDOWN_TIMEOUTis pinned to 20s: adapter-node's default of 30s outlasts both Railway's ~30s SIGKILL and the supervisor's grace period, so a redeploy would be killed mid-drain.Drive-by fix
The landing page tested
data.session, a leftover from Supabase. The root layout returns{ user }, so that branch was never taken and a signed-in user was still shown "Please sign in to continue".Verification
Built and run locally against a migrated SQLite file:
That is the first time the loop closes from an HTTP entry point — web enqueues, orchestrator in the same container drains it.
SIGTERM handling:
GET /andGET /auth/loginboth 200. Tests: 390 passing (132 vitest incl. 10 new supervisor tests, 258 node:test).Note
railway.web.tomlis deleted — it described a second service that no longer reflects how this deploys.🤖 Generated with Claude Code