A Railway shared variable is not a broadcast. It's a value you define once in Project Settings, and then link, service by service, on purpose. Railway's docs are explicit about the linking step: you either "click the Share button from Project Settings and select specific services" or open a service's Variables tab and click "Shared Variable."
That distinction matters more than it sounds, because most of the security advice written about this feature assumes the opposite. If shared variables really did land in every service automatically, the advice would be "use fewer of them." They don't, so the actual risk lives somewhere else: in which services you link, and in what happens to those links when Railway copies an environment.
TL;DR
Shared variables sit at the project level, scoped per environment, and each service opts in by linking one. Linking writes a reference of the form ${{shared.KEY}} into that service. The exposure to watch is that a linked variable is available to the service's build as well as its runtime, and that sealed variables silently do not follow into PR environments, duplicated environments, or duplicated services.
How Linking Actually Works
Three things happen in sequence, and the middle one is the part people skip.
You add the variable under Project Settings, Shared Variables, picking the environment first. Production and staging each hold their own value under the same key.
You link it to specific services. Nothing reaches a service until you do this.
Railway writes a reference variable into the linked service, in the form ${{shared.KEY}}. It is a pointer, not a copy.
The pointer behavior is the useful half. Rotate STRIPE_SECRET_KEY once in Project Settings and every linked service picks up the new value on its next deploy, with no chance of one service being left on the old key. That is a genuine improvement over pasting the same secret into six Variables tabs.
# In the service's Variables tab
API_KEY=${{shared.API_KEY}}
# Resolves at build and deploy time to whatever
# Project Settings holds for the current environment
The Blast Radius Question
Because linking is manual, every link is a decision you made. The question worth asking on each one is not "does this service use the value" but "what else runs inside this service."
Railway injects variables into four contexts, per its own documentation: the build process for each service deployment, the running deployment, any command you run through railway run, and railway shell.
The first of those is the one that surprises people. A shared database password linked to a frontend service is present during that service's build, which means it's present while npm executes install scripts for every package in that dependency tree. You didn't grant a secret to your code. You granted it to your lockfile.
This is the concrete cost of a lazy link. Adding ${{shared.DATABASE_URL}} to a static frontend "so the build can prerender some pages" hands your production database credentials to several hundred packages' postinstall hooks. We flag this shape constantly in scans, and it almost never comes from someone being careless with the secret itself. It comes from someone being casual about which service gets the link.
The fix is boring and it works: link a shared variable to the services that need it at the moment they need it, and unlink when a service stops needing it. Unlinking is a real operation, not just deleting a line, and it's the step that never happens because nothing breaks when you skip it.
If your build genuinely needs a value that the browser will also see, that's a different problem with a different failure mode. Railway reference variables and the build-time leak covers what happens when a value ends up baked into a client bundle.
Sealed Variables and the Four Places They Don't Follow
Sealing is Railway's answer to "I don't want this readable from the dashboard." A sealed variable's value goes to builds and deployments but, in Railway's words, is "never visible in the UI nor can it be retrieved via the API."
It's the right control for production secrets. It also has a set of documented gaps that turn into outages if you don't know them going in.
| Situation | What happens to a sealed variable |
|---|---|
| Creating a PR environment | Not copied |
| Duplicating an environment | Not copied |
| Duplicating a service | Not copied |
railway variables or railway run from the CLI | Value not provided |
| Syncing environment changes | Not shown in the diff |
| External integrations | Not synced |
| Trying to un-seal it | Not possible, ever |
The PR environment row is the one that bites. You open a pull request, Railway spins up an environment, and the service crashes on boot because DATABASE_URL isn't there. Note the failure mode: the variable is absent, not empty. Your app doesn't quietly connect to the wrong database, it fails to start.
That's the good outcome. The bad outcome is what people do next.
The natural workaround is to add the missing value straight into the PR environment as a normal, unsealed variable so the preview finally boots. Now a production credential exists in plaintext, in a short-lived environment, readable from the dashboard and the API, on a branch anyone with repo access can push to. The seal you set up on production is doing nothing for the copy you just made of it.
Do this instead: give PR environments their own credentials. A separate database, a Stripe test key, a scoped-down API token. If a preview environment can reach production data, the preview environment is production as far as an attacker is concerned.
The un-sealing row deserves its own note. Since you can never read a sealed value back, store it somewhere durable before you seal it. Otherwise recovering it means rotating at the provider, which for something like a Stripe live key is a coordinated change across every service holding the old one.
A Five-Minute Audit
Open Project Settings, Shared Variables, and go environment by environment.
The fourth item is where the real findings tend to be. Preview environments get set up once, in a hurry, by whoever needed the PR to deploy that afternoon.
When a Service Variable Is the Better Choice
Shared variables earn their keep when several services need the same value and you want one rotation point. When only one service needs it, a plain service variable is stricter and costs you nothing.
A rough rule: if the answer to "which services need this" is one service, don't share it. If it's every service, ask why, because a value that genuinely every service needs is often either a project-wide config flag (fine to share) or a credential with far too much scope (worth splitting before you share it).
Are Railway shared variables automatically available to every service?
No. The variable is defined once at the project level, but each service has to link it before it shows up in that service's environment. Railway's docs describe the two ways to do it: the Share button in Project Settings, or the "Shared Variable" option in a service's Variables tab. Until you do one of those, the value exists and reaches nothing.
What is the syntax for a Railway shared variable?
${{shared.KEY}}, where KEY is the shared variable's name. A linked service ends up with a line like API_KEY=${{shared.API_KEY}}. Railway writes that reference for you during linking, so you rarely type it by hand, but it is worth recognising in a Variables tab because it tells you the value is a pointer to the project rather than a copy living on the service.
Are shared variables scoped per environment?
Yes. You choose the environment when adding one in Project Settings, so DATABASE_URL under production and DATABASE_URL under staging are separate values sharing a name. That is what makes the pattern work: a service references ${{shared.DATABASE_URL}} once and resolves to the right database depending on where it's deployed.
Why is my sealed variable missing in a Railway PR environment?
Railway does not copy sealed variables into PR environments, and the same is true when duplicating an environment or a service. The variable is absent rather than empty, so the service usually fails at startup instead of connecting somewhere unexpected. Give PR environments their own non-production credentials rather than pasting the production value in unsealed to get the preview running.
Can I un-seal a Railway variable?
No. Railway's documentation states that sealed variables cannot be un-sealed, and the value is not retrievable through the UI, the API, or railway variables from the CLI. Save the value in your own password manager before sealing it. Recovering it otherwise means rotating the credential at its source.
See What Your Deployed App Exposes
Our scanner checks your live site for secrets baked into client bundles, exposed config endpoints, and the misconfigurations that follow a variable into production.