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.
Prerequisites
Section titled “Prerequisites”- 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/amd64images and pushes them with an App deploy token.
Create a project
Section titled “Create a project”Start with an empty directory and initialize a package.json:
mkdir my-app && cd my-app && bun init -ymkdir my-app && cd my-app && npm init -ymkdir my-app && cd my-app && pnpm initmkdir my-app && cd my-app && yarn init -yInstall dependencies
Section titled “Install dependencies”Install alchemy@latest and effect@rc:
bun add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"npm install "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"pnpm add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"yarn add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"Create the Stack
Section titled “Create the Stack”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:
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) ― 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.
Configure state
Section titled “Configure state”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:
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 }),);Create the Service file
Section titled “Create the Service file”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:
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.
Serve HTTP with fetch
Section titled “Serve HTTP with fetch”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:
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.
Add the Service to the Stack
Section titled “Add the Service to the Stack”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:
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) ― 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.
Fix the Providers
Section titled “Fix the Providers”Replace Layer.empty with Fly.providers() to resolve the type
error:
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.
Return Stack outputs
Section titled “Return Stack outputs”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.
Deploy
Section titled “Deploy”Run alchemy deploy:
bun alchemy deploynpm run alchemy deploypnpm alchemy deployyarn alchemy deployThe 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.
Try it out
Section titled “Try it out”curl https://myapp-api-dev-a1b2c3d4.fly.dev# → Hello from Fly!Verify it worked
Section titled “Verify it worked”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.tswith a Stack and aFly.Service - A live Service in its own App with a generated globally unique name
- A public
https://{appName}.fly.devURL answering HTTP
In Part 2, you’ll pin the Service’s region and port, add a health route, and ship a code change.