The context argument documented in the IaC reference is never populated. ctx.environment, ctx.environmentName, ctx.isEnvironment(), ctx.projectId and ctx.shared are all undefined, so a single authoring file cannot render different desired state per environment — which is the documented purpose of the context.
Reproduction
.railway/railway.ts:
import { defineRailway } from "railway/iac";
export default defineRailway((ctx) => {
throw new Error(`ctx = ${JSON.stringify(ctx)}`);
});
$ railway config plan
Error: ctx = {}
Object.getOwnPropertyNames(ctx) is also empty, so it is not a matter of non-enumerable properties.
Root cause
src/iac/eval.rs, in evaluate_javascript:
const project = typeof exported === "function" ? await exported({}) : exported;
The empty object is a literal inside the wrapper script passed to node --eval. The context is never constructed, and createRailwayContext — which the TypeScript SDK exports for exactly this — is not called anywhere in the CLI.
Because it is a constant in the wrapper, this is not platform- or Node-specific. Reproduced on Windows with Node 23.3.0, and #1117 reports the same missing context on macOS with Node 26.7.0, reaching it through ctx.shared instead.
evaluate_file takes only &Path, so nothing about the project or environment can reach the evaluator today.
The values are already at the call site
engine::run holds everything RailwayContextInput declares, two lines above the call to evaluate_file:
LinkedProject field |
context field |
project |
projectId |
name |
projectName |
environment |
environmentId |
environment_name |
environmentName |
plus the command: &str parameter of run itself.
Suggested fix
Thread a context value through evaluate_file / evaluate_javascript and build it in the wrapper with the SDK's own createRailwayContext, which already normalises environment / environmentName and supplies isEnvironment and randomString.
ctx.shared needs more than parameter threading: the shared variables come from fetch_current_environment, which runs after evaluate_file. That half is #1117 and could be handled separately — the static fields alone are enough to unblock per-environment configuration.
The Python and Go evaluators pass no context either.
Workaround, for anyone who lands here
Until the context is populated, per-environment differences cannot be expressed in one file. Splitting the definition into a shared module plus one thin entry file per environment works, selected with --file:
.railway/stack.ts the topology, parameterised
.railway/railway.ts staging
.railway/railway.production.ts production
railway config plan
railway config plan --file .railway/railway.production.ts
Note that the file does not choose the environment — railway link does — so planning the production file against a staging link is accepted and applies production values to staging. Since the context cannot be read, the only guard available is shelling out to railway status from the authoring file and refusing to build when the linked environment disagrees with the file.
Environment
- Railway CLI 5.45.0
- Node 23.3.0
- Windows 11
railway npm package 3.11.0
The context argument documented in the IaC reference is never populated.
ctx.environment,ctx.environmentName,ctx.isEnvironment(),ctx.projectIdandctx.sharedare allundefined, so a single authoring file cannot render different desired state per environment — which is the documented purpose of the context.Reproduction
.railway/railway.ts:Object.getOwnPropertyNames(ctx)is also empty, so it is not a matter of non-enumerable properties.Root cause
src/iac/eval.rs, inevaluate_javascript:The empty object is a literal inside the wrapper script passed to
node --eval. The context is never constructed, andcreateRailwayContext— which the TypeScript SDK exports for exactly this — is not called anywhere in the CLI.Because it is a constant in the wrapper, this is not platform- or Node-specific. Reproduced on Windows with Node 23.3.0, and #1117 reports the same missing context on macOS with Node 26.7.0, reaching it through
ctx.sharedinstead.evaluate_filetakes only&Path, so nothing about the project or environment can reach the evaluator today.The values are already at the call site
engine::runholds everythingRailwayContextInputdeclares, two lines above the call toevaluate_file:LinkedProjectfieldprojectprojectIdnameprojectNameenvironmentenvironmentIdenvironment_nameenvironmentNameplus the
command: &strparameter ofrunitself.Suggested fix
Thread a context value through
evaluate_file/evaluate_javascriptand build it in the wrapper with the SDK's owncreateRailwayContext, which already normalisesenvironment/environmentNameand suppliesisEnvironmentandrandomString.ctx.sharedneeds more than parameter threading: the shared variables come fromfetch_current_environment, which runs afterevaluate_file. That half is #1117 and could be handled separately — the static fields alone are enough to unblock per-environment configuration.The Python and Go evaluators pass no context either.
Workaround, for anyone who lands here
Until the context is populated, per-environment differences cannot be expressed in one file. Splitting the definition into a shared module plus one thin entry file per environment works, selected with
--file:Note that the file does not choose the environment —
railway linkdoes — so planning the production file against a staging link is accepted and applies production values to staging. Since the context cannot be read, the only guard available is shelling out torailway statusfrom the authoring file and refusing to build when the linked environment disagrees with the file.Environment
railwaynpm package 3.11.0