Skip to content

Part 1: Your First Service

Install Alchemy and Effect, create a Stack, and deploy a Fly.Service: an Effect program running in a Fly Machine. The Service creates its own Fly App, so it gets its own public https://{name}.fly.dev URL. Alchemy bundles it, builds a Docker image, and pushes it to registry.fly.io. No Dockerfile. No fly.toml.

  • Bun or Node.js 22+
  • A Fly organization and an API token — see Setup if you haven’t created those yet
  • Docker running locally. Alchemy builds linux/amd64 images and pushes them with an App deploy token.

Start with an empty directory and initialize a package.json:

Terminal window
mkdir my-app && cd my-app && bun init -y

Install alchemy@latest and effect@rc:

Terminal window
bun add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"

Every Alchemy program starts with a Stack — a collection of Resources managed by Providers with state tracked between deploys.

Create an alchemy.run.ts file:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
export default Alchemy.Stack(
"MyApp",
{
Error ts(2345) ― Argument of type '{ providers: Layer.Layer<never, never, never>; }' is not assignable to parameter of type 'StackProps<never>'. Property 'state' is missing in type '{ providers: Layer.Layer<never, never, never>; }' but required in type 'StackProps<never>'.
providers: Layer.empty,
},
Effect.gen(function* () {
// we'll add resources here next
}),
);

TypeScript is unhappy: the state property is required. Every Stack needs a state store so Alchemy can persist resource state between deploys and compute diffs against your infrastructure.

Fly has no state backend of its own, so we’ll keep state on disk with Alchemy.localState() — it writes to .alchemy/ next to your code, no setup required:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
export default Alchemy.Stack(
"MyApp",
{
providers: Layer.empty,
state: Alchemy.localState(),
},
Effect.gen(function* () {
// we'll add resources here next
}),
);

A Service in Alchemy is a class — it has both an infrastructure definition and a runtime implementation expressed as an Effect. Create src/api.ts with the smallest possible declaration:

src/api.ts
import * as Fly from "alchemy/Fly";
import * as Effect from "effect/Effect";
export default class Api extends Fly.Service<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
return {};
}),
) {}

The <Api> type argument plus the empty () is a one-time bit of ceremony — it lets TypeScript reason about Api as a typed handle. main: import.meta.url tells Alchemy this same file is the bundle entrypoint. The Service creates and manages its own Fly App; you don’t declare one.

Add a fetch field — Alchemy treats anything returned from the Effect.gen block as the runtime API, and fetch specifically is wired to an HTTP server listening on the Service’s port:

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

HttpServerResponse is the same effect/http API used on every other Alchemy runtime — the handler you write here would run unchanged on Cloudflare Workers or AWS Lambda.

The Api class is just a typed identifier — yielding it inside the Stack’s Effect is what registers the resource. Yield it and observe the type error:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import Api from "./src/api.ts";
export default Alchemy.Stack(
"MyApp",
{
providers: Layer.empty,
Error ts(2322) ― Type 'Layer<never, never, never>' is not assignable to type 'Layer<NoInfer<Providers>, never, StackServices>'. Type 'Providers' is not assignable to type 'never'.
state: Alchemy.localState(),
},
Effect.gen(function* () {
// we'll add resources here next
const api = yield* Api;
}),
);

TypeScript is telling us that Layer.empty doesn’t provide Fly.Providers — the layer required by Service.

Replace Layer.empty with Fly.providers() to resolve the type error:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Fly from "alchemy/Fly";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import Api from "./src/api.ts";
export default Alchemy.Stack(
"MyApp",
{
providers: Fly.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const api = yield* Api;
}),
);

Now the program type-checks. The providers layer tells Alchemy how to talk to the Fly Machines API, and the type system ensures you never forget to wire it up.

Stack outputs let you see important values after a deploy. Return the Service’s App name and public URL:

Effect.gen(function* () {
const api = yield* Api;
return {
appName: api.appName,
url: api.url,
};
}),

Omit name and Alchemy generates a globally unique, DNS-safe App name from the stack, stage, and logical ID. A Service is public by default: Alchemy allocates a free shared IPv4 and IPv6 on its App, and url is https://{appName}.fly.dev.

Run alchemy deploy:

Terminal window
bun alchemy deploy

The first time you deploy, Alchemy prompts for Fly credentials — paste the API token you generated in Setup. The token is verified and saved to your default profile, so you won’t be asked again.

Plan: 1 to create

+ Api (Fly.Service)

Proceed?
◉ Yes ○ No
✓ Api (Fly.Service) created
{
  appName: "myapp-api-dev-a1b2c3d4",
  url: "https://myapp-api-dev-a1b2c3d4.fly.dev",
}

Alchemy created the Service’s App and its addresses, bundled src/api.ts with Rolldown, built a linux/amd64 image, and pushed it to registry.fly.io/{app}:{id}-{hash}. It then created a Machine in iad, waited until it was started, and pointed the Fly proxy at the Service’s port.

Terminal window
curl https://myapp-api-dev-a1b2c3d4.fly.dev
# → Hello from Fly!

The App is listed in your org in the Fly dashboard. Run alchemy deploy again. Because nothing changed, the Service shows as a no-op:

Plan: no changes

{
  appName: "myapp-api-dev-a1b2c3d4",
  url: "https://myapp-api-dev-a1b2c3d4.fly.dev",
}

This is the core loop — declare resources in code, deploy, and Alchemy figures out what changed.

You now have:

  • An alchemy.run.ts with a Stack and a Fly.Service
  • A live Service in its own App with a generated globally unique name
  • A public https://{appName}.fly.dev URL answering HTTP

In Part 2, you’ll pin the Service’s region and port, add a health route, and ship a code change.