How to sync environment variables with Railway

Railway's variables live at the intersection of a service and an environment, which is precise and works well — right up until someone needs the same values on their own machine, and the only route is the dashboard's copy button and a lot of tabs.

Updated

Where Railway's model helps, and where it stops

Railway scopes variables to a service within an environment, and lets services reference each other's values rather than duplicating them. Compared to a flat list this is a genuine improvement — a database URL is defined once and referenced where it is needed.

What the model does not give you is anything outside Railway:

  • No history. A value that changes overwrites the one before it, and the old one is gone.
  • No record of who changed it, or when.
  • No way to hand a contractor read access to staging alone without giving them the project.
  • No path to a local .env except copying, which means the copy starts going stale immediately.

That last one is what usually pushes teams to look for something else. Local development needs the same values, and the moment they exist in two places with no way to compare them, they disagree.

Import a Railway environment

Create a Railway API token in your account settings, then link the directory and import. The token is used for the import request and never stored:

sink login
sink init                          # writes .sink.json — ids only, safe to commit

sink import railway staging \
  --project my-app \
  --service api \
  --provider-env staging \
  --dry-run

Railway needs both --service and --provider-env, because a variable is only fully identified by the pair. Omit either and the CLI will ask rather than guess.

--dry-run prints the create-and-change list and stops. Once it looks right, run it without the flag, and repeat per environment:

sink import railway development --project my-app --service api --provider-env development
sink import railway staging     --project my-app --service api --provider-env staging
sink import railway production  --project my-app --service api --provider-env production

A project with several services gets one Sink environment per service-and-stage pair, or one per stage with the services merged — whichever matches how your team actually thinks about them. Existing keys are skipped unless you pass --mode overwrite, and an overwrite adds a version rather than replacing one, so it can be read back later.

Railway's reference variables resolve to their values on the way out. What lands in Sink is the resolved value, not the reference — worth knowing before you import a service that leans on them heavily.

The loop after the import

sink pull staging      # writes .env with 0600 permissions
sink diff staging      # what differs between local and remote
sink push staging      # send local changes back up

sink push asks before it writes, shows what will change, and takes --dry-run if you want the preview without the prompt. Use --prune deliberately and rarely: it deletes remote secrets that are absent from your local file, which is correct when your file is authoritative and destructive when it is merely out of date.

The new-laptop test

A good way to tell whether your setup actually works is to imagine setting up a machine from scratch. With the values in Railway alone, that is a dashboard, a clipboard, and twenty minutes of hoping you did not miss a line. With them in Sink:

git clone [email protected]:you/my-app.git
cd my-app
sink login
sink pull development

The committed .sink.json carries the workspace, team, project and environment ids, so a fresh clone already knows where its secrets live. Nobody has to send anything to anyone, which is the point — see sharing a .env file securely for why the sending is the part worth eliminating.

CI and deploys

# CI secret store
SINK_API_KEY=sk-...

# job step
sink diff production --exit-code    # fail the build when local and remote disagree
sink pull production --stdout > .env

Railway keeps injecting its own variables at deploy time — Sink does not sit in that path and does not try to. It is where the team's copy is versioned, audited and shared; Railway remains the thing that runs your service. Rotate in the provider, update Sink, then let everyone re-pull.

Worth doing once

  • Re-run the import with --dry-run now and then. Differences mean someone edited the dashboard without telling anyone.
  • Keep roles narrow: Viewer for people who only read development.
  • sink pull --version N reads each secret at an earlier version, which is how you answer "what was this last week?"
  • Add .env to .gitignore and commit a .env.example. If it is already too late, see removing a .env from git history.
Try it on your own project

Sink keeps every environment under AES-256-GCM envelope encryption, with roles, versions and an audit trail — and puts it back in your .env with one command. The free tier does not ask for a card.