Deploy with managed services
Run an immutable Prosewire image with external Postgres, Redis, SMTP, and a load balancer.
This guide assumes you build the repository’s Dockerfile and push the resulting image to a registry your runtime can access. It does not assume that a public Prosewire image or latest tag is available.
Runtime topology
A cloud deployment runs three commands from the same immutable image:
| Process | Command | Scaling |
|---|---|---|
| Migration | node apps/worker/dist/migrate.mjs | Exactly one successful job per release |
| Web | node apps/web/server.js | One or more replicas |
| Worker | node apps/worker/dist/index.mjs | Exactly one per database |
Provide Postgres through DATABASE_URL, Redis through REDIS_URL, and a private S3-compatible bucket through the PROSEWIRE_MEDIA_* variables. Use CDN delivery or the signed delivery described below. Cloud mode refuses to start without media storage. Run the migration job to completion before replacing web replicas or the worker. Do not let an orchestrator repeatedly restart a completed migration as a long-running service.
Private Railway media bucket
Set PROSEWIRE_MEDIA_DELIVERY=signed and an explicit PROSEWIRE_PUBLIC_URL matching the dashboard’s HTTPS origin. Leave PROSEWIRE_MEDIA_PUBLIC_URL unset. Reference the existing bucket’s ENDPOINT, BUCKET, ACCESS_KEY_ID, SECRET_ACCESS_KEY and REGION in the corresponding media variables. Match PROSEWIRE_MEDIA_FORCE_PATH_STYLE to the bucket’s credential urlStyle; avoid rotating its credentials just to wire the service.
Preserve existing bucket CORS rules and add the dashboard’s exact origin with PUT and the Content-Type request header. Browser uploads need this rule; server-side SDK uploads do not. Verify a browser upload, completion and image preview, then a published image’s anonymous /media/... redirect. Signed reads expire after one minute; Prosewire does not proxy image bytes or make the bucket public. See media configuration for visibility and migration details.
Build and publish an image
Build from an immutable release tag or commit, then push a versioned tag to your registry:
docker build \
--build-arg NEXT_DEPLOYMENT_ID=release-0-2-0 \
--tag registry.example.com/team/prosewire:0.2.0 .
docker push registry.example.com/team/prosewire:0.2.0Record the resulting digest and deploy by digest when the platform supports it.
Use the cloud Compose topology
The repository includes docker-compose.cloud.yml for a host that connects to external services:
PROSEWIRE_IMAGE=registry.example.com/team/prosewire:0.2.0 \
docker compose -f docker-compose.cloud.yml up -dYour .env must contain the external database, Redis URL, public origins, authentication secret, server-action encryption key, SMTP settings, and media storage settings. The deployment ID is already part of the image. The cloud file does not provision Postgres, Redis, object storage, a CDN, SMTP, TLS, backups, or a load balancer.
Keep the media bucket private. Grant the web process permission to get, put, inspect, and delete objects only in that bucket. Configure the dashboard origin in the bucket CORS policy for signed PUT requests with Content-Type. The CDN should expose processed publications/ keys and deny the temporary _uploads/ prefix. Enable provider snapshots, versioning, replication, and retention according to your recovery target.
Bootstrap the first owner
Set PROSEWIRE_ALLOW_SIGN_UP=true only while creating the first account and workspace. Cloud deployments can create and switch between multiple workspaces. After onboarding, set the value to false and replace every web replica unless open Cloud registration is intentional. Invitation-based account creation continues to work when open registration is disabled.
Platform requirements
- Preserve the same
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYacross web replicas and rolling releases - Run the same image digest, including its baked deployment ID, on every replica of the same release
- Route traffic only after
/api/healthreturns 200 - Allow graceful termination so in-flight requests can finish
- Run web, worker, and migration from the same immutable image digest
- Back up Postgres and Redis independently of the application lifecycle, preserve matching recovery points for the email outbox and queue, and test restoration
- Back up both media buckets, preserve the database and object-store recovery point together, and verify stored checksums after restoration
- Encrypt and network-restrict Postgres connections
- Configure Redis persistence, backups, and replication; disable key eviction; and restrict it to the private application network
- Keep exactly one supervised worker running for each database; the Effect SQL workflow runner in this release does not support multiple Prosewire worker processes
- Set
PROSEWIRE_EMAIL_WORKER_CONCURRENCYso in-process concurrency stays within SMTP provider limits - Capture migration, web, and worker logs in the platform’s logging system
No managed Prosewire control plane or billing service is required. The runtime is self-contained apart from Postgres, Redis, object storage and CDN delivery, SMTP, TLS termination, and the infrastructure services you choose.
Allow an unused Railway stack to sleep
Railway Serverless observes outbound traffic and normally sleeps a container 5–10 minutes after that traffic stops. The setting takes effect on a new container after redeployment. A quiet metrics graph does not prove sleep: private Postgres and Redis traffic also counts. See Railway Serverless.
The default Prosewire worker continuously scans scheduled posts and the email outbox every 30 seconds. Its durable workflow engine also polls Postgres and consumes Redis queues. Keep that mode for deployments without a verified wake path, or whenever immediate recovery after a producer crash is required. Never enable Serverless on a continuous worker without a verified inbound wake path: a slept worker with no HTTP listener cannot be woken by a content write. Keep Serverless disabled on continuous workers.
An opt-in worker command releases the complete Effect runtime after two empty scans, thirty seconds apart, and retains only HTTP ingress:
node apps/worker/dist/index.mjs --idle-httpAn authenticated HTTP wake returns 202 as soon as it is accepted and starts
due publishing and email dispatch without making a content save wait for scans.
The dashboard, private API, SDK, CLI, and MCP share these repository hooks;
revision restores and applied imports containing scheduled posts also wake the
worker. Draft edits, synchronous publish-now operations, and reads send no hints.
A wake arriving during runtime disposal waits for a fresh runtime. No PostgreSQL
or Redis connection, notification listener, workflow poll, queue consumer, or
SMTP transport survives parking. The web pool uses node-postgres defaults to close idle clients after ten
seconds; the container image disables Next.js telemetry in build and runtime.
The worker does not park while any scheduled post (including a future one), undispatched outbox row (including a deferred retry or lease), unfinished email workflow, or queued/leased Redis email exists. These keep the established 30-second scans and email consumers running. Permanent delivery failures remain terminal failures; they are not reported as successful delivery. Missing SMTP preserves pending mail, which also prevents parking. Failed scans do not establish idleness and continue polling.
Configure and verify wake-up before switching modes
- Deploy the reviewed image to every producer/web replica, keeping the continuous worker running during setup. Older images do not send HTTP hints.
- Give the single worker HTTP ingress on its
PORT(default3000). Set a random, server-onlyPROSEWIRE_WORKER_WAKE_TOKENof at least 32 characters. Configure the same token andPROSEWIRE_WORKER_WAKE_URLending in/wakeon every web replica. Use private networking for web-to-worker traffic. Never embed this token in frontend code or a URL. - Configure an independent, deployment-owned recovery/maintenance wake. It must
run while web and worker are asleep, monitor failures, and retry a failed
wake. The same image includes
node apps/worker/dist/wake.mjs, which needs only the wake URL and token, opens no database or Redis connection, and exits nonzero if the wake is not accepted. It can run under Railway cron or another external scheduler. Do not use a timer in a sleeping container. - Stop the old worker before starting
--idle-http; keep exactly one worker per database, including during deployment transitions. SetPROSEWIRE_WORKER_RECOVERY_CONFIGURED=trueon the idle worker only after the independent recovery job has been configured and tested. This is an operator declaration, not automatic proof that an external scheduler exists. - Verify an authenticated
POST /wakereturns202after accepting the wake. An unauthenticated call returns401; shutdown returns503. Failed scans continue retrying while HTTP ingress remains available.GET /healthchecks HTTP ingress only and performs no database work. Avoid a continuous uptime monitor that keeps the service awake. - Verify an invitation is dispatched immediately from a parked worker; verify
a future scheduled post keeps the worker active and publishes with its
original
scheduledAt; restart the worker with queued mail and verify recovery. Simulate a missed hint and observe the independent recovery job. On an isolated Railway environment, also hold a future scheduled post for more than twenty minutes and verify the worker stays awake. Verify a cold wake succeeds inside the retry deadline. Then verify empty-stack connection counts, outbound traffic, and actual Railway sleep across a full inactivity window before production activation.
Optional configuration profiles are committed as
deploy/railway-worker-idle.json and deploy/railway-worker-recovery.json.
Select them explicitly on the corresponding services; neither is an automatic
root Railway configuration. They define commands and restart/cron behavior.
Serverless, ingress, secret variables, image digests, and persistent volumes
remain deployment-owned settings. Preserve the Postgres/Redis images, major
versions, volumes, persistence, and backups when enabling sleep.
The remaining recovery tradeoff
Postgres commit and HTTP wake are not one atomic operation. Wake hints run in an explicitly observed background fiber after the durable commit. Saves do not wait for worker boot. Each hint retries until accepted or a sixty-second deadline, with bounded request timeouts and exponential cold-start backoff. A process shutdown can also interrupt an in-flight hint. An unavailable endpoint is logged without rolling back or losing the committed content or email intent. A producer crash after commit can still lose its hint. The independent recovery timer covers that failure window and runs retention when there is no editorial activity. Idle workers prune on each cold process start and then at most once per day while that process remains alive; this idempotent pruning can run more often than the continuous worker’s daily timer.
The example recovery profile uses */15 * * * * in UTC. This is not the
ordinary job schedule: successful post-commit hints run immediately, and future
scheduled posts keep polling. A lost hint or producer crash can nevertheless
leave work waiting for the next recovery run, plus platform startup/scheduler
lag. If that exceptional delay is unacceptable, keep continuous mode until a
durable external event/wake service with the required retry guarantee exists.
Shorter recovery intervals reduce this delay but can prevent the stack from
reaching Railway’s 5–10 minute sleep window. Railway cron has a five-minute
minimum and does not promise exact-to-the-minute execution; see
Railway cron requirements.
The idle command does not exit to simulate platform sleep, pause a project, or drop deferred jobs. Local connection closure proves application idleness only; only the deployed platform can prove that a container has actually slept. To roll back, stop the idle worker and restart the normal worker command before removing the recovery job or web wake configuration.