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.
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
.envexcept 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-runnow and then. Differences mean someone edited the dashboard without telling anyone. - Keep roles narrow: Viewer for people who only read development.
sink pull --version Nreads each secret at an earlier version, which is how you answer "what was this last week?"- Add
.envto.gitignoreand commit a.env.example. If it is already too late, see removing a .env from git history.
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.