name: pulse-railway description: Railway deployment utilities for Pulse applications. Use this skill when deploying Pulse apps with pulse-railway, configuring RailwayPlugin or RailwaySessionStore, running pulse-railway scaffold/ensure/deploy/redeploy/remove, migrating from pulse-aws to Railway, or debugging Railway router/session/deployment affinity behavior.
Pulse Railway
Deploys Pulse applications to Railway with one stable public router service and one backend Railway service per deployment. The router keeps old deployments alive and routes HTTP + websocket traffic by deployment affinity.
Quick Reference
import pulse as ps
from pulse_railway import RailwayPlugin, RailwaySessionStore
app = ps.App(
routes=[...],
plugins=[RailwayPlugin(dockerfile="Dockerfile")],
session_store=RailwaySessionStore(), # optional Redis-backed server sessions
)
uv run pulse-railway ensure \
apps/my-app/main.py \
--project my-railway-project \
--environment production
uv run pulse-railway deploy \
apps/my-app/main.py \
--project my-railway-project \
--environment production
uv run pulse-railway redeploy
When To Use
- deploying a Pulse app to Railway
- setting up or reconciling the stable
pulse-router,pulse-env,pulse-redis, andpulse-janitorbaseline - rolling out new versions with deployment affinity
- preserving server-backed sessions through Railway Redis
- migrating a Pulse app from
pulse-aws/AWSECSPlugin - debugging
/_pulse/meta, websocket routing, stale deployment affinity, or drained deployments
App Integration
Add RailwayPlugin(...) to the app. It injects pulse_deployment into prerender and Socket.IO directives and exposes /_pulse/meta. Set dockerfile=... there.
import os
import pulse as ps
from pulse_railway import RailwayPlugin, RailwaySessionStore
app = ps.App(
routes=[...],
plugins=[
RailwayPlugin(
dockerfile="Dockerfile",
project="my-railway-project",
environment="production",
deployment_name="prod",
# Optional: default image deploy repo.
# image_repository="ghcr.io/acme/my-app",
)
],
session_store=RailwaySessionStore(),
server_address=os.environ.get("PULSE_SERVER_ADDRESS"),
)
Use RailwaySessionStore() when the app needs server-backed sessions that survive redeploys. Do not read REDIS_URL directly for app sessions and do not hand-roll a Railway fallback. pulse-railway deploy injects PULSE_RAILWAY_REDIS_URL when it detects RailwaySessionStore().
CLI Workflow
Use RAILWAY_API_TOKEN for local user/workspace tokens. Reserve RAILWAY_TOKEN for Railway project tokens, especially in CI. If neither is set, pulse-railway falls back to the local Railway CLI login session from ~/.railway/config*.json. CLI login tokens are allowed for local API calls, but scaffold and ensure will not write them into the long-lived router or janitor services; set RAILWAY_TOKEN or RAILWAY_API_TOKEN when initializing or repairing runtime credentials.
First-time setup:
set -a; source .env; set +a
uv run pulse-railway ensure \
apps/my-app/main.py
Deploy with Railway source builds by default:
uv run pulse-railway deploy \
apps/my-app/main.py
Deploy by building and pushing an image locally:
uv run pulse-railway deploy \
apps/my-app/main.py \
--image-repository ghcr.io/acme/my-app
Redeploy the active backend service:
uv run pulse-railway redeploy \
--project my-railway-project \
--environment production
Redeploy a specific Pulse deployment id:
uv run pulse-railway redeploy \
--deployment-id prod-260402-120000 \
--project my-railway-project \
--environment production
scaffold <app-file> and ensure <app-file> load the app and resolve project, environment, stable service names, and service prefix from RailwayPlugin. If project or environment is omitted from the plugin, the token must provide enough scope to infer them.
Use scaffold for fresh-only template setup. Use ensure for CI and repeated setup; it creates or reconciles the baseline with the official router and janitor images for the installed package version. If the router is in a Railway canvas group, ensure also moves Pulse baseline and deployment services into that group.
Target flags are shared by local commands:
- workspace:
--workspaceor--workspace-id; only needed to disambiguate project lookup by name - project:
--project-id, then--project, thenRailwayPlugin(project=...); absent means infer from a project token - environment:
--environment-id, then--environment, thenRailwayPlugin(environment=...), then the project token environment, thenproduction
Use either the name or ID form for each target, not both.
deploy precedence:
- deployment name:
--deployment-name, thenRailwayPlugin(deployment_name=...), thenprod - image repository:
--image-repository, thenRailwayPlugin(image_repository=...); absent means source deploy - server address:
--server-address, thenApp(server_address=...), then the initialized router service address - Dockerfile:
RailwayPlugin(dockerfile=...); one is required for deploy - web root:
--web-root, thenApp(..., codegen=CodegenConfig(web_dir=...))
Path Rules
- scaffold/ensure app file is positional:
pulse-railway ensure path/to/app.py - deploy app file is positional:
pulse-railway deploy path/to/app.py --contextresolves from the shell invocation directory--web-rootfor deploy overrides must be relative to the deploy context- source deploys use
railway up, so the Railway CLI must be available - image deploys use
docker buildx build --push
Railway Model
pulse-railway scaffold creates, and pulse-railway ensure creates only on an empty project or reconciles mutable config on a complete baseline:
- stable public router service, default
pulse-router - stable env service, default
pulse-env, for user-managed app variables - stable Redis service, default
pulse-redis, unless--redis-urlis supplied - janitor cron service, default
pulse-janitor - latest published official router/janitor GHCR image tags
The router resolves deployments from Redis only. Deploy registers a new backend
deployment in Redis before checking it through the router with
pulse_deployment=<id>, then promotes it active after the affinity check
passes. Railway API calls belong to the deploy CLI/control plane, not the
router request path.
scaffold is fresh-only. ensure fails on partial baselines instead of repairing missing services; delete partial leftovers and rerun scaffold. deploy is strict and will not repair missing baseline services.
If the router has no Railway canvas group, grouping reconciliation is skipped.
User-managed variables belong on pulse-env. New backend deployments reference every non-Pulse-managed variable from that service.
Migration From pulse-aws
Typical app migration:
- replace
AWSECSPlugin()withRailwayPlugin(...) - remove
pulse-awsdependency and addpulse-railway - replace AWS deploy wrapper with
pulse-railway ensure/pulse-railway deploy - if using custom Redis sessions, switch to
RailwaySessionStore() - set custom domains in the Railway UI and point them at the router service
Verification
After deploy:
curl https://<router-domain>/_pulse/meta
Check:
/_pulse/metareturns the active deployment id- app loads through the Railway router URL or custom domain
- session data survives a redeploy when using
RailwaySessionStore() - websocket navigation and actions still work after a redeploy
- old deployment services are deleted once they have no render sessions, or after the drain TTL
For a local repo example, inspect examples/railway/ and packages/pulse-railway/README.md.
Operational Gotchas
- Keep backend replicas at
1unless sessions are safe across replicas; Railway does not provide replica-level sticky routing. - The router can run multiple replicas because routing state lives in request affinity plus Redis.
pulse-railway janitor runis for the deployed janitor service only and fails outside Railway.ensurerewrites janitor runtime env withPULSE_RAILWAY_SERVICE,PULSE_RAILWAY_JANITOR_SERVICE, andPULSE_RAILWAY_REDIS_SERVICE; rerun it after changing baseline service names.- The janitor should run every 5 minutes or slower; Railway cron does not run more frequently.
pulse-railway redeploydefaults to the active deployment in Redis; use--deployment-idfor a specific Pulse deployment.- If a deployment name matches multiple generated ids,
pulse-railway removefails and prints matches; retry withpulse-railway delete --deployment-id ....