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.
Image service
Section titled “Image service”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,});Effect-native Service
Section titled “Effect-native Service”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")), }; }),) {}The public URL
Section titled “The public URL”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 }; }),);Pin a region
Section titled “Pin a region”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",});Healthcheck
Section titled “Healthcheck”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",});GitHub source
Section titled “GitHub source”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 * * * *",});Mount a disk
Section titled “Mount a disk”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)),) {}Schemaless RPC
Section titled “Schemaless RPC”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))), }; }),) {}Module-scope declarations
Section titled “Module-scope declarations”Declare the Project once. Pass it into every child. Resource-valued props accept the resource or an Effect producing it.
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,});