Skip to content

Part 4: Variables and Cleanup

A secret only this Service needs from .env is Config.redacted. See Variables.

This part is for a value Railway should own: a Variable Railway injects into services. You will read it with Config, then tear the stack down so billing stops.

Add a Variable next to the Project. Wrap the value with Redacted.make so it is never logged:

src/project.ts
import * as Railway from "alchemy/Railway";
import * as Redacted from "effect/Redacted";
export const Site = Railway.Project("Site");
export const Data = Railway.Volume("Data", { /* ... */ });
export const ApiToken = Railway.Variable("ApiToken", {
project: Site,
name: "API_TOKEN",
value: Redacted.make("not-a-real-token"),
});

name is the env-var Railway injects into services (API_TOKEN). Omit it and Alchemy generates an ownership-stamped name instead.

Railway injects the Variable as env. Yield it in init so it is part of the graph, then read the name with Config — never the plaintext:

src/api.ts
import { Data, Site } from "./project.ts";
import { ApiToken, Data, Site } from "./project.ts";
import * as Config from "effect/Config";
Effect.gen(function* () {
const mount = yield* Railway.MountVolume(Data, { path: "/data" });
const fs = yield* FileSystem.FileSystem;
yield* ApiToken;
const tokenName = "API_TOKEN";
const token = yield* Config.string(tokenName).pipe(
Effect.orElseSucceed(() => ""),
);
return {
fetch: /* ... */,
};
}).pipe(Effect.provide(Railway.MountVolumeLive)),

Add a /secret route that returns the variable’s name, never the plaintext:

if (url.pathname === "/health") {
return HttpServerResponse.json({ ok: true });
}
if (url.pathname === "/secret") {
return HttpServerResponse.json({
name: tokenName,
set: token.length > 0,
});
}
const file = `${mount.path}${url.pathname}`;
alchemy.run.ts
import { Data, Site } from "./src/project.ts";
import { ApiToken, Data, Site } from "./src/project.ts";
Effect.gen(function* () {
const site = yield* Site;
yield* Data;
yield* ApiToken;
const api = yield* Api;
Terminal window
bun alchemy deploy
Plan: 1 to create, 1 to update

+ ApiToken (Railway.Variable)
~ Api (Railway.Service)

Proceed?
◉ Yes ○ No
 ApiToken (Railway.Variable) created
 Api (Railway.Service) updated
Terminal window
curl https://myapp-api-dev-a1b2c3d4.up.railway.app/secret
# → {"name":"API_TOKEN","set":true}

You’re done — tear everything down so Service and Volume billing stops:

Terminal window
bun alchemy destroy

Alchemy deletes everything in reverse dependency order — Service, Variable, Volume, Project.

Over four parts you built a complete Railway deployment:

  • A Project with a generated unique name and a production environment
  • An HTTP Service bundled into a container, updated only when its code hash changes
  • A Volume mounted at /data whose contents outlive deploys
  • A Variable readable from the Service, never logged
  • Services — background workers, env, multiple Services per Project, image-based Services.
  • Volumes — attach at create time, region rules.
  • VariablesConfig.redacted vs Railway.Variable.
  • Postgres — bind ConnectPostgres and query with Drizzle.
  • Testing — deploy this stack from an integration test and drive it over HTTP.
  • CI — run alchemy deploy from GitHub Actions with RAILWAY_API_TOKEN.