Skip to content

Railway.Service reference

Source: src/Railway/Service.ts

A Railway.Service is a container in a Project. Point it at a public image (hashicorp/http-echo) or an Effect program (main). Alchemy stamps the name, creates a *.up.railway.app domain via serviceDomainCreate, and deploys.

Pass image without main. Railway pulls the image and runs it. url is the generated *.up.railway.app hostname.

const site = yield* Railway.Project("Site");
const api = yield* Railway.Service("Api", {
project: site,
image: "hashicorp/http-echo",
port: 5678,
});

A Service is a class. main: import.meta.url is the bundle entrypoint. Alchemy bundles this file with Rolldown, generates a Dockerfile (FROM node:26-slim), and uploads the context. Railway builds the image. build.install: ["pg"] ships pg unbundled.

export default class Api extends Railway.Service<Api>()(
"Api",
{
project: Site,
main: import.meta.url,
build: { install: ["pg"] },
},
Effect.gen(function* () {
return {
fetch: Effect.succeed(HttpServerResponse.text("hello")),
};
}),
) {}

context is a directory Railway builds with up. Mutually exclusive with image (without main) and repo. Docker ignore files apply.

const api = yield* Railway.Service("Api", {
project: site,
context: "./api",
port: 80,
});

Yield the Service in the Stack. api.url is https://{name}.up.railway.app.

export default Alchemy.Stack(
"MyApp",
{ providers: Railway.providers(), state: Alchemy.localState() },
Effect.gen(function* () {
const api = yield* Api;
return { url: api.url };
}),
);

publicDomain: false skips the generated *.up.railway.app hostname. url / domain stay unset. Reach it on the private mesh at {name}.railway.internal. Unowned generated or custom domains are left alone.

const worker = yield* Railway.Service("Worker", {
project: site,
image: "hashicorp/http-echo",
port: 5678,
publicDomain: false,
});

Omit region to leave placement alone. On first create that is the workspace default. Updating region moves the replicas in place.

const api = yield* Railway.Service("Api", {
project: site,
image: "hashicorp/http-echo",
port: 5678,
region: "us-west2",
});

healthcheckPath (or healthcheck, matching Railway IaC) is the HTTP path Railway probes. Railway load-balances public traffic across whatever replicas are running. Alchemy does not pin a count.

const api = yield* Railway.Service("Api", {
project: site,
image: "hashicorp/http-echo",
port: 5678,
healthcheck: "/health",
});

repo + branch is the third source, next to image and main. Railway must have GitHub connected to the account.

const api = yield* Railway.Service("Api", {
project: site,
repo: "acme/web",
branch: "main",
rootDirectory: "apps/api",
buildCommand: "pnpm build",
startCommand: "pnpm start",
});

Railway runs preDeploy.command after the image build and before start — the same setting as the dashboard Pre-deploy Command.

const api = yield* Railway.Service("Api", {
project: site,
image: "hashicorp/http-echo",
preDeploy: { command: "bun --cwd apps/api migrate" },
});

cronSchedule runs the service on a cron expression.

const worker = yield* Railway.Service("Worker", {
project: site,
image: "hashicorp/http-echo",
cronSchedule: "0 * * * *",
});

Bind MountVolume inside init. Provide MountVolumeLive.

export default class Api extends Railway.Service<Api>()(
"Api",
{ project: Site, image: "hashicorp/http-echo", port: 5678 },
Effect.gen(function* () {
const disk = yield* Railway.MountVolume(Data, { path: "/data" });
return {
fetch: Effect.succeed(HttpServerResponse.text(disk.path)),
};
}).pipe(Effect.provide(Railway.MountVolumeLive)),
) {}

Return methods next to fetch. Another Service or Function binds this class and calls them over {name}.railway.internal with a shared token. Public *.up.railway.app requests to /__rpc__/* get 401.

export default class Query extends Railway.Service<Query>()(
"Query",
{ project: Site, main: import.meta.url },
Effect.gen(function* () {
return {
greet: (name: string) => Effect.succeed(`hello ${name}`),
};
}),
) {}
export default class Api extends Railway.Function<Api>()(
"Api",
{ project: Site, main: import.meta.url },
Effect.gen(function* () {
const query = yield* Railway.bindService(Query);
return {
fetch: query
.greet("sam")
.pipe(Effect.map((greeting) => HttpServerResponse.text(greeting))),
};
}),
) {}

Declare the Project once. Pass it into every child. Resource-valued props accept the resource or an Effect producing it.

src/api.ts
import * as Railway from "alchemy/Railway";
export const Site = Railway.Project("Site");
export const Api = Railway.Service("Api", {
project: Site,
image: "hashicorp/http-echo",
port: 5678,
});

Source: src/Railway/Variable.ts

A Railway.Variable is a project- or service-scoped env var. Railway injects it into services in that environment. The plaintext is never stored in attributes.

Wrap the value with Redacted.make so it is never logged. Omit name and Alchemy generates an ownership-stamped name. Shared (no service) variables apply to every service in the environment.

const site = yield* Railway.Project("Site");
const dbUrl = yield* Railway.Variable("DatabaseUrl", {
project: site,
value: Redacted.make("postgres://…"),
});

name is the env-var services see. It is stored as-is (case-sensitive).

export const ApiToken = Railway.Variable("ApiToken", {
project: Site,
name: "API_TOKEN",
value: Redacted.make("sk_live_…"),
});

Defaults to the Project’s primary environment. Pass a Railway.Environment (or { environmentId }) 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_…"),
});

Pass service to attach the variable to one service instead of the shared project set.

const api = yield* Railway.Service("Api", {
project: site,
image: "hashicorp/http-echo",
});
const token = yield* Railway.Variable("ApiToken", {
project: site,
service: api,
name: "API_TOKEN",
value: Redacted.make("sk_live_…"),
});

value may be a Railway.ref(resource, key) template instead of a plaintext secret. Upsert stores ${{LogicalName.KEY}} (or ${{shared.NAME}}) — not a resolved URI. Railway interpolates it. Next to ConnectPostgres: use ConnectPostgres for a typed client inside an Effect-native Service; use Railway.ref when you want Railway’s own ${{Db.DATABASE_URL}} interpolation (IaC db.env.DATABASE_URL).

Reference Postgres DATABASE_URL

const db = yield* Railway.Postgres("Db", { project: site });
const api = yield* Railway.Service("Api", {
project: site,
image: "hashicorp/http-echo",
});
const databaseUrl = yield* Railway.Variable("DatabaseUrl", {
project: site,
service: api,
name: "DATABASE_URL",
value: Railway.ref(db, "DATABASE_URL"),
});

Shared variable

yield* Railway.Variable("SentryDsn", {
project: site,
name: "SENTRY_DSN",
value: Redacted.make("https://…"),
});
yield* Railway.Variable("ApiSentry", {
project: site,
service: api,
name: "SENTRY_DSN",
value: Railway.ref("shared", "SENTRY_DSN"),
});

Updating value is in place via variableUpsert. Deploys are skipped (skipDeploys: true); the Service resource owns deploys.

export const ApiToken = Railway.Variable("ApiToken", {
project: Site,
name: "API_TOKEN",
value: Redacted.make("sk_live_rotated"),
});

Declare the Project once. Pass it into every child. Resource-valued props accept the resource or an Effect producing it.

src/secrets.ts
import * as Railway from "alchemy/Railway";
import * as Redacted from "effect/Redacted";
export const Site = Railway.Project("Site");
export const ApiToken = Railway.Variable("ApiToken", {
project: Site,
name: "API_TOKEN",
value: Redacted.make("sk_live_…"),
});