Skip to content

Serve the web app and orchestrator from one Railway service - #236

Merged
ralyodio merged 1 commit into
masterfrom
feat/web-on-same-service
Aug 12, 2026
Merged

Serve the web app and orchestrator from one Railway service#236
ralyodio merged 1 commit into
masterfrom
feat/web-on-same-service

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What

The meshhook Railway service ran only workers/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.mjs now supervises both halves under one start command.

Why two child processes, not one process

Both halves install their own SIGTERM/SIGINT handlers, and they are incompatible in a shared process:

  • adapter-node drains in-flight requests and deliberately never calls process.exit — it emits sveltekit:shutdown and lets the loop empty.
  • the orchestrator exits as soon as its workers stop.

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_DOMAIN when not set explicitly. SvelteKit rejects form posts whose Origin doesn'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_TIMEOUT is 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:

POST /api/hooks/smoke-hook  ->  202
[orchestrator] Processing job 1..5
[orchestrator] ✓ run f5f76bd2-… completed

That is the first time the loop closes from an HTTP entry point — web enqueues, orchestrator in the same container drains it.

SIGTERM handling:

[orchestrator] Orchestrator stopped
[supervisor] web stopped (code 0)
[supervisor] orchestrator stopped (code 0)
[supervisor] all children stopped

GET / and GET /auth/login both 200. Tests: 390 passing (132 vitest incl. 10 new supervisor tests, 258 node:test).

Note

railway.web.toml is deleted — it described a second service that no longer reflects how this deploys.

🤖 Generated with Claude Code

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>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

17 finding(s)

HIGH/CRITICAL: 6 | MEDIUM: 10 | LOW: 1

Severity Rule Location
HIGH secret-database-url apps/web/.env.example:9
HIGH secret-database-url docs/Environment-Setup.md:46
HIGH secret-database-url docs/Environment-Setup.md:132
HIGH secret-slack-webhook docs/WEBHOOK_CONFIGURATION.md:145
HIGH secret-database-url RAILWAY_DEPLOYMENT.md:58
HIGH secret-generic-credential src/nodes/README.md:271
MEDIUM sql-template-interpolation apps/web/src/routes/api/secrets/[id]/+server.js:80
MEDIUM sql-template-interpolation apps/web/src/routes/api/workflows/[id]/+server.js:141
MEDIUM sql-template-interpolation docs/Turso-Migration.md:70
MEDIUM manifest-install-lifecycle-script package.json:9
MEDIUM js-shell-exec-interpolation scripts/generate-issue-prds.mjs:71
MEDIUM js-shell-exec-interpolation scripts/generate-issue-prds.mjs:586
MEDIUM js-shell-exec-interpolation scripts/generate-issue-prds.mjs:651
MEDIUM js-shell-exec-interpolation scripts/generate-issue-prds.mjs:685
MEDIUM js-shell-exec-interpolation scripts/gh-project-status.js:71
MEDIUM js-shell-exec-interpolation scripts/github-issues-sync.mjs:33
LOW secret-generic-credential src/nodes/webhook.test.js:287

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 38d7eb6 into master Aug 12, 2026
4 checks passed
@ralyodio
ralyodio deleted the feat/web-on-same-service branch August 12, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant