Skip to content

Variables

Most secrets in a Service come from your .env. Yield Config.Redacted in the constructor. Alchemy binds the value onto the Service.

Use Railway.Variable when the value should be shared across services and managed in one place by Railway. The plaintext is never stored in attributes.

import * as Config from "effect/Config";
import * as Redacted from "effect/Redacted";
export default class Api extends Railway.Service<Api>()(
"Api",
{
project: Site,
main: import.meta.url,
port: 3000,
},
Effect.gen(function* () {
const apiKey = yield* Config.Redacted("API_KEY");
return {
fetch: Effect.gen(function* () {
const token = Redacted.value(apiKey);
// ...
}),
};
}),
) {}

Config.Redacted("API_KEY") reads API_KEY from the env of whoever runs the deploy and writes it onto the Service. At runtime the same line resolves from that env var.

The value is Redacted<string>. Unwrap with Redacted.value only where you need the raw string. Do not pass env: { ... } on a Service. Yield Config.

See Secrets & Config for combinators and stages.

.env is only the default. A Stack’s secrets option reads the same Config values from a secrets manager instead, like Doppler or Infisical, so production keys never sit in a file on anyone’s machine. Pick the source per stage:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Railway from "@alchemy.run/railway";
import * as Doppler from "@alchemy.run/doppler";
import * as Effect from "effect/Effect";
import Api from "./src/api.ts";
export default Alchemy.Stack(
"MyApp",
{
providers: Railway.providers(),
state: Alchemy.localState(),
secrets: ({ stage }) =>
stage === "prod"
? Doppler.Secrets({ project: "my-app", config: "prd" })
: Alchemy.Secrets.DotEnv(),
},
Effect.gen(function* () {
const api = yield* Api;
return { url: api.url };
}),
);

Your code doesn’t change: Config.Redacted("API_KEY") reads from whichever source the stage uses. See Secret providers for Doppler, Infisical, ordering several sources, and writing your own.

Railway.Variable is injected as an environment variable into services in that environment. Use it when the value is shared and managed in one place, not when only this Service reads a deploy-time .env key.

import * as Railway from "@alchemy.run/railway";
import * as Redacted from "effect/Redacted";
export const ApiToken = Railway.Variable("ApiToken", {
project: Site,
name: "API_TOKEN",
value: Redacted.make("…"),
});

name is the env-var services see. It is stored as-is (case-sensitive). Omit it and Alchemy generates an ownership-stamped name. Updating value is in place via variableUpsert. Changing project, environment, service, or name replaces.

The resolved resource exposes name and digest. Never the plaintext.

Shared (no service) variables apply to every service in the environment. Pass service to scope one Variable to one Service.

const token = yield* Railway.Variable("ApiToken", {
project: site,
service: api,
name: "API_TOKEN",
value: Redacted.make("sk_live_…"),
});

Railway injects the Variable as env. Read it with Config — never log the plaintext.

export default class Api extends Railway.Service<Api>()(
"Api",
{
project: Site,
main: import.meta.url,
port: 3000,
},
Effect.gen(function* () {
yield* ApiToken;
const name = yield* Config.String("API_TOKEN").pipe(
Effect.orElseSucceed(() => ""),
);
return {
fetch: Effect.succeed(
HttpServerResponse.json({ name: "API_TOKEN", set: name.length > 0 }),
),
};
}),
) {}

Yield the Variable in the Stack (or from the Service’s constructor) so it is part of the graph.

Railway.ref emits a Railway template (${{Service.KEY}} or ${{shared.NAME}}). Store it as a Variable value or a Service.env entry. Railway keeps the template (unrendered: true) and interpolates it — it is not a resolved URI. Distinct from the unexported resource-prop Ref<T>.

Use ConnectPostgres / ConnectMySQL / ConnectMongo for a typed client inside an Effect-native Service. Use Railway.ref when you want Railway’s own interpolation (IaC db.env.DATABASE_URL, ctx.shared.SENTRY_DSN).

const db = yield* Railway.Postgres("Db", { project: site });
yield* Railway.Variable("DatabaseUrl", {
project: site,
service: api,
name: "DATABASE_URL",
value: Railway.ref(db, "DATABASE_URL"),
});

Shared variables use the "shared" namespace:

yield* Railway.Variable("ApiSentry", {
project: site,
service: api,
name: "SENTRY_DSN",
value: Railway.ref("shared", "SENTRY_DSN"),
});

Railway interpolates by service name. Set name on Postgres/Service to the LogicalId if the template must resolve to that service.

Defaults to the Project’s primary environment. Pass a Environment to target another one.

const staging = yield* Railway.Environment("Staging", { project: site });
const token = yield* Railway.Variable("StagingToken", {
project: site,
environment: staging,
value: Redacted.make("sk_staging_…"),
});

The tutorial stores a Variable and reads it from a Service. Services covers Config.Redacted for values from .env. Postgres, MySQL, Mongo, Redis, and Buckets bind typed clients (ConnectPostgres, ConnectMySQL, ConnectMongo, ReadWriteRedis, PutObject) that use Railway-owned variables internally. See the Variable reference and ref reference.