Service
Source:
src/Fly/Service.ts
A Service is an Effect program running in a Fly.io Machine. Set
count to scale it up or down. Several Services share one App.
Declare a Service
Section titled “Declare a Service”A Service is a class. Props describe the Machine. The Effect is the program that runs on it.
app is the parent App. Pass the declaration directly,
yielded or module-scope. main: import.meta.url is the bundle
entrypoint. Alchemy bundles this file with Rolldown, builds a
Docker image (default oven/bun:1), and pushes it to
registry.fly.io/{app}:{id}-{hash}.
import * as Fly from "alchemy/Fly";import * as Effect from "effect/Effect";import { Site } from "./app.ts";
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url }, Effect.gen(function* () { return {}; }),) {}Serve HTTP with fetch
Section titled “Serve HTTP with fetch”Return fetch from the init Effect to boot an HTTP server. Omit
fetch for a background service.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}Pin a region
Section titled “Pin a region”Fly Machines live in a region. Default is iad. See
Regions for the list of codes.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, region: "iad" }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}Set the port
Section titled “Set the port”port is the port the process listens on inside the Machine.
Alchemy writes it to PORT. Default is 3000.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, region: "iad", port: 3000 }, 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://{appName}.fly.dev. Alchemy does not create this hostname.
It is the parent App’s fly.dev name. The Service does not
get its own URL.
export default Alchemy.Stack( "MyApp", { providers: Fly.providers(), state: Alchemy.localState() }, Effect.gen(function* () { const api = yield* Api; return { url: api.url }; }),);url is undefined when you pass services: [] (nothing is
published).
Fly’s proxy is the load balancer
Section titled “Fly’s proxy is the load balancer”There is no LoadBalancer resource. Fly runs an Anycast proxy at the edge.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, region: "iad", port: 3000 }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}Unless you override services, Alchemy publishes HTTP 80 and
HTTPS 443 on that proxy and points them at port inside each
Machine (internal_port). A request to
https://{appName}.fly.dev lands on Fly’s edge. Fly terminates
TLS on 443, picks one started Machine that published this service,
and forwards to port where fetch runs.
An address so it answers
Section titled “An address so it answers”{app}.fly.dev does not answer over IPv4 until the App has an
IpAssignment. Allocate a shared Anycast IPv4 on the same
App and yield it next to the Service.
export const PublicIp = Fly.IpAssignment("Shared", { app: Site, type: "shared_v4",});Effect.gen(function* () { const api = yield* Api; const ip = yield* PublicIp; return { url: api.url, ip: ip.ip };});Scale with count
Section titled “Scale with count”count is how many Machines to keep running. Default is 1. They
all publish the same proxy service, so they all sit behind
{app}.fly.dev. Fly’s proxy picks one Machine per request. Each
replica gets its own Volume from every MountVolume binding.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, region: "iad", count: 3, port: 3000 }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}Config
Section titled “Config”Yield Config in init. Alchemy reads the value from the env of
whoever deploys and writes it onto the Machine. Do not pass
env: { ... } on a Service.
Config.redacted("API_KEY") is Redacted<string>. Unwrap with
Redacted.value only where you need the raw string.
Alchemy also injects PORT (when port is set) and stack metadata.
For a secret Fly should own and inject into every Machine on the
App, use Secret.
import * as Config from "effect/Config";import * as Redacted from "effect/Redacted";
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, port: 3000 }, Effect.gen(function* () { const apiKey = yield* Config.redacted("API_KEY");
return { fetch: Effect.gen(function* () { const token = Redacted.value(apiKey); return HttpServerResponse.text("ok"); }), }; }),) {}Mount a disk
Section titled “Mount a disk”Bind MountVolume inside init. App and region come from the
Service. count: 3 creates three Volumes, one per replica. Provide
MountVolumeLive.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, region: "iad", count: 3, port: 3000 }, Effect.gen(function* () { const disk = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 }); const fs = yield* FileSystem.FileSystem; return { fetch: Effect.gen(function* () { const text = yield* fs.readFileString(`${disk.path}/hello.txt`); return HttpServerResponse.text(text); }), }; }).pipe(Effect.provide(Fly.MountVolumeLive)),) {}Guest size
Section titled “Guest size”guest is CPU kind, CPU count, and memory. Default is shared-cpu,
1 CPU, 256 MB. Set gpuKind and gpus for a GPU. Guest updates in
place.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, region: "iad", port: 3000, guest: { cpuKind: "shared", cpus: 2, memoryMb: 512 }, }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}A stable name
Section titled “A stable name”Machine names are unique per App. Omit name and Alchemy generates
one from the stack, stage, and logical ID.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, name: "api", port: 3000 }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}Named export
Section titled “Named export”handler is the named export to load from main. Default is
"default".
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, handler: "api", port: 3000 }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}Base image
Section titled “Base image”image is the generated Dockerfile’s FROM. Default is
oven/bun:1. It must still run bun. A content-hash change of
main updates the Machine in place.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, image: "oven/bun:1.2", port: 3000, }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}Custom proxy services
Section titled “Custom proxy services”services defaults to HTTP 80 + HTTPS 443 toward port. Pass a
custom list to change handlers or autostop. Pass [] so Fly does
not publish a proxy.
export default class Worker extends Fly.Service<Worker>()( "Worker", { app: Site, main: import.meta.url, region: "iad", services: [] }, Effect.gen(function* () { return {}; }),) {}Background services
Section titled “Background services”Omit port and fetch. Pass services: []. Use ServerHost.run
for a long-running loop. If the process exits, Fly restarts it.
import { ServerHost } from "alchemy/Server";
export default class Worker extends Fly.Service<Worker>()( "Worker", { app: Site, main: import.meta.url, region: "iad", services: [] }, Effect.gen(function* () { const host = yield* ServerHost;
yield* host.run( Effect.gen(function* () { return yield* Effect.never; }).pipe(Effect.orDie), ); }),) {}Bundle config
Section titled “Bundle config”build is Rolldown input / output overrides plus
pure-annotation options. Use it when main needs extra entry
points or externals.
Externals
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, port: 3000, build: { input: { external: ["sharp"] } }, }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}Install pg unbundled
pg is CommonJS. Rolldown’s interop turns Client into a namespace.
Install it into the image so @effect/sql-pg / Drizzle.Postgres load
it with Node’s CJS semantics — same build.install as Lambda.
export default class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, port: 3000, build: { install: ["pg"] }, }, Effect.gen(function* () { const conn = yield* Fly.ConnectPostgres(Db); const db = yield* Drizzle.Postgres(conn.connectionString); return { fetch: Effect.gen(function* () { const rows = yield* db.execute("select 1 as ok"); return HttpServerResponse.json({ rows }); }), }; }).pipe(Effect.provide(Fly.ConnectPostgresHttp)),) {}Multiple Services, one App
Section titled “Multiple Services, one App”Each Service has its own Machines, image, and lifecycle. Point
several at the same app.
class Api extends Fly.Service<Api>()( "Api", { app: Site, main: import.meta.url, port: 3000 }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello")), }; }),) {}
class Worker extends Fly.Service<Worker>()( "Worker", { app: Site, main: import.meta.url, services: [] }, Effect.gen(function* () { return {}; }),) {}