Defining a variable in Railway puts it in the project. It does not put it in any service. A service sees a value only when its own Variables tab holds a reference to it.
This is the source of most "why is my env var undefined on Railway" threads, and it is also the reason Railway's shared variables are safer than they look. Nothing is broadcast. Every service that holds a secret holds it because someone added the link.

Reading the diagram
The two grey boxes at the top are where values are defined. Neither one is a service you deploy code to.
Postgres service, left. When you add a Railway Postgres database, Railway sets DATABASE_URL on the database service itself. It does not copy it into your app. Plenty of first deploys crash on exactly this, because the value is visibly there in the dashboard, just on the wrong service.
Shared Variables, right. These live at the project level. Putting STRIPE_SECRET_KEY here defines it once, and that is all it does.
The two green boxes at the bottom are your services. Both are server-side, and the difference between them is only what their Variables tabs contain.
web service. It has two references, ${{Postgres.DATABASE_URL}} and ${{shared.STRIPE_SECRET_KEY}}, so both values arrive at build and at runtime. The edge labels are the references you type, which is the point: the arrow is the line in the Variables tab.
worker service. No references, so nothing arrives. process.env.STRIPE_SECRET_KEY is undefined there, even though the key sits one box away in the same project.
The fix for an undefined variable is a reference, not a copy. Pasting the raw value into the worker's Variables tab also works, but now you have two copies to rotate. A ${{shared.KEY}} reference keeps one source and follows rotations on the next deploy.
Why the missing arrow is a feature
The dotted line is the one people hit as a bug, but it is the safe default. A secret you did not link is a secret that service's dependencies cannot read.
That matters because a linked value is present during the build, while npm runs every install script in the dependency tree. Link your database URL to a static frontend "just in case" and it is available to all of those scripts, and if it sits behind a VITE_ or NEXT_PUBLIC_ prefix it gets compiled into the JavaScript you serve. Each arrow you add is a decision about who else gets the value, so add the ones a service actually uses and remove the ones it stopped using.
The Mermaid source
Copy this and rename the services to match your project.
---
title: "A variable only reaches services that link it"
---
flowchart TB
P["Postgres service<br/>Railway sets <code>DATABASE_URL</code> here"]
S["Shared Variables<br/><code>STRIPE_SECRET_KEY</code>"]
W["web service<br/>both references added"]
K["worker service<br/>no references added"]
P -->|"<code>${{Postgres.DATABASE_URL}}</code>"| W
S -->|"<code>${{shared.STRIPE_SECRET_KEY}}</code>"| W
S -.->|"not linked: <b>undefined</b>"| K
classDef private fill:#ecfdf5,stroke:#10b981,stroke-width:2px,color:#1c1917
classDef store fill:#f5f5f4,stroke:#57534e,stroke-width:2px,color:#1c1917
class P,S store
class W,K private
linkStyle 2 stroke:#57534e,stroke-width:3px
Why is DATABASE_URL undefined in my Railway service?
Because Railway sets DATABASE_URL on the Postgres service, not on your app. Your app only gets it when its own Variables tab contains a reference such as DATABASE_URL=${{Postgres.DATABASE_URL}}. Add the reference and Railway redeploys the service with the value.
Do Railway shared variables apply to every service automatically?
No. A shared variable is defined once at the project level and reaches nothing until a service links it, which writes a reference of the form ${{shared.KEY}} into that service. A service you never linked reads undefined.
Is a reference a copy of the value?
No, it is a pointer. Rotate the shared value once and every service that references it picks up the new value on its next deploy. That is the main reason to use shared variables for keys several services need.
Does linking a variable expose it anywhere new?
A linked variable is present during the service's build as well as at runtime, so it is visible to install scripts and, behind a VITE_ or NEXT_PUBLIC_ prefix, gets compiled into your frontend bundle. Link each value only to the services that need it.
Did a Railway variable end up in your frontend?
A scan reads your live site the way a visitor would and reports any keys it finds in the bundle.