opencode-effect-enforcer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,767 @@
1
+ ---
2
+ name: effect-rpc-server
3
+ description: Serve RpcGroup contracts with Effect's RpcServer — handler layers (RpcGroup.toLayer/toLayerHandler), protocol layers (HTTP, WebSocket, TCP, stdio, worker), RpcSerialization, server middleware, streaming results, interruption and shutdown semantics, RpcTest. Use when implementing the server side of an Effect RPC API, mounting RPC on an HttpRouter or existing HTTP app, choosing a wire format, implementing RpcMiddleware, handling client aborts, or testing RPC handlers.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in serving RPC groups with `RpcServer` from `effect/unstable/rpc`.
7
+
8
+ Everything ships from the `effect` package under `effect/unstable/rpc` — there is no `@effect/rpc` package in v4. This skill covers the **server side**: implementing handlers, transports, serialization, middleware, and lifecycle. For defining `Rpc`/`RpcGroup` contracts see the `effect-rpc-api` skill; for building clients see the `effect-rpc-client` skill; for cluster entities see the `effect-rpc-cluster` skill; for `HttpRouter`/`HttpServer` fundamentals see the `effect-http-server` skill.
9
+
10
+ ## Effect Source Reference
11
+
12
+ The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read it directly when in doubt — these modules change between betas.
13
+
14
+ Key files:
15
+
16
+ - `packages/effect/src/unstable/rpc/RpcServer.ts` — `make`, `makeNoSerialization`, `layer`, `layerHttp`, every `layerProtocol*` / `makeProtocol*`, `toHttpEffect*`, the `Protocol` service
17
+ - `packages/effect/src/unstable/rpc/RpcGroup.ts` — `toLayer`, `toLayerHandler`, `toHandlers`, `accessHandler`, `of`, handler type derivation
18
+ - `packages/effect/src/unstable/rpc/Rpc.ts` — `ServerClient`, `Handler`, `ToHandlerFn`, `ResultFrom`, `fork`, `uninterruptible`, `ServicesServer`
19
+ - `packages/effect/src/unstable/rpc/RpcMiddleware.ts` — `Service` constructor, server middleware function shape, `layerClient`
20
+ - `packages/effect/src/unstable/rpc/RpcSerialization.ts` — `json`, `ndjson`, `jsonRpc`, `ndJsonRpc`, `msgPack` parsers and their layers
21
+ - `packages/effect/src/unstable/rpc/RpcMessage.ts` — the wire vocabulary (`Request`, `Ack`, `Interrupt`, `Eof`, `Chunk`, `Exit`, `Defect`, `ClientEnd`)
22
+ - `packages/effect/src/unstable/rpc/RpcWorker.ts` — `InitialMessage` for worker transports
23
+ - `packages/effect/src/unstable/rpc/RpcTest.ts` — in-process test client
24
+ - `packages/effect/src/unstable/rpc/RpcSchema.ts` — `ClientAbort` cause annotation, stream schema markers
25
+ - `packages/platform-node/test/RpcServer.test.ts` + `test/fixtures/rpc-{schemas,e2e}.ts` — the best end-to-end reference for real wiring across http/ws/tcp transports and every serialization
26
+ - `packages/platform-browser/test/fixtures/rpc-worker.ts` — minimal worker-side server entrypoint
27
+
28
+ ## Core Model
29
+
30
+ A running RPC server is four layers snapped together:
31
+
32
+ ```
33
+ RpcGroup.toLayer(handlers) → Layer<Rpc.ToHandler<Rpcs>> (your logic)
34
+ Layer.succeed(MyMiddleware)(...) → Layer<Rpc.Middleware<Rpcs>> (wraps handlers)
35
+ RpcServer.layerProtocol* → Layer<RpcServer.Protocol> (transport)
36
+ RpcSerialization.layer* → Layer<RpcSerialization> (wire format)
37
+
38
+ RpcServer.layer(group, options?) → consumes all four, runs forever
39
+ ```
40
+
41
+ `RpcServer.layer` decodes incoming `Request` messages with the rpc's payload schema, runs the matching handler (wrapped in middleware and a per-request `Scope`), and encodes the resulting `Exit` — or stream `Chunk`s — back through the protocol. A handler is just a function:
42
+
43
+ ```ts
44
+ // Rpc.ToHandlerFn — what you write for each rpc tag
45
+ (
46
+ payload: Rpc.Payload<R>,
47
+ options: {
48
+ readonly client: Rpc.ServerClient; // client.id: number; client.annotations; client.annotate(key, value)
49
+ readonly requestId: RpcMessage.RequestId; // branded string | number
50
+ readonly headers: Headers; // transport headers merged with per-call headers
51
+ readonly rpc: R;
52
+ }
53
+ ) =>
54
+ | Effect<Success | Deferred<Success, Error>, Error, R> // non-stream rpc
55
+ | Stream<Elem, Error, R> // stream rpc
56
+ | Effect<Queue.Dequeue<Elem, Error | Cause.Done>, Error, R>; // stream rpc, queue form
57
+ ```
58
+
59
+ Imports used throughout:
60
+
61
+ ```ts
62
+ import { Cause, Context, Deferred, Effect, Layer, Queue, Schema, Stream } from 'effect';
63
+ import { Headers, HttpRouter } from 'effect/unstable/http';
64
+ import {
65
+ Rpc,
66
+ RpcGroup,
67
+ RpcMessage,
68
+ RpcMiddleware,
69
+ RpcSchema,
70
+ RpcSerialization,
71
+ RpcServer,
72
+ RpcTest,
73
+ RpcWorker
74
+ } from 'effect/unstable/rpc';
75
+ ```
76
+
77
+ Running example group (definition details belong to the `effect-rpc-api` skill):
78
+
79
+ ```ts
80
+ class User extends Schema.Class<User>('User')({
81
+ id: Schema.String,
82
+ name: Schema.String
83
+ }) {}
84
+
85
+ class GetUser extends Rpc.make('GetUser', {
86
+ success: User,
87
+ payload: { id: Schema.String }
88
+ }) {}
89
+
90
+ class StreamUsers extends Rpc.make('StreamUsers', {
91
+ success: User, // element type when stream: true
92
+ payload: { id: Schema.String },
93
+ stream: true
94
+ }) {}
95
+
96
+ const UserRpcs = RpcGroup.make(GetUser, StreamUsers);
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 1. Implementing Handlers
102
+
103
+ ### `group.toLayer(handlers | Effect<handlers>)` — the 80% case
104
+
105
+ Build every handler at once. The build argument can be a plain handlers object or an Effect (for pulling dependencies). Always wrap the object in `group.of(...)` so type errors point at the offending handler:
106
+
107
+ ```ts
108
+ const UsersLive = UserRpcs.toLayer(
109
+ Effect.gen(function* () {
110
+ const db = yield* Database;
111
+ return UserRpcs.of({
112
+ GetUser: (payload, { headers, client }) => db.findUser(payload.id),
113
+ StreamUsers: (payload) => db.changeFeed(payload.id) // Stream<User>
114
+ });
115
+ })
116
+ );
117
+ // Layer<Rpc.ToHandler<typeof GetUser | typeof StreamUsers>, never, Database>
118
+ ```
119
+
120
+ The services captured when the layer builds are stored alongside each handler and provided automatically on every request — handler `R` beyond middleware-provided services becomes a requirement of the layer, not of the server.
121
+
122
+ Handlers run with a **per-request `Scope`** already in context: `Effect.addFinalizer` / `Effect.forkScoped` inside a handler are scoped to that single request and cleaned up when it completes, fails, or is interrupted. `Scope` never appears in the layer's requirements.
123
+
124
+ ### Handler metadata
125
+
126
+ The second argument carries request metadata. `client` is a `Rpc.ServerClient` — not a raw number:
127
+
128
+ ```ts
129
+ GetUser: (payload, { client, requestId, headers }) =>
130
+ Effect.gen(function* () {
131
+ yield* Effect.annotateCurrentSpan({ clientId: client.id });
132
+ // client.annotations is a Context.Context<never> middleware may have extended
133
+ return yield* db.findUser(payload.id);
134
+ });
135
+ ```
136
+
137
+ ### Deferred responses
138
+
139
+ A non-stream handler may succeed with a `Deferred<Success, Error>` instead of the value. The server completes the handler fiber immediately (releasing its concurrency permit) and sends the final `Exit` only when the deferred resolves — invisible to the client:
140
+
141
+ ```ts
142
+ GetUserDeferred: (payload) => {
143
+ const deferred = Deferred.makeUnsafe<User>();
144
+ // complete later — from a webhook, another fiber, a queue worker...
145
+ Deferred.doneUnsafe(deferred, Effect.succeed(new User({ id: '1', name: 'John' })));
146
+ return Effect.succeed(deferred);
147
+ };
148
+ ```
149
+
150
+ ### `Rpc.fork` and `Rpc.uninterruptible` — handler wrappers
151
+
152
+ These wrap the handler's *return value* (they are not rpc options):
153
+
154
+ ```ts
155
+ GetUser: (payload) => db.findUser(payload.id).pipe(Rpc.fork), // skip the concurrency semaphore
156
+ Charge: (payload) => chargeOnce(payload).pipe(Rpc.uninterruptible), // run even through aborts/shutdown
157
+ Both: (payload) => work(payload).pipe(Rpc.wrap({ fork: true, uninterruptible: true }))
158
+ ```
159
+
160
+ `Rpc.fork` exempts one handler from the server's `concurrency` semaphore. `Rpc.uninterruptible` forks the handler fiber uninterruptibly, so client aborts and server shutdown cannot cancel it mid-flight.
161
+
162
+ ### `group.toLayerHandler(tag, handler | Effect<handler>)` — one handler per layer
163
+
164
+ Keeps handlers with wildly different dependencies in separate files. The server requires the union `Rpc.ToHandler<Rpcs>`, so a missing tag is a compile error at the composition site:
165
+
166
+ ```ts
167
+ const GetUserLive = UserRpcs.toLayerHandler(
168
+ 'GetUser',
169
+ Effect.gen(function* () {
170
+ const db = yield* Database;
171
+ return (payload) => db.findUser(payload.id);
172
+ })
173
+ );
174
+ const HandlersLive = Layer.mergeAll(GetUserLive, StreamUsersLive);
175
+ ```
176
+
177
+ ### `group.toHandlers(handlers)` — raw context form
178
+
179
+ Returns `Effect<Context.Context<Rpc.ToHandler<R>>>` instead of a Layer. Use when providing handlers manually to `RpcServer.make` or composing contexts by hand.
180
+
181
+ ### `group.accessHandler(tag)` — call one handler directly
182
+
183
+ Resolves a single handler with its captured services already attached. The returned function takes `(payload, { client, requestId, headers })` — the `rpc` field is injected for you (pass a fresh mutable object; the implementation assigns `options.rpc`). The easiest way to unit-test one handler:
184
+
185
+ ```ts
186
+ const user = yield* UserRpcs.accessHandler('GetUser').pipe(
187
+ Effect.flatMap((handler) =>
188
+ handler(
189
+ { id: 'u1' },
190
+ {
191
+ client: new Rpc.ServerClient(0),
192
+ requestId: RpcMessage.RequestId(1),
193
+ headers: Headers.empty
194
+ }
195
+ )
196
+ ),
197
+ Effect.provide(UsersLive)
198
+ );
199
+ ```
200
+
201
+ ---
202
+
203
+ ## 2. Starting a Server — `RpcServer.layer` / `make` / `layerHttp`
204
+
205
+ ### `RpcServer.layer(group, options?)`
206
+
207
+ Transport-agnostic. Requires a `Protocol`, the handlers, any middleware implementations, and `Rpc.ServicesServer<Rpcs>` (services your schemas need to decode payloads / encode results — usually `never`):
208
+
209
+ ```ts
210
+ const ServerLayer = RpcServer.layer(UserRpcs, {
211
+ concurrency: 'unbounded', // default
212
+ disableFatalDefects: false, // default — see below
213
+ disableTracing: false,
214
+ spanPrefix: 'RpcServer', // span per request: `${spanPrefix}.${tag}`
215
+ spanAttributes: { service: 'users' }
216
+ }).pipe(
217
+ Layer.provide(UsersLive),
218
+ Layer.provide(RpcServer.layerProtocolHttp({ path: '/rpc' })),
219
+ Layer.provide(RpcSerialization.layerNdjson)
220
+ // the http/websocket protocols additionally need HttpRouter (see §3)
221
+ );
222
+ ```
223
+
224
+ Option semantics (verified against `makeNoSerialization`):
225
+
226
+ - **`concurrency: number | 'unbounded'`** (default `'unbounded'`) — a single `Semaphore` for the **whole server instance**, shared across all clients and requests; it is not per-connection. `Rpc.fork(...)` opts an individual handler out.
227
+ - **`disableFatalDefects: boolean`** (default `false`) — by default a defect in a handler (a `die` with no interruption) is treated as a protocol-level fault: the server sends a connection `Defect` message and the client fails **every in-flight request on that connection** with `Cause.die`. With `true`, the defect is delivered as that one request's `Exit`. Production servers usually want `true`. Either way the defect crosses the wire encoded with the rpc's defect schema — the default `Schema.Defect()` keeps an `Error`'s name/message but drops stacks; declare `defect: Schema.Defect({ includeStack: true })` on the contract (see the `effect-rpc-api` skill) to preserve them.
228
+ - **Undecodable payloads** never reach the handler: the server answers that one request with a die exit carrying the schema issue string (regardless of `disableFatalDefects`); the connection stays up.
229
+ - **`disableTracing` / `spanPrefix` / `spanAttributes`** — each request runs in a span named `${spanPrefix}.${tag}` (default prefix `RpcServer`), with the client's span as parent when the transport supports span propagation.
230
+
231
+ `RpcServer.make(group, options?)` is the Effect form — `Effect<never, ...>` that runs the server loop forever; `layer` is exactly `Layer.effectDiscard(Effect.forkScoped(make(group, options)))`.
232
+
233
+ ### `RpcServer.layerHttp({ group, path, protocol?, ...options })` — one-call HTTP setup
234
+
235
+ Note the different calling convention: the group goes **inside** the options object, and `protocol` defaults to `'websocket'`, not `'http'`:
236
+
237
+ ```ts
238
+ const ServerLayer = RpcServer.layerHttp({
239
+ group: UserRpcs,
240
+ path: '/rpc',
241
+ protocol: 'http', // or 'websocket' (the default!)
242
+ disableFatalDefects: true,
243
+ streamBufferSize: 16 // framed HTTP only; default 16
244
+ }).pipe(
245
+ Layer.provide(UsersLive),
246
+ Layer.provide(RpcSerialization.layerNdjson)
247
+ );
248
+ ```
249
+
250
+ ### Serving multiple groups
251
+
252
+ `layer`/`layerHttp` take exactly one group. Either merge the contracts — `UserRpcs.merge(AdminRpcs)` (see the `effect-rpc-api` skill) — and serve one server, or build a complete `RpcServer.layer(...)` + `layerProtocolHttp({ path })` composition per group at distinct paths and `Layer.mergeAll` them: each composition encapsulates its own `Protocol`, so several can register routes on the same `HttpRouter` without clashing.
253
+
254
+ ### Full Node wiring
255
+
256
+ The protocol layers register routes on `HttpRouter`; `HttpRouter.serve` provides the router and turns it into an HTTP app:
257
+
258
+ ```ts
259
+ import { createServer } from 'node:http';
260
+ import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
261
+
262
+ const Main = HttpRouter.serve(ServerLayer).pipe(
263
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
264
+ );
265
+
266
+ NodeRuntime.runMain(Layer.launch(Main));
267
+ ```
268
+
269
+ See the `effect-http-server` skill for `HttpRouter.serve` options (middleware, `disableLogger`, `disableListenLog`) and adding sibling routes.
270
+
271
+ ---
272
+
273
+ ## 3. Protocol Layers — Picking a Transport
274
+
275
+ | Layer | Requires | supportsAck | span propagation | transferables |
276
+ |---|---|---|---|---|
277
+ | `RpcServer.layerProtocolHttp({ path, streamBufferSize? })` | `RpcSerialization`, `HttpRouter` | no | no | no |
278
+ | `RpcServer.layerProtocolWebsocket({ path })` | `RpcSerialization`, `HttpRouter` | yes | yes | no |
279
+ | `RpcServer.layerProtocolSocketServer` | `RpcSerialization`, `SocketServer` | yes | yes | no |
280
+ | `RpcServer.layerProtocolStdio` | `RpcSerialization`, `Stdio` | yes | yes | no |
281
+ | `RpcServer.layerProtocolWorkerRunner` | `WorkerRunner.WorkerRunnerPlatform` | yes | yes | yes |
282
+
283
+ `supportsAck` is what enables **stream backpressure** (§7). Each layer has a `makeProtocol*` Effect counterpart for inline composition, and `makeProtocolWithHttpEffect` / `makeProtocolWithHttpEffectWebsocket` return `{ protocol, httpEffect }` when you need both (§5).
284
+
285
+ - **HTTP** (`layerProtocolHttp`) registers a `POST` route. Each HTTP request is a short-lived client: all messages in the body are processed, then the response is either buffered or streamed depending on serialization framing (§4). For framed responses, `streamBufferSize` bounds the response queue and defaults to `16`; pass `'unbounded'` to restore unbounded buffering. The option is also accepted by `layerHttp`, `makeProtocolHttp`, `makeProtocolWithHttpEffect`, and `toHttpEffect`. HTTP request headers are prepended to every rpc request's headers — so `authorization` etc. is visible to middleware without client cooperation.
286
+ - **WebSocket** (`layerProtocolWebsocket`) registers a `GET` route that upgrades the connection. Upgrade-request headers are merged into every rpc's headers. Full duplex, acks, span propagation.
287
+ - **TCP** (`layerProtocolSocketServer`) serves raw sockets:
288
+
289
+ ```ts
290
+ import { NodeSocketServer } from '@effect/platform-node';
291
+
292
+ const TcpServer = RpcServer.layer(UserRpcs).pipe(
293
+ Layer.provide(UsersLive),
294
+ Layer.provide(RpcServer.layerProtocolSocketServer),
295
+ Layer.provide(NodeSocketServer.layer({ port: 9000 })),
296
+ Layer.provide(RpcSerialization.layerMsgPack) // must be a framed format (§4)
297
+ );
298
+ ```
299
+
300
+ `SocketServer` layers and the raw socket surface underneath are covered by the `effect-socket` skill.
301
+
302
+ - **Stdio** (`layerProtocolStdio`) serves RPC over the current process's stdin/stdout — for CLI subprocess protocols (LSP-style tooling, plugin hosts). When stdin ends, the protocol interrupts the server fiber so the process can exit:
303
+
304
+ ```ts
305
+ import { NodeRuntime, NodeStdio } from '@effect/platform-node';
306
+
307
+ const StdioMain = RpcServer.layer(UserRpcs).pipe(
308
+ Layer.provide(UsersLive),
309
+ Layer.provide(RpcServer.layerProtocolStdio),
310
+ Layer.provide(RpcSerialization.layerNdjson),
311
+ Layer.provide(NodeStdio.layer)
312
+ );
313
+
314
+ NodeRuntime.runMain(Layer.launch(StdioMain));
315
+ ```
316
+
317
+ - **Worker** — see §9.
318
+
319
+ ### Inspecting the live protocol
320
+
321
+ `RpcServer.Protocol` is a `Context.Service` you can read to branch on transport capabilities (tests use this to skip backpressure assertions on HTTP):
322
+
323
+ ```ts
324
+ const { supportsAck, supportsTransferables, supportsSpanPropagation, clientIds, initialMessage } =
325
+ yield* RpcServer.Protocol;
326
+ ```
327
+
328
+ The complete protocol surface also includes:
329
+
330
+ ```ts
331
+ {
332
+ readonly supportsNotifications: boolean;
333
+ }
334
+ ```
335
+
336
+ `supportsNotifications` is `true` for sockets, stdio, workers, and framed HTTP; it is `false` for buffered unframed HTTP.
337
+
338
+ Server-originated calls and notifications use `RpcMessage.RequestEncoded` in `FromServerEncoded`. Set `isNotification: true` for notifications; JSON-RPC serialization omits their id. Unframed HTTP buffers normal responses and intentionally drops notifications because it cannot deliver them before the response closes.
339
+
340
+ ---
341
+
342
+ ## 4. Serialization — Picking a Wire Format
343
+
344
+ Provide exactly one `RpcSerialization` layer. The load-bearing property is `includesFraming` — whether the format can split a byte stream back into messages:
345
+
346
+ | Layer | Content-Type | Framed? | Notes |
347
+ |---|---|---|---|
348
+ | `RpcSerialization.layerJson` | `application/json` | no | whole-payload JSON |
349
+ | `RpcSerialization.layerNdjson` | `application/ndjson` | yes | newline-delimited JSON |
350
+ | `RpcSerialization.layerJsonRpc({ contentType? })` | `application/json` | no | JSON-RPC 2.0 interop |
351
+ | `RpcSerialization.layerNdJsonRpc({ contentType? })` | `application/json-rpc` | yes | JSON-RPC 2.0, newline-framed |
352
+ | `RpcSerialization.layerMsgPack` | `application/msgpack` | yes | binary, smallest; msgpackr `useRecords: true` |
353
+
354
+ Rules, verified against the protocol implementations and the e2e matrix:
355
+
356
+ - **Raw TCP sockets need a framed format** (`ndjson`, `ndJsonRpc`, `msgPack`). Plain `json` cannot split the byte stream — decoding breaks as soon as two messages share a chunk.
357
+ - **WebSocket frames messages itself**, so *any* format works there — the e2e suite runs ws with json, ndjson, msgpack, and jsonRpc.
358
+ - **HTTP POST works with both**, with different response behavior: with an **unframed** format the server buffers all responses and returns one JSON array when every request finishes (a streaming rpc arrives as one big batch at the end); with a **framed** format the server returns a chunked streaming response and chunks arrive incrementally. Use `layerNdjson` (or msgpack) over HTTP if you serve streaming rpcs.
359
+ - The response `content-type` is the serialization's `contentType`.
360
+ - `RpcSerialization.layerJsonRpc()` / `layerNdJsonRpc()` speak JSON-RPC 2.0: rpc tags map to `method`, batched arrays are preserved, and internal signals travel as `@effect/rpc/Ack`-style methods. Use for interop with non-Effect JSON-RPC clients.
361
+ - `RpcSerialization.makeMsgPack(options)` customizes msgpackr (`useRecords`, `useFloat32`, ...); wrap with `Layer.succeed(RpcSerialization.RpcSerialization)(RpcSerialization.makeMsgPack({ ... }))`.
362
+
363
+ Client and server must use the **same** serialization.
364
+
365
+ ---
366
+
367
+ ## 5. Embedding in an Existing HTTP App
368
+
369
+ When you want the RPC handler as a value to mount yourself — alongside other routes, behind your own middleware, or in a Fetch-style handler — use the `toHttpEffect` helpers instead of `layerProtocolHttp`.
370
+
371
+ `RpcServer.toHttpEffect(group, options?)` starts the server in the current `Scope` and returns the request-handling Effect (`Effect<HttpServerResponse, never, Scope | HttpServerRequest>`). `toHttpEffectWebsocket` is the upgrade-handler equivalent. HTTP options are `disableTracing` / `spanPrefix` / `spanAttributes` / `disableFatalDefects` / `streamBufferSize` — note there is **no `concurrency` option** on these two.
372
+
373
+ ```ts
374
+ const RpcRoute = Layer.effectDiscard(
375
+ Effect.gen(function* () {
376
+ const router = yield* HttpRouter.HttpRouter;
377
+ const rpcHandler = yield* RpcServer.toHttpEffect(UserRpcs, {
378
+ disableFatalDefects: true
379
+ });
380
+ yield* router.add('POST', '/rpc', rpcHandler);
381
+ })
382
+ ).pipe(Layer.provide([UsersLive, RpcSerialization.layerNdjson]));
383
+
384
+ // merge with your other route layers and serve as usual
385
+ const Main = HttpRouter.serve(Layer.mergeAll(RpcRoute, HealthRoutes)).pipe(
386
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
387
+ );
388
+ ```
389
+
390
+ `Layer.effectDiscard` supplies the `Scope` that keeps the forked RPC server alive for the lifetime of the layer.
391
+
392
+ Lower still: `RpcServer.makeProtocolWithHttpEffect({ streamBufferSize? })` / `makeProtocolWithHttpEffectWebsocket` give you `{ protocol, httpEffect }` so you can provide the `Protocol` to `RpcServer.make` yourself — useful when one process must mount the same server behind several routes or compose with a hand-built runtime. `makeProtocolWithHttpEffect` is a function and must be called, even when no options are supplied: `yield* RpcServer.makeProtocolWithHttpEffect()`.
393
+
394
+ ---
395
+
396
+ ## 6. Implementing Middleware
397
+
398
+ A middleware is a `Context.Service` whose value is a function wrapping handler execution. Definition (shared with the contract — see the `effect-rpc-api` skill) and server implementation:
399
+
400
+ ```ts
401
+ class CurrentUser extends Context.Service<CurrentUser, User>()('CurrentUser') {}
402
+
403
+ class Unauthorized extends Schema.Error<Unauthorized>('Unauthorized')({
404
+ _tag: Schema.tag('Unauthorized')
405
+ }) {}
406
+
407
+ class AuthMiddleware extends RpcMiddleware.Service<AuthMiddleware, {
408
+ provides: CurrentUser; // injected into the handler
409
+ requires: never; // services the middleware itself needs at runtime
410
+ clientError: never; // error type only the client-side wrapper can produce
411
+ }>()('AuthMiddleware', {
412
+ error: Unauthorized, // wire-encodable failure this middleware may produce
413
+ requiredForClient: true // clients must provide RpcMiddleware.layerClient(...) or fail to compile
414
+ }) {}
415
+
416
+ // Attach: per-rpc `rpc.middleware(AuthMiddleware)` or whole-group `group.middleware(AuthMiddleware)`
417
+ const SecureRpcs = UserRpcs.middleware(AuthMiddleware);
418
+ ```
419
+
420
+ ### Server implementation layer
421
+
422
+ The middleware function receives `(effect, options)` where `options` is `{ client, requestId, rpc, payload, headers }` (note: `payload` is `unknown` here — middleware is generic over rpcs). Provide it as a Layer:
423
+
424
+ ```ts
425
+ const AuthLive = Layer.effect(AuthMiddleware)(
426
+ Effect.gen(function* () {
427
+ const tokens = yield* TokenService; // middleware dependencies
428
+ return AuthMiddleware.of((effect, { headers, client }) =>
429
+ tokens.verify(headers.authorization).pipe(
430
+ Effect.mapError(() => new Unauthorized()),
431
+ Effect.flatMap((user) => {
432
+ client.annotate(CurrentUser, user); // optional: visible to later requests' middleware via client.annotations
433
+ return Effect.provideService(effect, CurrentUser, user);
434
+ })
435
+ )
436
+ );
437
+ })
438
+ );
439
+ // dependency-free middleware: Layer.succeed(AuthMiddleware)(AuthMiddleware.of(...))
440
+ ```
441
+
442
+ Then `Layer.provide(AuthLive)` to `RpcServer.layer` along with the handlers — the server's `Rpc.Middleware<Rpcs>` requirement makes forgetting it a compile error.
443
+
444
+ ### Semantics
445
+
446
+ - **Ordering**: middlewares are applied in attach order, each wrapping the previous result — so the **last attached middleware is outermost** and runs first on the way in; services it provides are visible to earlier-attached middleware and the handler. In the platform fixture, `Rpc.make(...).middleware(TimingMiddleware)` inside a group with `.middleware(AuthMiddleware)` runs Auth (outer) then Timing (inner).
447
+ - **Failure**: failing with the declared `error` schema becomes that request's `Exit` and is typed on the client. Dying inside middleware follows the same fatal-defect rules as handlers (§2).
448
+ - **Streams**: for stream rpcs the middleware wraps the *entire stream-running effect* — it runs once per request and stays active for the stream's lifetime; provided services reach the stream handler.
449
+ - **Chaining**: a middleware config may declare `requires: SomeService` that another middleware `provides` — the type system forces the providing middleware to be attached too.
450
+ - **Cross-cutting observability**: middleware with no `provides` is the place for metrics/timing — wrap with `Effect.tap` / `Effect.tapDefect` / `Effect.ensuring`:
451
+
452
+ ```ts
453
+ class TimingMiddleware extends RpcMiddleware.Service<TimingMiddleware>()('TimingMiddleware') {}
454
+
455
+ const TimingLive = Layer.succeed(TimingMiddleware)(
456
+ TimingMiddleware.of((effect) =>
457
+ effect.pipe(
458
+ Effect.tap(Metric.update(rpcSuccesses, 1)),
459
+ Effect.tapDefect(() => Metric.update(rpcDefects, 1)),
460
+ Effect.ensuring(Metric.update(rpcCount, 1))
461
+ )
462
+ )
463
+ );
464
+ ```
465
+
466
+ `requiredForClient: true` middlewares additionally need `RpcMiddleware.layerClient(...)` on every client (including `RpcTest`) — see the `effect-rpc-client` skill.
467
+
468
+ ---
469
+
470
+ ## 7. Streaming Handlers
471
+
472
+ A `stream: true` rpc's handler returns either a `Stream` or an Effect producing a `Queue.Dequeue`:
473
+
474
+ ```ts
475
+ // Stream form — simplest
476
+ StreamUsers: (payload) =>
477
+ db.changeFeed(payload.id).pipe(Stream.map((row) => new User(row)));
478
+
479
+ // Queue form — push values from background fibers; the per-request Scope cleans up
480
+ StreamUsers: Effect.fnUntraced(function* (payload) {
481
+ const queue = yield* Queue.bounded<User, Cause.Done>(16);
482
+ yield* Effect.addFinalizer(() => Effect.log('subscription closed'));
483
+ yield* pollSource(payload.id, queue).pipe(Effect.forkScoped); // dies with the request
484
+ return queue;
485
+ });
486
+ ```
487
+
488
+ Semantics, verified against `RpcServer.makeNoSerialization`:
489
+
490
+ - The server batches available values into `Chunk` messages (`Stream.runForEachArray` / `Queue.takeAll`), so one message can carry many elements.
491
+ - **Backpressure** exists only on ack-supporting transports (everything except plain HTTP): after writing a chunk the server waits for the client's `Ack` before pulling more. Over plain HTTP the producer is never throttled.
492
+ - **Ending**: a `Stream` ending ends the response; for the queue form call `Queue.end(queue)` — `Cause.Done` is the end-of-stream signal, and `Queue.end` only typechecks when the queue's error channel includes it, so create the queue as `Queue.bounded<User, Cause.Done>(16)`. The client then sees the stream complete with a final void `Exit`.
493
+ - **Failures**: failing the stream with the rpc's declared error fails the client's stream with that typed error.
494
+ - The client consumes the result as a `Stream` by default or a `Queue.Dequeue` with `{ asQueue: true }` — see the `effect-rpc-client` skill.
495
+
496
+ ---
497
+
498
+ ## 8. Interruption, Client Aborts & Graceful Shutdown
499
+
500
+ ### Client aborts
501
+
502
+ When a client interrupts an in-flight call (or a streaming subscription), the server interrupts the handler fiber with the `RpcSchema.ClientAbort` cause annotation. The request scope closes and finalizers run. To distinguish a client abort from server shutdown, inspect the cause's interrupt reasons:
503
+
504
+ ```ts
505
+ Never: () =>
506
+ Effect.never.pipe(
507
+ Effect.onExit((exit) => {
508
+ const clientAborted =
509
+ exit._tag === 'Failure' &&
510
+ exit.cause.reasons.some(
511
+ (r) => r._tag === 'Interrupt' && r.annotations.has(RpcSchema.ClientAbort.key)
512
+ );
513
+ return Effect.log(clientAborted ? 'client cancelled' : 'other interrupt');
514
+ })
515
+ );
516
+ ```
517
+
518
+ `Effect.onInterrupt` only receives interruptor fiber ids — use `Effect.onExit` / `Effect.onError` when you need the annotation.
519
+
520
+ ### Disconnects
521
+
522
+ - **HTTP**: if the request's scope closes before responses finish (client went away), the protocol sends an `Interrupt` for every request id from that HTTP call.
523
+ - **Sockets/WebSockets**: a closed connection is pushed to the protocol's `disconnects` queue; the server interrupts all of that client's in-flight fibers.
524
+ - An `Interrupt` for an unknown/finished request id is answered with `Exit.interrupt()` — interruption is idempotent.
525
+
526
+ ### `Rpc.uninterruptible`
527
+
528
+ Handlers that must complete (payment capture, append-then-ack) should return `value.pipe(Rpc.uninterruptible)` — the fiber is forked uninterruptibly, so client aborts and shutdown wait for it.
529
+
530
+ ### Graceful shutdown
531
+
532
+ When the server layer's scope closes (e.g. `NodeRuntime.runMain` received SIGINT):
533
+
534
+ 1. New messages are rejected (`Effect.interrupt`).
535
+ 2. Every in-flight handler fiber is interrupted — **without** the `ClientAbort` annotation; `Rpc.uninterruptible` handlers run to completion.
536
+ 3. Each finished request's final `Exit` (interrupt or result) is still flushed to its client, then a `ClientEnd` is sent per client.
537
+ 4. The server's finalizer waits on a latch until every client has been ended — shutdown does not race the final writes.
538
+
539
+ ---
540
+
541
+ ## 9. Worker-Backed Servers
542
+
543
+ Run the RPC server inside a Web/Node worker; the parent process is the client (`RpcClient.layerProtocolWorker` — see the `effect-rpc-client` skill). The worker protocol does its own structured-clone transport, so **no `RpcSerialization` layer is needed** and `supportsTransferables` is true.
544
+
545
+ Worker entrypoint (browser shown; for Node use `NodeWorkerRunner.layer` from `@effect/platform-node`):
546
+
547
+ ```ts
548
+ // worker.ts
549
+ import { BrowserWorkerRunner } from '@effect/platform-browser';
550
+ import { Effect, Layer } from 'effect';
551
+ import { RpcServer } from 'effect/unstable/rpc';
552
+
553
+ const MainLive = RpcServer.layer(UserRpcs).pipe(
554
+ Layer.provide(UsersLive),
555
+ Layer.provide(RpcServer.layerProtocolWorkerRunner),
556
+ Layer.provide(BrowserWorkerRunner.layer)
557
+ );
558
+
559
+ Effect.runFork(Layer.launch(MainLive));
560
+ ```
561
+
562
+ ### Initial message
563
+
564
+ A worker client can send one schema-encoded value before any rpc traffic — spawn-time config without an extra round-trip. Server side, read it before/while serving:
565
+
566
+ ```ts
567
+ class WorkerConfig extends Schema.Class<WorkerConfig>('WorkerConfig')({
568
+ apiUrl: Schema.String
569
+ }) {}
570
+
571
+ // inside the worker (requires RpcServer.Protocol in context):
572
+ const config = yield* RpcWorker.initialMessage(WorkerConfig);
573
+ // fails with NoSuchElementError if the parent provided none
574
+ ```
575
+
576
+ The parent provides it with `RpcWorker.layerInitialMessage(WorkerConfig, buildEffect)` on the client protocol. Only the worker protocol supports initial messages — `protocol.initialMessage` is `Option.none()` everywhere else.
577
+
578
+ ---
579
+
580
+ ## 10. Testing Servers
581
+
582
+ ### `RpcTest.makeClient(group)` — in-process, no transport
583
+
584
+ Wires `RpcServer.makeNoSerialization` to a no-serialization client. Requests, stream chunks, acks, interrupts, headers, and middleware all behave like production (acks are enabled, so backpressure is testable). Required context: `Scope | Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs> | Rpc.MiddlewareClient<Rpcs>` — handler layers, middleware layers, **and** any `requiredForClient` client layers:
585
+
586
+ ```ts
587
+ import { assert, it } from '@effect/vitest';
588
+ import { RpcClient } from 'effect/unstable/rpc';
589
+
590
+ class UsersClient extends Context.Service<
591
+ UsersClient,
592
+ RpcClient.RpcClient<RpcGroup.Rpcs<typeof SecureRpcs>>
593
+ >()('UsersClient') {
594
+ static layerTest = Layer.effect(UsersClient)(RpcTest.makeClient(SecureRpcs)).pipe(
595
+ Layer.provide([UsersLive, AuthLive, TimingLive, AuthClient]) // AuthClient = RpcMiddleware.layerClient(...)
596
+ );
597
+ }
598
+
599
+ it.effect('GetUser', () =>
600
+ Effect.gen(function* () {
601
+ const client = yield* UsersClient;
602
+ const user = yield* client.GetUser({ id: '1' });
603
+ assert.deepStrictEqual(user, new User({ id: '1', name: 'Logged in user' }));
604
+ }).pipe(Effect.provide(UsersClient.layerTest)));
605
+ ```
606
+
607
+ `makeClient` accepts `{ flatten?: boolean }` mirroring `RpcClient.make`.
608
+
609
+ ### Transport integration tests
610
+
611
+ For exercising a real transport in-process, use `NodeHttpServer.layerTest` (provides both `HttpServer` and `HttpClient` on a random port) under your normal server+client layers — `packages/platform-node/test/RpcServer.test.ts` is the template; it runs one shared e2e suite against http/ws/tcp with the serializations each transport supports (plain json only over ws).
612
+
613
+ ### Unit-testing one handler
614
+
615
+ Use `group.accessHandler(tag)` (§1) — no client, no protocol, just the handler with its services.
616
+
617
+ ---
618
+
619
+ ## 11. Custom Protocols & `makeNoSerialization`
620
+
621
+ ### Custom `Protocol`
622
+
623
+ `RpcServer.Protocol.make` (built on the `withRun` buffering helper) builds a transport from a callback that receives `writeRequest` — the function you call with decoded `FromClientEncoded` messages. You return the rest of the service:
624
+
625
+ ```ts
626
+ const myProtocol = RpcServer.Protocol.make((writeRequest) =>
627
+ Effect.gen(function* () {
628
+ const disconnects = yield* Queue.make<number>();
629
+ // wire your transport: on inbound data → writeRequest(clientId, message)
630
+ return {
631
+ disconnects,
632
+ send: (clientId, response, _transferables) => sendToTransport(clientId, response),
633
+ end: (clientId) => Effect.void,
634
+ clientIds: Effect.sync(() => connectedIds),
635
+ initialMessage: Effect.succeedNone,
636
+ supportsAck: true,
637
+ supportsTransferables: false,
638
+ supportsSpanPropagation: false,
639
+ supportsNotifications: true
640
+ };
641
+ })
642
+ );
643
+ ```
644
+
645
+ Writes made before the server loop starts are buffered and replayed — you don't need to sequence startup manually. The message vocabulary lives in `RpcMessage` (`FromClientEncoded` = `RequestEncoded | AckEncoded | InterruptEncoded | Ping | Eof`; `FromServerEncoded` = `ResponseChunkEncoded | ResponseExitEncoded | ResponseDefectEncoded | Pong | ClientProtocolError | RequestEncoded`). The final `RequestEncoded` case is the server-originated request/notification surface.
646
+
647
+ ### `RpcServer.makeNoSerialization(group, options)`
648
+
649
+ The decoded core with no transport at all: you push `FromClient<Rpcs>` messages via the returned `server.write(clientId, message)` / `server.disconnect(clientId)`, and receive decoded `FromServer<Rpcs>` responses through `options.onFromServer`. Extra options beyond `make`: `disableSpanPropagation`, `disableClientAcks` (the serialized `make` derives both from the protocol's capabilities). This is what `RpcTest` and the cluster runtime build on — reach for it for in-process bridges where schema encoding would be wasted.
650
+
651
+ ---
652
+
653
+ ## Key Patterns
654
+
655
+ ### Production HTTP server (handlers + auth middleware + http protocol)
656
+
657
+ ```ts
658
+ // server/main.ts
659
+ import { createServer } from 'node:http';
660
+ import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
661
+ import { Effect, Layer } from 'effect';
662
+ import { HttpRouter } from 'effect/unstable/http';
663
+ import { RpcSerialization, RpcServer } from 'effect/unstable/rpc';
664
+ import { SecureRpcs } from '../domain/rpc.ts';
665
+
666
+ const UsersLive = SecureRpcs.toLayer(
667
+ Effect.gen(function* () {
668
+ const db = yield* Database;
669
+ return SecureRpcs.of({
670
+ GetUser: (payload) => db.findUser(payload.id),
671
+ StreamUsers: (payload) => db.changeFeed(payload.id)
672
+ });
673
+ })
674
+ );
675
+
676
+ const RpcLayer = RpcServer.layerHttp({
677
+ group: SecureRpcs,
678
+ path: '/rpc',
679
+ protocol: 'http',
680
+ disableFatalDefects: true
681
+ }).pipe(
682
+ Layer.provide([UsersLive, AuthLive]),
683
+ Layer.provide(RpcSerialization.layerNdjson) // framed → streaming rpcs stream over HTTP
684
+ );
685
+
686
+ const Main = HttpRouter.serve(RpcLayer).pipe(
687
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
688
+ );
689
+
690
+ NodeRuntime.runMain(Layer.launch(Main));
691
+ ```
692
+
693
+ Switch `protocol: 'websocket'` (and have clients connect a socket) to gain acks/backpressure and span propagation with no other changes.
694
+
695
+ ### RPC endpoint inside a larger router
696
+
697
+ ```ts
698
+ const RpcRoute = Layer.effectDiscard(
699
+ Effect.gen(function* () {
700
+ const router = yield* HttpRouter.HttpRouter;
701
+ yield* router.add('POST', '/rpc', yield* RpcServer.toHttpEffect(UserRpcs, {
702
+ disableFatalDefects: true
703
+ }));
704
+ })
705
+ ).pipe(Layer.provide([UsersLive, RpcSerialization.layerNdjson]));
706
+
707
+ const Main = HttpRouter.serve(Layer.mergeAll(RpcRoute, ApiRoutes, HealthRoute)).pipe(
708
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
709
+ );
710
+ ```
711
+
712
+ ### Streaming subscription with cleanup and abort logging
713
+
714
+ ```ts
715
+ StreamUsers: Effect.fnUntraced(function* (payload) {
716
+ const queue = yield* Queue.bounded<User, Cause.Done>(16);
717
+
718
+ yield* Effect.addFinalizer(() => Effect.log('feed closed', { id: payload.id }));
719
+
720
+ yield* db.subscribe(payload.id, (row) => Queue.offerUnsafe(queue, new User(row))).pipe(
721
+ Effect.forkScoped // tied to this request; interrupted on client abort
722
+ );
723
+
724
+ return queue;
725
+ });
726
+ ```
727
+
728
+ ### Read-heavy group with bounded writes
729
+
730
+ ```ts
731
+ const ServerLayer = RpcServer.layer(StoreRpcs, { concurrency: 1 }).pipe(
732
+ Layer.provide(
733
+ StoreRpcs.toLayer(
734
+ Effect.gen(function* () {
735
+ const state = yield* Ref.make(initialState);
736
+ return StoreRpcs.of({
737
+ // reads bypass the server-wide semaphore
738
+ Get: () => Ref.get(state).pipe(Rpc.fork),
739
+ // writes serialize through concurrency: 1 and survive aborts
740
+ Put: (payload) =>
741
+ Ref.update(state, applyPut(payload)).pipe(Rpc.uninterruptible)
742
+ });
743
+ })
744
+ )
745
+ )
746
+ );
747
+ ```
748
+
749
+ ## Common Mistakes
750
+
751
+ 1. **Importing from `@effect/rpc`.** v3 habit; the package does not exist in v4. Everything is `effect/unstable/rpc` (and platform layers come from `@effect/platform-node` / `-bun` / `-browser`).
752
+ 2. **Forgetting `protocol: 'http'` on `layerHttp`.** The default is `'websocket'` — your `POST /rpc` curl returns 404 and only a `GET` upgrade route exists. Also note `layerHttp` takes `{ group, path, ... }` as one options bag, while `layer(group, options)` takes the group positionally.
753
+ 3. **Providing handlers/middleware but no `Protocol` or `RpcSerialization`.** `RpcServer.layer` requires all of: handler layer(s), middleware implementation layers, a `layerProtocol*`, and (for non-worker protocols) a `RpcSerialization.layer*`. Missing ones surface as unresolved layer requirements.
754
+ 4. **`layerJson` on a raw TCP socket server.** No framing — decode breaks when messages span chunks. Sockets need `layerNdjson`, `layerNdJsonRpc()`, or `layerMsgPack`. (WebSocket is fine with `layerJson` — ws frames messages itself.)
755
+ 5. **Streaming rpcs over `layerProtocolHttp` + `layerJson` and wondering why chunks arrive all at once.** Unframed HTTP buffers the whole response until every request in the call finishes. Use a framed serialization to get a chunked streaming response, and remember HTTP has no acks → no backpressure either way.
756
+ 6. **Leaving `disableFatalDefects: false` in production.** One handler `die` then nukes every in-flight request on that connection with a connection-level defect. Set `true` to confine defects to the failing request.
757
+ 7. **Assuming `concurrency` is per-client.** It is one semaphore per server instance shared by all clients. Use `Rpc.fork` to exempt cheap read handlers instead of raising the global limit.
758
+ 8. **Treating `Rpc.fork`/`Rpc.uninterruptible` as rpc options.** They wrap the handler's *returned* Effect/Stream: `db.get(id).pipe(Rpc.fork)`. There is no `{ fork: true }` key on `Rpc.make`.
759
+ 9. **Typing the handler metadata as `clientId: number`.** It is `client: Rpc.ServerClient` with `client.id`, `client.annotations`, and `client.annotate(key, value)`.
760
+ 10. **Implementing middleware as `(payload, next) => ...`.** Server middleware is `(effect, { client, requestId, rpc, payload, headers }) => Effect` — you wrap the already-built handler effect (and `payload` is `unknown`). The `{ request, next }` shape belongs to client middleware (`RpcMiddleware.layerClient`).
761
+ 11. **Expecting first-attached middleware to run first.** The last attached middleware is outermost. `group.middleware(M)` also only affects rpcs already in the group — rpcs `.add(...)`ed later don't get it.
762
+ 12. **Forgetting client middleware layers in `RpcTest.makeClient`.** Its context includes `Rpc.MiddlewareClient<Rpcs>` — every `requiredForClient: true` middleware needs its `RpcMiddleware.layerClient` provided alongside the handler and server-middleware layers.
763
+ 13. **Detecting client aborts with `Effect.onInterrupt`.** Its callback only gets interruptor ids. Inspect the exit: `exit.cause.reasons.some((r) => r._tag === 'Interrupt' && r.annotations.has(RpcSchema.ClientAbort.key))`. Server-shutdown interrupts do *not* carry the annotation.
764
+ 14. **Ending a queue-form stream by failing it.** Use `Queue.end(queue)` (the `Cause.Done` signal) for a clean end-of-stream; a typed failure fails the client's stream instead. `Queue.end` requires `Cause.Done` in the queue's error channel — `Queue.bounded<User, Cause.Done>(16)` — or it won't typecheck.
765
+ 15. **Calling `RpcServer.toHttpEffect` outside a scope.** It forks the server with `Effect.forkScoped` — build it inside `Layer.effectDiscard` (or another `Scope`-providing context) or the server dies immediately. Also note it has no `concurrency` option; use `layer`/`layerHttp` if you need one.
766
+ 16. **Leaving framed HTTP response buffering implicit under heavy load.** It is bounded to 16 messages by default. Set `streamBufferSize` deliberately when throughput or memory behavior requires a different bound; use `'unbounded'` only intentionally.
767
+ 17. **Omitting notification capability from a custom protocol.** `supportsNotifications` is required; report whether server-originated notifications can actually be delivered.