Skip to content

Function

Source: src/Railway/Function.ts

A Railway.Function is a single TypeScript file on Railway’s Bun canvas runtime. No GitHub repo. No Docker. No registry. Alchemy queries functionRuntime(bun), creates the Service with that image, and writes the source as startCommand (./run.sh + base64).

There are two ways to define a Function. Prefer async (main + export default { fetch }, no Effect) — Effect-native canvas Functions pull the Effect runtime into the 96KB encoded start-command cap and fail FunctionTooLarge for anything non-trivial. Use Service when you need an Effect program or a real image.

Distinct from Effect-native Service (main), which bundles with Rolldown and uploads a generated Dockerfile for Railway to build.

You don’t have to use Effect. Declare the Function with main pointing at a file and no Effect.gen implementation — Alchemy bundles that file as-is (no Effect runtime) and deploys it as a canvas Function. The handler is a plain async fetch. Use the env prop to declare variables, and InferEnv to type the second argument (Railway bindings are environment variables).

Defining an async Function in your stack

const db = yield* Railway.Postgres("Db", { project: site });
export const Ping = Railway.Function("Ping", {
project: site,
main: "./src/ping.ts",
env: { DATABASE_URL: db.connectionUri },
});
export type PingEnv = Railway.InferEnv<typeof Ping>;

Writing the async handler

import type { PingEnv } from "../alchemy.run.ts";
export default {
async fetch(_request: Request, env: PingEnv) {
return new Response(env.DATABASE_URL ? "ok" : "missing");
},
};

A Function is a class. Props describe the canvas Function. The Effect is the program that runs in it. main: import.meta.url is the bundle entrypoint — Alchemy bundles this file into one JS file and deploys it. No registry. Stay tiny: the encoded start command maxes out at 96KB (effect plus handlers overflows it).

export default class Ping extends Railway.Function<Ping>()(
"Ping",
{
project: Site,
main: import.meta.url,
},
Effect.gen(function* () {
return {
fetch: Effect.succeed(HttpServerResponse.text("ok")),
};
}),
) {}

Pass inline source (or path to a .ts file) instead of main when the Function is not an Effect class. Alchemy generates a *.up.railway.app domain unless http: false or a cron schedule is set.

const site = yield* Railway.Project("Site");
const ping = yield* Railway.Function("Ping", {
project: site,
source: `
Bun.serve({
hostname: "0.0.0.0",
port: Number(process.env.PORT ?? 3000),
fetch() { return new Response("ok"); },
});
`,
});

path is the CLI --path equivalent. The file is read at plan/reconcile. Content changes update in place.

const ping = yield* Railway.Function("Ping", {
project: site,
path: "./fn.ts",
});

cronSchedule runs the Function on a cron expression (--cron). HTTP domains are skipped unless http: true is set explicitly.

const job = yield* Railway.Function("Cleanup", {
project: site,
source: `console.log("tick");`,
cronSchedule: "0 * * * *",
});

sleepApplication sleeps the Function when idle (--serverless).

const ping = yield* Railway.Function("Ping", {
project: site,
source: `console.log("hi");`,
http: false,
sleepApplication: true,
});

Return methods next to fetch. Call enableRailwayRpc() in init (canvas Functions are capped at 96KB; the dispatcher is opt-in). Another Function or Service 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.Function<Query>()(
"Query",
{ project: Site, main: import.meta.url },
Effect.gen(function* () {
Railway.enableRailwayRpc();
return {
greet: (name: string) => Effect.succeed(`hello ${name}`),
};
}),
) {}
export default class Api extends Railway.Service<Api>()(
"Api",
{ project: Site, main: import.meta.url },
Effect.gen(function* () {
const query = yield* Railway.bindFunction(Query);
return {
fetch: query
.greet("sam")
.pipe(Effect.map((greeting) => HttpServerResponse.text(greeting))),
};
}),
) {}

Resource-valued props accept the resource or an Effect producing it.

src/ping.ts
import * as Railway from "alchemy/Railway";
export const Site = Railway.Project("Site");
export const Ping = Railway.Function("Ping", {
project: Site,
source: `console.log("hello");`,
http: false,
});