| 1 | --- |
| 2 | title: "Part 1: Your First Service" |
| 3 | description: Install Alchemy, create a Stack, and deploy an Effect HTTP Service to Fly on its own fly.dev hostname. |
| 4 | sidebar: |
| 5 | order: 1 |
| 6 | --- |
| 7 | |
| 8 | import Terminal from "../../../../components/Terminal.astro"; |
| 9 | import { Code, Tabs, TabItem } from "@astrojs/starlight/components"; |
| 10 | import { effectVersion } from "../../../../versions"; |
| 11 | |
| 12 | export const pkgs = `"alchemy@latest" "effect@${effectVersion}" "@effect/platform-bun@${effectVersion}" "@effect/platform-node@${effectVersion}"`; |
| 13 | |
| 14 | Install Alchemy and Effect, create a Stack, and deploy a |
| 15 | `Fly.Service`: an Effect program running in a Fly Machine. The |
| 16 | Service creates its own Fly App, so it gets its own public |
| 17 | `https://{name}.fly.dev` URL. Alchemy bundles it, builds a Docker |
| 18 | image, and pushes it to `registry.fly.io`. No Dockerfile. No |
| 19 | `fly.toml`. |
| 20 | |
| 21 | ## Prerequisites |
| 22 | |
| 23 | - [Bun](https://bun.sh) or Node.js 22+ |
| 24 | - A Fly organization and an API token — see [Setup](/fly/setup) if |
| 25 | you haven't created those yet |
| 26 | - Docker running locally. Alchemy builds `linux/amd64` images and |
| 27 | pushes them with an App deploy token. |
| 28 | |
| 29 | :::note[Costs] |
| 30 | The Service's App and addresses are free. Its Machine (shared-cpu, |
| 31 | 256 MB) is billed by the second. `alchemy destroy` tears everything |
| 32 | down when you're done. |
| 33 | ::: |
| 34 | |
| 35 | ## Create a project |
| 36 | |
| 37 | Start with an empty directory and initialize a `package.json`: |
| 38 | |
| 39 | <Tabs syncKey="pkgManager"> |
| 40 | <TabItem label="bun" icon="bun"> |
| 41 | <Code code="mkdir my-app && cd my-app && bun init -y" lang="sh" /> |
| 42 | </TabItem> |
| 43 | <TabItem label="npm" icon="npm"> |
| 44 | <Code code="mkdir my-app && cd my-app && npm init -y" lang="sh" /> |
| 45 | </TabItem> |
| 46 | <TabItem label="pnpm" icon="pnpm"> |
| 47 | <Code code="mkdir my-app && cd my-app && pnpm init" lang="sh" /> |
| 48 | </TabItem> |
| 49 | <TabItem label="yarn" icon="seti:yarn"> |
| 50 | <Code code="mkdir my-app && cd my-app && yarn init -y" lang="sh" /> |
| 51 | </TabItem> |
| 52 | </Tabs> |
| 53 | |
| 54 | ## Install dependencies |
| 55 | |
| 56 | Install <code>alchemy@latest</code> and <code>effect@{effectVersion}</code>: |
| 57 | |
| 58 | <Tabs syncKey="pkgManager"> |
| 59 | <TabItem label="bun" icon="bun"> |
| 60 | <Code code={`bun add ${pkgs}`} lang="sh" /> |
| 61 | </TabItem> |
| 62 | <TabItem label="npm" icon="npm"> |
| 63 | <Code code={`npm install ${pkgs}`} lang="sh" /> |
| 64 | </TabItem> |
| 65 | <TabItem label="pnpm" icon="pnpm"> |
| 66 | <Code code={`pnpm add ${pkgs}`} lang="sh" /> |
| 67 | </TabItem> |
| 68 | <TabItem label="yarn" icon="seti:yarn"> |
| 69 | <Code code={`yarn add ${pkgs}`} lang="sh" /> |
| 70 | </TabItem> |
| 71 | </Tabs> |
| 72 | |
| 73 | ## Create the Stack |
| 74 | |
| 75 | Every Alchemy program starts with a `Stack` — a collection of |
| 76 | Resources managed by Providers with state tracked between deploys. |
| 77 | |
| 78 | Create an `alchemy.run.ts` file: |
| 79 | |
| 80 | ```typescript |
| 81 | // alchemy.run.ts |
| 82 | import * as Alchemy from "alchemy"; |
| 83 | import * as Effect from "effect/Effect"; |
| 84 | import * as Layer from "effect/Layer"; |
| 85 | |
| 86 | export default Alchemy.Stack( |
| 87 | "MyApp", |
| 88 | { |
| 89 | // @error: ts(2345) Argument of type '{ providers: Layer.Layer<never, never, never>; }' is not assignable to parameter of type 'StackProps<never>'. |
| 90 | // @error: Property 'state' is missing in type '{ providers: Layer.Layer<never, never, never>; }' but required in type 'StackProps<never>'. |
| 91 | providers: Layer.empty, |
| 92 | }, |
| 93 | Effect.gen(function* () { |
| 94 | // we'll add resources here next |
| 95 | }), |
| 96 | ); |
| 97 | ``` |
| 98 | |
| 99 | TypeScript is unhappy: the `state` property is required. Every Stack |
| 100 | needs a **state store** so Alchemy can persist resource state between |
| 101 | deploys and compute diffs against your infrastructure. |
| 102 | |
| 103 | ## Configure state |
| 104 | |
| 105 | Fly has no state backend of its own, so we'll keep state on disk |
| 106 | with `Alchemy.localState()` — it writes to `.alchemy/` next to your |
| 107 | code, no setup required: |
| 108 | |
| 109 | ```diff lang="typescript" |
| 110 | // alchemy.run.ts |
| 111 | import * as Alchemy from "alchemy"; |
| 112 | import * as Effect from "effect/Effect"; |
| 113 | import * as Layer from "effect/Layer"; |
| 114 | |
| 115 | export default Alchemy.Stack( |
| 116 | "MyApp", |
| 117 | { |
| 118 | providers: Layer.empty, |
| 119 | + state: Alchemy.localState(), |
| 120 | }, |
| 121 | Effect.gen(function* () { |
| 122 | // we'll add resources here next |
| 123 | }), |
| 124 | ); |
| 125 | ``` |
| 126 | |
| 127 | :::note[Other state stores] |
| 128 | Local state is perfect for a solo project. When you need state your |
| 129 | team and CI can share, swap in `Cloudflare.state()` or `AWS.state()` |
| 130 | — see [State Store](/state-store). |
| 131 | ::: |
| 132 | |
| 133 | ## Create the Service file |
| 134 | |
| 135 | A Service in Alchemy is a class — it has both an infrastructure |
| 136 | definition and a runtime implementation expressed as an Effect. |
| 137 | Create `src/api.ts` with the smallest possible declaration: |
| 138 | |
| 139 | ```typescript |
| 140 | // src/api.ts |
| 141 | import * as Fly from "alchemy/Fly"; |
| 142 | import * as Effect from "effect/Effect"; |
| 143 | |
| 144 | export default class Api extends Fly.Service<Api>()( |
| 145 | "Api", |
| 146 | { main: import.meta.url }, |
| 147 | Effect.gen(function* () { |
| 148 | return {}; |
| 149 | }), |
| 150 | ) {} |
| 151 | ``` |
| 152 | |
| 153 | The `<Api>` type argument plus the empty `()` is a one-time bit of |
| 154 | ceremony — it lets TypeScript reason about `Api` as a typed handle. |
| 155 | `main: import.meta.url` tells Alchemy this same file is the bundle |
| 156 | entrypoint. The Service creates and manages its own Fly App; you |
| 157 | don't declare one. |
| 158 | |
| 159 | ## Serve HTTP with `fetch` |
| 160 | |
| 161 | Add a `fetch` field — Alchemy treats anything returned from the |
| 162 | `Effect.gen` block as the runtime API, and `fetch` specifically is |
| 163 | wired to an HTTP server listening on the Service's port: |
| 164 | |
| 165 | ```diff lang="typescript" |
| 166 | // src/api.ts |
| 167 | import * as Fly from "alchemy/Fly"; |
| 168 | import * as Effect from "effect/Effect"; |
| 169 | +import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 170 | |
| 171 | export default class Api extends Fly.Service<Api>()( |
| 172 | "Api", |
| 173 | { main: import.meta.url }, |
| 174 | Effect.gen(function* () { |
| 175 | - return {}; |
| 176 | + return { |
| 177 | + fetch: Effect.succeed(HttpServerResponse.text("Hello from Fly!")), |
| 178 | + }; |
| 179 | }), |
| 180 | ) {} |
| 181 | ``` |
| 182 | |
| 183 | `HttpServerResponse` is the same `effect/http` API used on |
| 184 | every other Alchemy runtime — the handler you write here would run |
| 185 | unchanged on Cloudflare Workers or AWS Lambda. |
| 186 | |
| 187 | :::note |
| 188 | Notice the "Effect returning an Effect" pattern. The outer generator |
| 189 | is the **Construction phase** — it runs at deploy time and at process |
| 190 | startup. The inner `fetch` generator is the **Runtime phase** — it |
| 191 | runs per request. |
| 192 | ::: |
| 193 | |
| 194 | ## Add the Service to the Stack |
| 195 | |
| 196 | The `Api` class is just a typed identifier — yielding it inside the |
| 197 | Stack's Effect is what registers the resource. Yield it and observe |
| 198 | the type error: |
| 199 | |
| 200 | ```diff lang="typescript" |
| 201 | // alchemy.run.ts |
| 202 | import * as Alchemy from "alchemy"; |
| 203 | import * as Effect from "effect/Effect"; |
| 204 | import * as Layer from "effect/Layer"; |
| 205 | +import Api from "./src/api.ts"; |
| 206 | |
| 207 | export default Alchemy.Stack( |
| 208 | "MyApp", |
| 209 | { |
| 210 | providers: Layer.empty, |
| 211 | // @error: ts(2322) Type 'Layer<never, never, never>' is not assignable to type 'Layer<NoInfer<Providers>, never, StackServices>'. |
| 212 | // @error: Type 'Providers' is not assignable to type 'never'. |
| 213 | state: Alchemy.localState(), |
| 214 | }, |
| 215 | Effect.gen(function* () { |
| 216 | - // we'll add resources here next |
| 217 | + const api = yield* Api; |
| 218 | }), |
| 219 | ); |
| 220 | ``` |
| 221 | |
| 222 | TypeScript is telling us that `Layer.empty` doesn't provide |
| 223 | `Fly.Providers` — the layer required by `Service`. |
| 224 | |
| 225 | ## Fix the Providers |
| 226 | |
| 227 | Replace `Layer.empty` with `Fly.providers()` to resolve the type |
| 228 | error: |
| 229 | |
| 230 | ```diff lang="typescript" |
| 231 | // alchemy.run.ts |
| 232 | import * as Alchemy from "alchemy"; |
| 233 | +import * as Fly from "alchemy/Fly"; |
| 234 | import * as Effect from "effect/Effect"; |
| 235 | -import * as Layer from "effect/Layer"; |
| 236 | import Api from "./src/api.ts"; |
| 237 | |
| 238 | export default Alchemy.Stack( |
| 239 | "MyApp", |
| 240 | { |
| 241 | + providers: Fly.providers(), |
| 242 | state: Alchemy.localState(), |
| 243 | }, |
| 244 | Effect.gen(function* () { |
| 245 | const api = yield* Api; |
| 246 | }), |
| 247 | ); |
| 248 | ``` |
| 249 | |
| 250 | Now the program type-checks. The providers layer tells Alchemy how to |
| 251 | talk to the Fly Machines API, and the type system ensures you never |
| 252 | forget to wire it up. |
| 253 | |
| 254 | ## Return Stack outputs |
| 255 | |
| 256 | Stack outputs let you see important values after a deploy. Return the |
| 257 | Service's App name and public URL: |
| 258 | |
| 259 | ```diff lang="typescript" |
| 260 | Effect.gen(function* () { |
| 261 | const api = yield* Api; |
| 262 | + |
| 263 | + return { |
| 264 | + appName: api.appName, |
| 265 | + url: api.url, |
| 266 | + }; |
| 267 | }), |
| 268 | ``` |
| 269 | |
| 270 | Omit `name` and Alchemy generates a globally unique, DNS-safe App |
| 271 | name from the stack, stage, and logical ID. A Service is public by |
| 272 | default: Alchemy allocates a free shared IPv4 and IPv6 on its App, |
| 273 | and `url` is `https://{appName}.fly.dev`. |
| 274 | |
| 275 | ## Deploy |
| 276 | |
| 277 | Run `alchemy deploy`: |
| 278 | |
| 279 | <Tabs syncKey="pkgManager"> |
| 280 | <TabItem label="bun" icon="bun"> |
| 281 | <Code code="bun alchemy deploy" lang="sh" /> |
| 282 | </TabItem> |
| 283 | <TabItem label="npm" icon="npm"> |
| 284 | <Code code="npm run alchemy deploy" lang="sh" /> |
| 285 | </TabItem> |
| 286 | <TabItem label="pnpm" icon="pnpm"> |
| 287 | <Code code="pnpm alchemy deploy" lang="sh" /> |
| 288 | </TabItem> |
| 289 | <TabItem label="yarn" icon="seti:yarn"> |
| 290 | <Code code="yarn alchemy deploy" lang="sh" /> |
| 291 | </TabItem> |
| 292 | </Tabs> |
| 293 | |
| 294 | The first time you deploy, Alchemy prompts for Fly credentials — |
| 295 | paste the API token you generated in [Setup](/fly/setup). The token |
| 296 | is verified and saved to your **`default`** |
| 297 | [profile](/environments/profiles), so you won't be asked again. |
| 298 | |
| 299 | <Terminal content={`[u]Plan[/u]: [g]1 to create[/g] |
| 300 | |
| 301 | [g]+[/g] [b]Api[/b] [d](Fly.Service)[/d] |
| 302 | |
| 303 | Proceed? |
| 304 | ◉ Yes ○ No |
| 305 | [g]✓[/g] [b]Api[/b] [d](Fly.Service)[/d] [g]created[/g] |
| 306 | { |
| 307 | [s2]appName: "myapp-api-dev-a1b2c3d4", |
| 308 | [s2]url: "https://myapp-api-dev-a1b2c3d4.fly.dev", |
| 309 | }`} /> |
| 310 | |
| 311 | Alchemy created the Service's App and its addresses, bundled |
| 312 | `src/api.ts` with Rolldown, built a `linux/amd64` image, and pushed |
| 313 | it to `registry.fly.io/{app}:{id}-{hash}`. It then created a Machine |
| 314 | in `iad`, waited until it was `started`, and pointed the Fly proxy |
| 315 | at the Service's port. |
| 316 | |
| 317 | ## Try it out |
| 318 | |
| 319 | ```sh |
| 320 | curl https://myapp-api-dev-a1b2c3d4.fly.dev |
| 321 | # → Hello from Fly! |
| 322 | ``` |
| 323 | |
| 324 | ## Verify it worked |
| 325 | |
| 326 | The App is listed in your org in the |
| 327 | [Fly dashboard](https://fly.io/dashboard). Run `alchemy deploy` |
| 328 | again. Because nothing changed, the Service shows as a no-op: |
| 329 | |
| 330 | <Terminal content={`[u]Plan[/u]: [d]no changes[/d] |
| 331 | |
| 332 | { |
| 333 | [s2]appName: "myapp-api-dev-a1b2c3d4", |
| 334 | [s2]url: "https://myapp-api-dev-a1b2c3d4.fly.dev", |
| 335 | }`} /> |
| 336 | |
| 337 | This is the core loop — declare resources in code, deploy, and |
| 338 | Alchemy figures out what changed. |
| 339 | |
| 340 | ## Recap |
| 341 | |
| 342 | You now have: |
| 343 | |
| 344 | - An `alchemy.run.ts` with a Stack and a `Fly.Service` |
| 345 | - A live Service in its own App with a generated globally unique name |
| 346 | - A public `https://{appName}.fly.dev` URL answering HTTP |
| 347 | |
| 348 | In [Part 2](/fly/tutorial/part-2), you'll pin the Service's region |
| 349 | and port, add a health route, and ship a code change. |