Metadata-Version: 2.3
Name: pulse-railway
Version: 0.2.1
Summary: Railway deployment utilities for Pulse applications
Author: Erwin Kuhn
Author-email: Erwin Kuhn <erwin.kuhn@protonmail.com>
Requires-Dist: aiohttp>=3.12.15
Requires-Dist: fastapi>=0.116.1
Requires-Dist: httpx>=0.28.1
Requires-Dist: pulse-framework
Requires-Dist: redis>=6.4.0
Requires-Dist: uvicorn>=0.35.0
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# pulse-railway

Railway deployment utilities for Pulse applications.

`pulse-railway` uses one stable public router service plus one Railway service per deployment. The router keeps older deployments alive and forwards HTTP and websocket traffic to the selected deployment based on `pulse_deployment`.

For local CLI usage, prefer `RAILWAY_API_TOKEN` when you are using a user/account/workspace token. 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`.

## Quick Start

```bash
set -a; source .env; set +a

uv run pulse-railway ensure \
  examples/railway/main.py

uv run pulse-railway deploy \
  examples/railway/main.py

uv run pulse-railway deploy \
  examples/railway/main.py \
  --image-repository ghcr.io/<org>/<name>
```

Use the commands like this:

- `pulse-railway scaffold` bootstraps the stable router, env, Redis, and janitor stack into an empty Railway project.
- `pulse-railway ensure` creates or reconciles that baseline stack and is safe to run from CI before deploy.
- `pulse-railway upgrade` is currently a no-op placeholder.
- `pulse-railway deploy` builds and rolls out a new backend service on top of an already-initialized stack.
  By default it uploads source and uses `railway up` to build on Railway. Passing an image repository switches deploy to the local `docker buildx build --push` path.
- `pulse-railway redeploy` redeploys the active backend service. Pass `--deployment-id <id>` to redeploy a specific Pulse deployment.

`scaffold <app-file>` bootstraps the baseline from `RailwayPlugin` config and defaults. Stable router, Redis, janitor, and backend service names come from the app plugin. The baseline also includes a stable env service that acts as the canonical source for user-managed deployment variables.

`ensure` uses the same target/options as `scaffold`, but it is idempotent. On an empty project it creates the baseline; on an existing or partial baseline it creates missing services and rewrites Pulse-managed runtime config. Existing router and janitor images are preserved unless you pass `--router-image` or `--janitor-image`.

`deploy` reads app configuration from `RailwayPlugin` on the target app. Provide the Dockerfile with `RailwayPlugin(dockerfile=...)` or `--dockerfile`.

`pulse-railway scaffold` is template-first and fresh-only. On an empty target it deploys the published `pulse-baseline` template so the router, janitor, and Redis land on the Railway canvas with a stable layout, creates the stable env service in the template group, then writes runtime images, variables, domains, cron, and healthchecks. Router and janitor services use the latest published official GHCR image tags unless you pass explicit image overrides: `ghcr.io/erwinkn/pulse-railway-router:<version>` and `ghcr.io/erwinkn/pulse-railway-janitor:<version>`. When you pass `--redis-url`, scaffold removes the managed Redis service plus any template-created Redis references, then rewrites the baseline to use the external URL. If any baseline service already exists, rerun with `pulse-railway ensure`.

`pulse-railway scaffold <app-file>`, `pulse-railway ensure <app-file>`, and `pulse-railway deploy <app-file>` load the app to read `RailwayPlugin`. For `scaffold` and `ensure`, set `RailwayPlugin(project="...", environment="...")`; if project or environment is omitted, the token must provide enough scope to infer them. For `deploy`, CLI `--project` / `--environment` override plugin config. Project and environment IDs are resolved internally before Railway API calls.

You can also set `deployment_name` on `RailwayPlugin` to provide the default deploy name from app config. Precedence is: `--deployment-name`, then `RailwayPlugin(deployment_name=...)`, then `prod`.

You can also set `image_repository` on `RailwayPlugin` to provide the default backend image registry from app config. Precedence is: `--image-repository`, then `RailwayPlugin(image_repository=...)`. If none is set, deploy uses source mode.

`pulse-railway deploy <app-file>` reads `server_address` and web root from the target `ps.App`, and the Dockerfile path from `RailwayPlugin(dockerfile=...)`. Precedence is: explicit CLI override, then app/plugin config, then the initialized router service address for `server_address`.

If `--redis-url` is omitted, `pulse-railway scaffold` and `pulse-railway ensure` create the stable Redis service in the Railway project.

`pulse-railway deploy` is strict. It does not create or repair the stable baseline stack. If the router, env, Redis, or janitor baseline is missing or outdated, run `pulse-railway ensure` first.

User-managed app variables should live on the stable env service. Each new backend deployment references every non-Pulse-managed variable from `pulse-env`, so users can sync secrets into that service however they want: Railway UI, Shared Variables, Doppler, or another workflow.

By default, `pulse-railway deploy` uses source mode. Image deployments require `--image-repository ghcr.io/<org>/<name>` or `RailwayPlugin(image_repository="ghcr.io/<org>/<name>")`.

## Model

- Stable Railway router service
- Stable Railway env service for user-managed deployment variables
- One Railway backend service per deployment
- Stable Railway Redis service unless you pass `--redis-url`
- Optional Railway cron job for janitor cleanup
- Active deployment stored in `PULSE_ACTIVE_DEPLOYMENT`
- Explicit affinity via `pulse_deployment` query param or `x-pulse-deployment` header
- Websockets proxied through the router to the selected backend service
- Draining and cleanup state stored in Redis
- Before janitor deletion, the backend broadcasts Pulse `reload` to connected clients
- Websocket reconnects with stale affinity fall back to the active deployment so the app can trigger a full-page reload

## Runtime

Backend services must set `PULSE_DEPLOYMENT_ID`. `RailwayPlugin` injects the affinity query into Pulse prerender and websocket directives and exposes `/_pulse/meta` for verification. Internal janitor endpoints receive `PULSE_RAILWAY_INTERNAL_TOKEN` through Railway reference variables using `${{ shared.PULSE_RAILWAY_INTERNAL_TOKEN }}`.

If your app opts into `pulse_railway.RailwaySessionStore()`:

- deploy injects `PULSE_RAILWAY_REDIS_URL` into the backend app
- the app session store uses that Redis for server-backed sessions

When the baseline stack has Redis configured:

- deploy marks the new deployment `active`
- previous active deployments become `draining`
- the router records HTTP activity and websocket leases in Redis
- the janitor signals connected browsers to reload before deleting a drained deployment
- the janitor cron job deletes drained deployments after they are idle

The janitor job runs as a Railway cron service, not a permanent always-on process. Use a cadence of 5 minutes or slower; Railway does not run cron jobs more frequently than that.

`pulse-railway janitor run` is for the deployed janitor service only. It probes `*.railway.internal` backends and now fails fast outside Railway.

If you need to trigger cleanup manually, run the command from inside the deployed janitor service:

```bash
pulse-railway janitor run
```

To rerun Railway's build/deploy for the active backend deployment, use:

```bash
pulse-railway redeploy \
  --project <project-name> \
  --environment production \
  --token <project-token>
```

To redeploy a specific Pulse deployment id:

```bash
pulse-railway redeploy \
  --deployment-id prod-260402-120000 \
  --project <project-name> \
  --environment production \
  --token <project-token>
```

To remove a deployment by the original deployment name prefix, use:

```bash
pulse-railway remove \
  --service pulse-router \
  --deployment-name prod \
  --project <project-name> \
  --environment production \
  --token <project-token>
```

If the name matches multiple generated deployment ids, the command fails and prints the matching ids so you can retry with `pulse-railway delete --deployment-id ...`.

## Notes

- Backend services should run with `1` replica. Railway does not provide replica-level sticky routing, so deployment affinity alone is only safe with a single backend replica when sessions are stored in memory.
- The router can run with multiple replicas because routing state lives in the request query/header plus the active deployment variable.
- Healthchecks remain for crash recovery only. Deployment cleanup is handled by the janitor cron job, not by failing healthchecks on drained services.
