seekrit
Docs/Railway

Sync to Railway

Railway applies variables at deploy time and hands them to the process it starts, so there is no earlier point at which seekrit could inject them. A binding maps one seekrit environment onto one (project, environment, service) address inside Railway.

At a glance
What seekrit writesA service's variables, or an environment's shared variables
Addressed byRailway project, environment, and service — all three UUIDs
Connection carriesThe API token, and which kind of token it is
Token permissionAn account/workspace token, or a project token scoped to one project
Takes effectOn redeploy, which seekrit triggers by default
Value visibilityReadable by anyone with access to the Railway project

New to sync? Read Third-party sync first — the decryption grant, name mapping, deletions, and failure handling are the same on every destination.

1. Create a token, and know which kind it is

Railway has two kinds, and they travel in different headers — a project token sent as an account one fails exactly like an invalid token would, so seekrit asks you which you pasted rather than guessing:

  • account — from Account Settings → Tokens (personal or workspace). Reaches every project you can see. Sent as Authorization: Bearer.
  • project — from Project → Settings → Tokens. Railway scopes it to one project and environment, which makes it the least-privilege choice when a connection serves a single destination. Sent as Project-Access-Token.
printf '%s' "$RAILWAY_TOKEN" | seekrit sync connect \
  --name acme-railway --provider railway --token-kind project

--token-kind defaults to account. There is no workspace or team id to state: Railway ids are globally unique and a binding names its project outright, so the token plus the destination is the whole address.

2. Bind an environment

A Railway variable is addressed by project, environment, and service — all three are UUIDs, so each has its own flag. The environment here is Railway's (production, pr-42), not the seekrit environment the binding reads from; a binding is the mapping between the two.

seekrit sync verify acme-railway --provider railway \
  --railway-project 1111…  --railway-environment 2222…  --service 3333…

seekrit sync enable --connection acme-railway --provider railway \
  --railway-project 1111…  --railway-environment 2222…  --service 3333… \
  --app storefront --env production --acknowledge-decryption

Find the ids in the resource's URL, or on the project's Settings page. seekrit checks each is UUID-shaped before storing it, because the alternative is a bare Problem processing request hours later inside a run nobody is watching.

Verify asks for the environment and the service in one GraphQL document rather than two round trips — Railway's hourly quota is small enough that a spare request is worth avoiding.

Omit --service to write the environment's shared variables instead — the project-level set that services opt into with ${{shared.NAME}}. That is a genuinely different destination, not a wildcard: shared variables reach only the services that reference them.

note

Railway applies variables at deploy time, so changing one triggers a redeploy of the service. That is what makes a synced value reach the running process, and it is the default. Pass --skip-deploys (or tick Don't redeploy on change) when deploys are gated behind a release process — the values then sit staged until your next one.

How a push behaves

  • One request for the whole environment. variableCollectionUpsert takes a map of up to 100 names, so a normal run costs a single call no matter how many secrets it carries.
  • replace: false, always. Railway's upsert can replace a collection wholesale; seekrit never uses that. Railway's own PORT, reference variables, and anything another binding owns are left alone, and a removal deletes just that name.
  • Removals are one request each. Railway has no bulk delete. A variable Railway has never heard of counts as deleted rather than as a failure that would retry forever.
  • seekrit never reads Railway's existing values. Variables are addressed by name, not by an internal id, so unlike Vercel there is nothing to list first — which keeps unrelated third-party plaintext out of the sync engine entirely.
  • A run is capped at 200 requests, far below the other connectors' budgets, because Railway's own rate limits bite first: as low as 100 requests per hour on the free plan, and 10 per second on Hobby. Since a normal push is one request, that ceiling only ever bounds runs with many deletions.
caution

Railway answers a failed call with HTTP 200. A bad token, a project the token cannot see, and a deleted service all come back as 200 {"errors":[{"message":"Not Authorized"}]}. seekrit treats a populated errors array as a failure regardless of status — trusting the status alone would mark every secret as pushed on a run where nothing left the building.

It also means the reason has to be read out of the message text. Not Authorized is the one Railway returns for all three causes, so the fix is usually a process of elimination: check the token kind first, then the ids.

Troubleshooting

SymptomCauseFix
Not Authorized on everything, immediately after setupThe token kind is wrong — Railway sends the two in different headersRe-create the connection with the right --token-kind. A project token presented as a bearer fails identically to an invalid one
Not Authorized on a connection that workedThe token was revoked, or its owner lost access to the projectMint a new token and re-create the connection
Not Authorized naming one destination onlyA project token pointed at a project or environment outside its scopeUse an account token, or a project token minted in the right project
A UUID is rejected when enablingAn id was pasted from the wrong place — a slug, or a nameAll three ids are UUIDs. Take them from the resource's URL
Values pushed, process still on the old ones--skip-deploys is on, or the deploy has not finishedDeploy the service; without the flag seekrit triggers one for you
Run reported partial, 429 in the errorRailway rate-limited the tokenNothing to do — seekrit honors Retry-After and retries the rest

See also