1---
2title: Connect Services
3description: Build several Fly Services that call each other's methods over a private network, behind one public entry point.
4---
5
6Each [Service](/fly/compute/services) is its own Fly App. One Service
7calls another by **binding** it: `Fly.bindService(Users)` returns a
8typed client for the methods `Users` returns, and the calls travel over
9Fly's private network to `Users`'s `privateUrl`.
10
11Two things decide who can call a Service:
12
13- **The network.** Every App in the organization joins Fly's default
14 private network unless told otherwise. Set
15 `network: yield* Fly.stackNetwork` on each Service to put a stack's
16 Services on a network unique to its stack and stage. Apps on any
17 other network cannot resolve them.
18- **The binding.** Each Service has a caller token. Only Services that
19 bind it receive the token, so only they can call its methods.
20
21This guide builds the
22[fly-microservices example](https://github.com/alchemy-run/alchemy/tree/main/examples/fly-microservices):
23a private `Users` Service, a private `Orders` Service that calls
24`Users`, and a public `Gateway` that calls both.
25
26## Create the Users Service
27
28`Users` returns two methods, `list` and `get`. It returns no `fetch`;
29bound callers call the methods directly.
30
31```typescript
32// src/users.ts
33import * as Fly from "alchemy/Fly";
34import * as Effect from "effect/Effect";
35
36export interface User {
37 id: string;
38 name: string;
39}
40
41export const USERS: User[] = [
42 { id: "u1", name: "Ada" },
43 { id: "u2", name: "Grace" },
44];
45
46export default class Users extends Fly.Service<Users>()(
47 "Users",
48 Effect.gen(function* () {
49 return {
50 main: import.meta.url,
51 public: false,
52 };
53 }),
54 Effect.gen(function* () {
55 return {
56 list: () => Effect.succeed(USERS),
57 get: (id: string) => Effect.succeed(USERS.find((user) => user.id === id)),
58 };
59 }),
60) {}
61```
62
63`public: false` gives the Service's App a private Flycast address and
64no internet address. The props are an Effect so the next step can
65`yield*` inside them.
66
67## Put Users on the stack's network
68
69Add `network` so only this stage's Services can reach `Users`:
70
71```diff lang="typescript"
72Effect.gen(function* () {
73 return {
74 main: import.meta.url,
75 public: false,
76+ network: yield* Fly.stackNetwork,
77 };
78}),
79```
80
81`Fly.stackNetwork` yields a network name built from the stack and
82stage, such as `flymicroservices-dev`. Fly creates the network with the
83first App that names it.
84
85:::caution[Changing `network` replaces the Service]
86Fly cannot move an App between networks. Alchemy creates the Service
87on the new network, then deletes the old App. `network` only applies
88when the Service owns its App; combining it with `app` fails with
89`Fly.InvalidServiceProps`.
90:::
91
92## Create the Orders Service
93
94`Orders` is a second private Service on the same network:
95
96```typescript
97// src/orders.ts
98import * as Fly from "alchemy/Fly";
99import * as Effect from "effect/Effect";
100
101const ORDERS = [
102 { id: "o1", userId: "u1", item: "keyboard" },
103 { id: "o2", userId: "u2", item: "monitor" },
104];
105
106export default class Orders extends Fly.Service<Orders>()(
107 "Orders",
108 Effect.gen(function* () {
109 return {
110 main: import.meta.url,
111 public: false,
112 network: yield* Fly.stackNetwork,
113 };
114 }),
115 Effect.gen(function* () {
116 return {};
117 }),
118) {}
119```
120
121## Bind Users from Orders
122
123Bind `Users` in the Orders program:
124
125```diff lang="typescript"
126// src/orders.ts
127+import Users from "./users.ts";
128// ...
129Effect.gen(function* () {
130+ const users = yield* Fly.bindService(Users);
131 return {};
132}),
133```
134
135`users` is a typed client with `list` and `get`. Binding also makes
136`Orders` deploy after `Users` and hands `Orders` the address and caller
137token of `Users`.
138
139## Call Users from Orders
140
141Return a `list` method that looks up each order's user:
142
143```diff lang="typescript"
144Effect.gen(function* () {
145 const users = yield* Fly.bindService(Users);
146- return {};
147+ return {
148+ list: () =>
149+ Effect.forEach(ORDERS, (order) =>
150+ users
151+ .get(order.userId)
152+ .pipe(Effect.map((user) => ({ ...order, user }))),
153+ ),
154+ };
155}),
156```
157
158`users.get(order.userId)` is a call over the private network. The
159result is typed as `User | undefined`, the return type of `get` in
160`Users`.
161
162## Create the public Gateway
163
164`Gateway` joins the same network and leaves out `public: false`, so it
165is public:
166
167```typescript
168// src/gateway.ts
169import * as Fly from "alchemy/Fly";
170import * as Effect from "effect/Effect";
171
172export default class Gateway extends Fly.Service<Gateway>()(
173 "Gateway",
174 Effect.gen(function* () {
175 return {
176 main: import.meta.url,
177 network: yield* Fly.stackNetwork,
178 };
179 }),
180 Effect.gen(function* () {
181 return {};
182 }),
183) {}
184```
185
186A public Service on the network still serves `url`, so `Gateway` is
187the stack's single entry point from the internet.
188
189## Bind Users and Orders from the Gateway
190
191```diff lang="typescript"
192// src/gateway.ts
193+import Orders from "./orders.ts";
194+import Users from "./users.ts";
195// ...
196Effect.gen(function* () {
197+ const users = yield* Fly.bindService(Users);
198+ const orders = yield* Fly.bindService(Orders);
199 return {};
200}),
201```
202
203`orders` exposes the `list` method `Orders` returns.
204
205## Serve routes from the Gateway
206
207Call the bound methods from `fetch`:
208
209```diff lang="typescript"
210// src/gateway.ts
211+import { HttpServerRequest } from "effect/http/HttpServerRequest";
212+import * as HttpServerResponse from "effect/http/HttpServerResponse";
213// ...
214Effect.gen(function* () {
215 const users = yield* Fly.bindService(Users);
216 const orders = yield* Fly.bindService(Orders);
217- return {};
218+ return {
219+ fetch: Effect.gen(function* () {
220+ const request = yield* HttpServerRequest;
221+ const path = new URL(request.url, "http://gateway").pathname;
222+ if (path === "/users") {
223+ return yield* HttpServerResponse.json(yield* users.list());
224+ }
225+ if (path === "/orders") {
226+ return yield* HttpServerResponse.json(yield* orders.list());
227+ }
228+ return HttpServerResponse.text("try /users or /orders");
229+ }).pipe(Effect.orDie),
230+ };
231}),
232```
233
234## Wire the Stack
235
236Yield the three Services and return their URLs:
237
238```typescript
239// alchemy.run.ts
240import * as Alchemy from "alchemy";
241import * as Fly from "alchemy/Fly";
242import * as Effect from "effect/Effect";
243import Gateway from "./src/gateway.ts";
244import Orders from "./src/orders.ts";
245import Users from "./src/users.ts";
246
247export default Alchemy.Stack(
248 "FlyMicroservices",
249 { providers: Fly.providers(), state: Alchemy.localState() },
250 Effect.gen(function* () {
251 const users = yield* Users;
252 const orders = yield* Orders;
253 const gateway = yield* Gateway;
254
255 return {
256 url: gateway.url,
257 network: gateway.network,
258 usersPrivateUrl: users.privateUrl,
259 ordersPrivateUrl: orders.privateUrl,
260 };
261 }),
262);
263```
264
265## Deploy
266
267```sh
268alchemy deploy
269```
270
271Alchemy deploys `Users`, then `Orders`, then `Gateway`, and prints the
272outputs:
273
274```text
275url: "https://flymicroservices-gateway-dev-a1b2c3d4.fly.dev"
276network: "flymicroservices-dev"
277usersPrivateUrl: "http://flymicroservices-users-dev-e5f6a7b8.flycast"
278ordersPrivateUrl: "http://flymicroservices-orders-dev-c9d0e1f2.flycast"
279```
280
281## Try it out
282
283```sh
284curl https://flymicroservices-gateway-dev-a1b2c3d4.fly.dev/users
285# → [{"id":"u1","name":"Ada"},{"id":"u2","name":"Grace"}]
286
287curl https://flymicroservices-gateway-dev-a1b2c3d4.fly.dev/orders
288# → [{"id":"o1","userId":"u1","item":"keyboard","user":{"id":"u1","name":"Ada"}}, ...]
289```
290
291The second request crosses two private calls: Gateway → Orders →
292Users.
293
294## How deploy order works
295
296`Fly.bindService(Users)` makes the caller depend on `Users`. `Orders`
297binds `Users`, and `Gateway` binds both, so Alchemy deploys `Users`,
298then `Orders`, then `Gateway`. The caller's deploy writes the bound
299Service's private address and caller token onto the caller's Machines;
300the client reads them at runtime.
301
302The order of `yield*` in the Stack does not matter. The bindings
303decide it.
304
305## Public `url` and private `privateUrl`
306
307| Service | `url` | `privateUrl` |
308| --------- | ------------------------------- | ---------------------------------- |
309| `Users` | `undefined` | `http://{users-app}.flycast` |
310| `Orders` | `undefined` | `http://{orders-app}.flycast` |
311| `Gateway` | `https://{gateway-app}.fly.dev` | `http://{gateway-app}.flycast:7780` |
312
313`url` is the internet endpoint and is set only for a public Service.
314`privateUrl` is where bound callers reach the Service. A private
315Service serves plain HTTP on port 80, so its `privateUrl` has no port.
316A public Service's default ports are an HTTPS redirect (80) and HTTPS
317(443), so Alchemy publishes an extra plain-HTTP port for bound callers,
318`bindingPort` (default `7780`).
319
320You rarely use `privateUrl` directly. `Fly.bindService` resolves it
321for you.
322
323## Security model
324
325A method call from `Orders` to `Users` is accepted only when all of
326these hold:
327
328- **Same network.** `Users` is on the stack's network, so only Apps on
329 that network can resolve `{users-app}.flycast`. Binding a Service on
330 another network fails the caller's deploy with
331 `Fly.ServiceUnreachable`, before the caller's App is created.
332- **Caller token.** Each Service has its own caller token, generated
333 by Alchemy. A Service receives the token only for the Services it
334 binds, and sends it with every method call. A Service on the same
335 network that did not bind `Users` gets `401` from its methods.
336- **Fly-Src signature.** Fly's proxy signs every request that arrives
337 over Flycast with an ed25519 `Fly-Src` header naming the calling App
338 and organization. `Users` verifies the signature with Fly's public
339 key and checks the caller is in the same organization. A request
340 from the internet carries no valid signature, so a public Service's
341 methods answer `401` to it, and the extra `bindingPort` answers only
342 private-network requests.
343
344Fly issues no certificate for `.flycast`, so calls are plain HTTP.
345They travel inside Fly's private network, which Fly encrypts with
346WireGuard.
347
348:::caution[`fetch` routes are not token-protected]
349The caller token guards methods. A Service's `fetch` routes answer any
350request that reaches them: anything on the Service's network for a
351private Service, and the internet for a public one. Put operations
352that only bound Services should call in methods.
353:::
354
355## What an outside App sees
356
357An App that is not on the stack's network cannot resolve the private
358names. From a Machine in an App on the org's default network:
359
360```sh
361wget -qO- http://flymicroservices-users-dev-e5f6a7b8.flycast
362# wget: bad address 'flymicroservices-users-dev-e5f6a7b8.flycast'
363
364wget -qO- http://flymicroservices-users-dev-e5f6a7b8.internal
365# wget: bad address 'flymicroservices-users-dev-e5f6a7b8.internal'
366```
367
368Other stages of the same stack get their own `Fly.stackNetwork`, so
369`dev` and `prod` Services cannot call each other either.
370
371## Call HTTP routes with `client.fetch`
372
373A bound client also forwards requests to the Service's `fetch`
374handler. A relative URL resolves against the Service's `privateUrl`:
375
376```typescript
377import * as HttpClientRequest from "effect/http/HttpClientRequest";
378
379const users = yield* Fly.bindService(Users);
380const response = yield* users.fetch(HttpClientRequest.get("/hello"));
381const text = yield* response.text;
382```
383
384Use it for a Service that serves HTTP routes, such as a framework
385server. These requests carry no caller token; see the
386[caution above](#security-model).
387
388## Stream results from a method
389
390A method can return a `Stream`. The bound client returns the same
391`Stream`, delivered element by element:
392
393```typescript
394// in Users
395streamAll: () => Stream.fromIterable(USERS),
396
397// in the caller
398const all = yield* users.streamAll().pipe(Stream.runCollect);
399```
400
401## Bind one port with `Fly.bindEndpoint`
402
403`Fly.bindEndpoint` binds a single published port of a Service, for a
404port other than its main HTTP port. Publish the port on the target.
405Here both ports forward to the Service's `fetch` on 3000, which reads
406Fly's `fly-forwarded-port` header to tell them apart:
407
408```typescript
409services: [
410 { protocol: "tcp", internalPort: 3000, ports: [{ port: 80, handlers: ["http"] }] },
411 { protocol: "tcp", internalPort: 3000, ports: [{ port: 9000, handlers: ["http"] }] },
412],
413```
414
415Then bind it from the caller:
416
417```typescript
418const admin = yield* Fly.bindEndpoint(Users, { port: 9000 });
419const stats = yield* admin.client.get("/stats");
420```
421
422`admin.client` is an `HttpClient` whose relative URLs go to
423`http://{users-app}.flycast:9000`. For a raw TCP port, open your own
424connection to `yield* admin.host` (`{users-app}.flycast`) and
425`admin.port`. `yield* admin.url` is the full plain-HTTP URL.
426
427`bindEndpoint` sends no caller token. It relies on the network and
428whatever the port itself checks.
429
430## Two Services that bind each other
431
432A class can only bind a Service that already exists as a value. For
433two Services that bind each other, declare each as a tag class with the
434shape of its methods:
435
436```typescript
437// src/services.ts
438interface Named {
439 name: () => Effect.Effect<string>;
440}
441
442export class Ping extends Fly.Service<Ping, Named>()("Ping") {}
443export class Pong extends Fly.Service<Pong, Named>()("Pong") {}
444```
445
446Implement each in its own file with `.make`, binding the other. The
447file's default export is the implementation the Machine runs:
448
449```typescript
450// src/ping.ts
451import { Ping, Pong } from "./services.ts";
452
453export default Ping.make(
454 { main: import.meta.url },
455 Effect.gen(function* () {
456 const pong = yield* Fly.bindService(Pong);
457 return {
458 name: () => Effect.succeed("ping"),
459 fetch: Effect.gen(function* () {
460 return HttpServerResponse.text(`ping hears ${yield* pong.name()}`);
461 }).pipe(Effect.orDie),
462 };
463 }),
464);
465```
466
467`src/pong.ts` is the same with the roles swapped.
468
469Provide both implementations to the Stack's Effect:
470
471```typescript
472import PingLive from "./src/ping.ts";
473import PongLive from "./src/pong.ts";
474
475Effect.gen(function* () {
476 const ping = yield* Ping;
477 const pong = yield* Pong;
478 return { ping: ping.url, pong: pong.url };
479}).pipe(Effect.provide(Layer.mergeAll(PingLive, PongLive)));
480```
481
482Alchemy creates a stub of each Service first, then wires the bindings.
483Only Services that own their App can be part of such a cycle. A
484Service placed in a shared App with `app` cannot.
485
486## Errors
487
488| Error | When | Fix |
489| --------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
490| `Fly.ServiceUnreachable` | The caller and the bound Service are on different networks. Fails before the caller's App is created. | Give both the same `network`, usually `Fly.stackNetwork`. |
491| `Fly.ServiceNotBindable` | The bound Service publishes no ports (`services: []`), so it has no private address. | Publish a port, or stop binding it. |
492| `Fly.EndpointNotPublished` | `Fly.bindEndpoint` names a port the Service does not publish. | Add the port to the Service's `services`, or bind a published port. |
493
494## Where next
495
496- [Services](/fly/compute/services) — private Services, `bindingPort`,
497 and grouping Services in one App.
498- [IPs & certificates](/fly/networking) — public and private
499 Services, and addresses on your own App.
500- [Blue/green deployments](/fly/compute/deployments) — replace a
501 Service without dropping calls from its callers.
502- The [fly-microservices example](https://github.com/alchemy-run/alchemy/tree/main/examples/fly-microservices)
503 and the [`Service` reference](/providers/fly/service).