Skip to content

Part 1: Your First App

Install Alchemy and Effect, create a Stack with a Fly App, and deploy it. An App is the parent for Machines, Services, Secrets, and disks.

  • Bun (recommended) or Node.js 22+
  • A Fly organization and an API token — see Setup if you haven’t created those yet

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

Resources represent cloud infrastructure. Each resource is yield*-ed inside the Stack’s Effect generator.

Let’s declare a Fly App and observe 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";
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* () {
const site = yield* Fly.App("Site");
}),
);

Omit name and Alchemy generates a globally unique, DNS-safe App name (max 30 characters, leading letter). The org defaults to whatever org your token belongs to.

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

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";
export default Alchemy.Stack(
"MyApp",
{
providers: Fly.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const site = yield* Fly.App("Site");
}),
);

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 an object from the generator to expose the App’s name and public URL:

Effect.gen(function* () {
const site = yield* Fly.App("Site");
return {
appName: site.appName,
url: site.url,
};
}),

url is https://{appName}.fly.dev. Nothing is listening there yet — that’s Part 2.

Run alchemy deploy to create the App:

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

+ Site (Fly.App)

Proceed?
◉ Yes ○ No
 Site (Fly.App) created
{
  appName: "myapp-site-dev-a1b2c3d4",
  url: "https://myapp-site-dev-a1b2c3d4.fly.dev",
}

Alchemy shows a plan, asks for confirmation, creates the App, and prints the stack outputs.

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

Plan: no changes

{
  appName: "myapp-site-dev-a1b2c3d4",
  url: "https://myapp-site-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 App
  • A live App in your org with a generated globally unique name
  • Stack outputs showing appName and https://{appName}.fly.dev

In Part 2, you’ll deploy code to this App — a Fly.Service that bundles an Effect HTTP handler into a Machine.