Skip to content

Service

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")),
};
}),
) {}

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 };
}),
);

Omit region to use Railway’s default. Updating it is 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",
});

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,
});