Skip to content

Services

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.

A Service is a class. Props describe the Machine. The Effect is the program that runs on it.

src/api.ts
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 {};
}),
) {}

app is the parent App. Pass the declaration directly, yielded or module-scope. Changing app replaces the Service.

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}.

Return fetch from the init Effect to boot an HTTP server.

import * as Fly from "alchemy/Fly";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import { Site } from "./app.ts";
export default class Api extends Fly.Service<Api>()(
"Api",
{ app: Site, main: import.meta.url },
Effect.gen(function* () {
return {};
return {
fetch: Effect.succeed(HttpServerResponse.text("hello")),
};
}),
) {}

Omit fetch for a background service.

Fly Machines live in a region. The default is iad. Changing region replaces the Service.

export default class Api extends Fly.Service<Api>()(
"Api",
{ app: Site, main: import.meta.url },
{ app: Site, main: import.meta.url, region: "iad" },

port is the port the process listens on inside the Machine. Alchemy writes it to PORT. Default 3000.

export default class Api extends Fly.Service<Api>()(
"Api",
{ app: Site, main: import.meta.url, region: "iad" },
{ app: Site, main: import.meta.url, region: "iad", port: 3000 },

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: [].

There is no LoadBalancer resource. Fly runs an Anycast proxy at the edge. Unless you override services, Alchemy publishes HTTP 80 and HTTPS 443 on that proxy and points them at port inside each Machine.

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.

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

count is how many Machines to keep running. Default 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", port: 3000 },
{ app: Site, main: import.meta.url, region: "iad", count: 3, port: 3000 },

Docker must be running.

Alchemy bundles main with Rolldown. If the hash matches the last deploy, it skips build and push. Otherwise it builds linux/amd64 from image (default oven/bun:1) and pushes to registry.fly.io/{app}:{id}-{hash}. Then it creates or updates count Machines and waits until they are started.

Changed code is a new image and an in-place Machine update. Unchanged code is a no-op.

Override the base image with image (must still run bun). Pass services: [] for a process that should not be published.

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.

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

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 Fly.Secret.

Omit port and fetch. Pass services: [] so Fly does not publish a proxy. Use ServerHost.run for a long-running loop:

src/worker.ts
import * as Fly from "alchemy/Fly";
import { ServerHost } from "alchemy/Server";
import * as Effect from "effect/Effect";
import { Site } from "./app.ts";
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),
);
}),
) {}

If the process exits, Fly restarts it.

Each Service has its own Machines, image, env, and lifecycle. Point several at the same app.

class Api extends Fly.Service<Api>()(
"Api",
{ app: Site, main: import.meta.url, port: 3000 },
/* HTTP */
) {}
class Worker extends Fly.Service<Worker>()(
"Worker",
{ app: Site, main: import.meta.url, services: [] },
/* mounts a disk, writes files */
) {}

A Volume attaches to one Machine. MountVolume with count: 3 creates three disks.

Machine logs live in the Fly dashboard and fly logs. alchemy logs / alchemy tail don’t support Fly Services yet.

The tutorial builds a Service step by step. Sprites are org-scoped sandboxes that hibernate — no parent App, no Docker image. Regions lists codes and how Volumes follow. Volumes covers MountVolume. Postgres binds with ConnectPostgres. Redis binds with ReadWriteRedis. Tigris binds with PutObject / GetObject. Secrets covers Config.redacted and Fly.Secret. The Service reference lists every prop.