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,624 @@
1
+ ---
2
+ name: effect-rpc-api
3
+ description: Define type-safe RPC contracts with effect/unstable/rpc — Rpc.make payload/success/error/defect schemas, RpcSchema.Stream streaming responses, RpcGroup composition (add/merge/omit/prefix/annotate), and RpcMiddleware.Service definitions shared by client and server. Use when declaring or evolving RPC procedures, building a shared contract package, adding streaming endpoints, or defining auth/observability middleware types.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in defining shared RPC contracts with `Rpc`, `RpcGroup`, `RpcSchema`, and `RpcMiddleware` from `effect/unstable/rpc`.
7
+
8
+ This skill covers the **contract layer**: the definitions that client and server packages both import. Wiring handlers into a server is the `effect-rpc-server` skill; constructing clients and protocols is the `effect-rpc-client` skill; distributed entities are the `effect-rpc-cluster` 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 are under `unstable` and move between betas.
13
+
14
+ Key files:
15
+
16
+ - `packages/effect/src/unstable/rpc/Rpc.ts` — `Rpc.make`, `Rpc.custom`, per-rpc combinators, `exitSchema`, `Wrapper` (`fork`/`uninterruptible`/`wrap`), `ServerClient`, every type helper (`Payload`, `Success`, `Error`, `Exit`, `ToHandlerFn`, `ResultFrom`, ...)
17
+ - `packages/effect/src/unstable/rpc/RpcGroup.ts` — group construction and composition (`add`/`merge`/`omit`/`prefix`/`middleware`), group vs per-rpc annotations, handler-conversion surface (`toLayer`/`toHandlers`/`toLayerHandler`/`accessHandler`/`of`)
18
+ - `packages/effect/src/unstable/rpc/RpcSchema.ts` — the `Stream` schema marker, `isStreamSchema`, `ClientAbort` cause annotation
19
+ - `packages/effect/src/unstable/rpc/RpcMiddleware.ts` — `Service` constructor, `layerClient`, `ForClient`, `ApplyServices` and the middleware function shapes
20
+ - `packages/effect/src/unstable/rpc/RpcMessage.ts` — wire envelopes: `Request`, `Ack`, `Interrupt`, `Eof`, `Ping`, `ResponseChunk`, `ResponseExit`, `ExitEncoded`, `RequestId`
21
+ - `packages/effect/src/unstable/rpc/RpcClientError.ts` — the transport error type referenced when typing shared client aliases
22
+ - `packages/effect/src/unstable/rpc/index.ts` — public exports of the rpc namespace
23
+ - `packages/platform-node/test/fixtures/rpc-schemas.ts` — the best real-world contract fixture: rpcs, streaming, middleware, deferred responses
24
+ - `packages/effect/test/rpc/Rpc.test.ts` — `exitSchema`, custom defect schemas, `getStreamSchemas` semantics
25
+
26
+ ## Core Model
27
+
28
+ An `Rpc` is a **value-level contract** for one procedure:
29
+
30
+ ```ts
31
+ interface Rpc<
32
+ Tag extends string,
33
+ Payload extends Schema.Top = Schema.Void,
34
+ Success extends Schema.Top = Schema.Void,
35
+ Error extends Schema.Top = Schema.Never,
36
+ Middleware extends RpcMiddleware.AnyService = never,
37
+ Requires = never
38
+ > // _tag, payloadSchema, successSchema, errorSchema, defectSchema,
39
+ // annotations: Context.Context<never>, middlewares: ReadonlySet<Middleware>
40
+ ```
41
+
42
+ It records a tag, four schemas (payload, success, error, defect), a set of middleware service keys, and a `Context` of annotations. An `RpcGroup<R>` is an immutable `ReadonlyMap<tag, Rpc>` plus group-level annotations. Neither does any I/O — servers interpret them into handlers, clients into methods, entities into mailboxes. Define them once in a shared module and import them everywhere.
43
+
44
+ Both `Rpc` and `RpcGroup` declare `new (_: never): {}`, so both `const X = Rpc.make(...)` and `class X extends Rpc.make(...) {}` are valid (same for groups). All combinators return **new values** — these structures are immutable.
45
+
46
+ Imports used throughout (all from the `effect` package; there is no `@effect/rpc` in v4):
47
+
48
+ ```ts
49
+ import { Context, Schema } from 'effect';
50
+ import { Rpc, RpcGroup, RpcMiddleware, RpcSchema } from 'effect/unstable/rpc';
51
+ ```
52
+
53
+ Deep subpath imports also work (the package exports a `./*` wildcard), e.g. `import * as RpcSchema from 'effect/unstable/rpc/RpcSchema'` or `import { RpcClientError } from 'effect/unstable/rpc/RpcClientError'`.
54
+
55
+ ---
56
+
57
+ ## 1. Defining RPCs — `Rpc.make`
58
+
59
+ ```ts
60
+ Rpc.make(tag, {
61
+ payload?: Schema.Top | Schema.Struct.Fields, // default Schema.Void
62
+ success?: Schema.Top, // default Schema.Void
63
+ error?: Schema.Top, // default Schema.Never
64
+ defect?: Rpc.DefectSchema, // default Schema.Defect()
65
+ stream?: boolean, // default false
66
+ primaryKey?: (payload) => string // only with struct-fields payload
67
+ });
68
+ ```
69
+
70
+ Two declaration styles, both official:
71
+
72
+ ```ts
73
+ import { Schema } from 'effect';
74
+ import { Rpc } from 'effect/unstable/rpc';
75
+
76
+ export class User extends Schema.Class<User>('User')({
77
+ id: Schema.String,
78
+ name: Schema.String
79
+ }) {}
80
+
81
+ export class UserNotFound extends Schema.TaggedError<UserNotFound>()('UserNotFound', {
82
+ id: Schema.String
83
+ }) {}
84
+
85
+ // Style A — const value. Compact; good when listing many rpcs in one file.
86
+ export const Ping = Rpc.make('Ping', { success: Schema.String });
87
+
88
+ // Style B — class extends. Nominal identity; good for rpcs imported widely.
89
+ export class GetUser extends Rpc.make('GetUser', {
90
+ payload: { id: Schema.String },
91
+ success: User,
92
+ error: UserNotFound
93
+ }) {}
94
+ ```
95
+
96
+ With class style, the class itself is the rpc value (`RpcGroup.make(GetUser, ...)`) and `typeof GetUser` works with all `Rpc.*` type helpers.
97
+
98
+ ### Payload: struct fields vs schema
99
+
100
+ ```ts
101
+ // Inline fields — Rpc.make builds Schema.Struct({ ... }) for you
102
+ Rpc.make('CreateUser', {
103
+ payload: { name: Schema.String, email: Schema.String },
104
+ success: User
105
+ });
106
+
107
+ // Named schema — reuse a Schema.Class (or any Schema.Top)
108
+ class CreateUserInput extends Schema.Class<CreateUserInput>('CreateUserInput')({
109
+ name: Schema.String,
110
+ email: Schema.String
111
+ }) {}
112
+ Rpc.make('CreateUser', { payload: CreateUserInput, success: User });
113
+ ```
114
+
115
+ With the default `Schema.Void` payload, the generated client method takes no payload argument (`client.Ping()`).
116
+
117
+ ### `defect` — controlling defect serialization
118
+
119
+ Defects (`Effect.die`) cross the wire through `defectSchema`. The default `Schema.Defect()` round-trips defects as `unknown`, JSON-encoding `Error` values as `{ name, message, cause? }` and **stripping stack traces** for security. Options:
120
+
121
+ ```ts
122
+ // Keep stack traces across the wire
123
+ Rpc.make('Risky', { defect: Schema.Defect({ includeStack: true }) });
124
+
125
+ // Also available: { excludeCause: true } to drop Error.cause
126
+ // Or any schema satisfying Rpc.DefectSchema (decodes/encodes with no services):
127
+ Rpc.make('RiskyRaw', { defect: Schema.Any });
128
+ ```
129
+
130
+ Note `Schema.Defect()` normalizes: a non-`Error` object like `{ message: 'boom' }` decodes back as an `Error`; non-JSON values fall back to a formatted string.
131
+
132
+ ### `primaryKey` — deterministic payload identity
133
+
134
+ `primaryKey` is only available when `payload` is given as **struct fields** (the option is typed `never` for schema payloads). It makes `Rpc.make` build a `Schema.Class` for the payload that implements `PrimaryKey.symbol`, giving each payload value a deterministic string identity. The cluster layer uses it to dedupe retried sends of persisted messages:
135
+
136
+ ```ts
137
+ export const Charge = Rpc.make('Charge', {
138
+ payload: { invoiceId: Schema.String, amountCents: Schema.Int },
139
+ success: Schema.Boolean,
140
+ primaryKey: ({ invoiceId }) => invoiceId
141
+ });
142
+ ```
143
+
144
+ Clients still pass plain objects; the class is an implementation detail of the payload schema. See the `effect-rpc-cluster` skill for persistence semantics.
145
+
146
+ ---
147
+
148
+ ## 2. Streaming Contracts — `stream: true` and `RpcSchema.Stream`
149
+
150
+ `stream: true` changes how the `success` and `error` options are interpreted:
151
+
152
+ - `successSchema` becomes `RpcSchema.Stream(success, error)` — `success` is the **element** schema, `error` is the **stream error** schema
153
+ - `errorSchema` is set to `Schema.Never` (the declared `error` moved into the stream)
154
+
155
+ ```ts
156
+ export class StreamUsers extends Rpc.make('StreamUsers', {
157
+ payload: { since: Schema.DateTimeUtc },
158
+ success: User, // element schema, not the Effect success
159
+ error: UserNotFound, // stream error schema
160
+ stream: true
161
+ }) {}
162
+ ```
163
+
164
+ Consequences for both sides of the contract:
165
+
166
+ - the handler must return `Stream<User, UserNotFound, R>` — or `Effect<Queue.Dequeue<User, UserNotFound | Cause.Done>, ..., R>` to drive the queue itself
167
+ - the client sees `Stream<User, UserNotFound | RpcClientError>` (or a `Queue.Dequeue` with `{ asQueue: true }` — see the `effect-rpc-client` skill)
168
+ - the rpc's terminal `Exit` success is `void`; elements travel separately as `Chunk` messages
169
+
170
+ `RpcSchema.Stream` is itself a schema, so `Rpc.make('X', { success: RpcSchema.Stream(User, UserNotFound) })` is equivalent — `stream: true` is just the ergonomic spelling. Introspect with:
171
+
172
+ ```ts
173
+ RpcSchema.isStreamSchema(StreamUsers.successSchema); // true
174
+ // streamSchema.success / streamSchema.error hold the element/error schemas
175
+ ```
176
+
177
+ ### `RpcSchema.ClientAbort`
178
+
179
+ `ClientAbort` is a `Cause` annotation marking interrupts that originated from a client abort: when a client interrupts a call, the server interrupts the handler fiber with it attached to the `Interrupt` reason. Servers distinguish client cancellation from shutdown by checking for `RpcSchema.ClientAbort.key` on `Interrupt` reasons — the handler-side detection snippet lives in the `effect-rpc-server` skill.
180
+
181
+ ---
182
+
183
+ ## 3. Per-RPC Combinators
184
+
185
+ Every `Rpc` is `Pipeable` and exposes instance methods, each returning a new rpc:
186
+
187
+ ```ts
188
+ Rpc.make('GetUser', { payload: { id: Schema.String }, success: User })
189
+ .setSuccess(Schema.Option(User)) // swap success schema
190
+ .setError(UserNotFound) // swap error schema
191
+ .setPayload({ id: Schema.String, tenant: Schema.String }) // fields or Schema.Top
192
+ .middleware(AuthMiddleware) // attach a middleware service key
193
+ .prefix('users.') // tag becomes 'users.GetUser'
194
+ .annotate(SomeKey, value) // add one annotation
195
+ .annotateMerge(someContext); // merge a Context.Context<I>
196
+ ```
197
+
198
+ Notes:
199
+
200
+ - `prefix` changes `_tag`, which is the **wire identity** of the procedure (and the key handlers are registered under). Prefix at definition time, before anything depends on the tag.
201
+ - `annotate`/`annotateMerge` attach metadata read by servers, clusters, and proxies (e.g. `ClusterSchema.Persisted`). Annotations are an open `Context`, so any `Context.Key` works — including your own.
202
+ - `middleware` accumulates into a `ReadonlySet`; attaching also threads the middleware's `provides`/`requires` through the rpc's `Requires` type parameter (section 6).
203
+
204
+ ---
205
+
206
+ ## 4. Composing Contracts — `RpcGroup`
207
+
208
+ `RpcGroup.make(...rpcs)` is variadic:
209
+
210
+ ```ts
211
+ export const UsersGroup = RpcGroup.make(GetUser, CreateUser, StreamUsers);
212
+ ```
213
+
214
+ The rpcs are reachable at `group.requests` (`ReadonlyMap<string, Rpc>`) — useful for contract introspection tests and codegen; `Rpc.isRpc(u)` guards individual values.
215
+
216
+ ### Combinators
217
+
218
+ ```ts
219
+ UsersGroup
220
+ .add(DeleteUser, UpdateUser) // append rpcs (same-tag add replaces)
221
+ .merge(OrdersGroup, PaymentsGroup) // union of groups
222
+ .omit('DeleteUser', 'UpdateUser') // remove by tag (variadic)
223
+ .prefix('v2.') // re-tag every rpc
224
+ .middleware(AuthMiddleware); // attach to every rpc currently in the group
225
+ ```
226
+
227
+ Sharp edges, verified in source:
228
+
229
+ - `merge` is **last-wins** on duplicate rpc tags *and* duplicate group-annotation keys. No error, silent override.
230
+ - `middleware` and the `annotateRpcs*` methods only affect rpcs **already in the group**. Rpcs added afterwards via `.add(...)` are not covered — re-apply, or attach on the rpc itself before adding.
231
+ - `omit` removes the rpc from the group but does not return it; keep the original rpc value if you need it elsewhere.
232
+
233
+ ### Group-level vs per-rpc annotations
234
+
235
+ ```ts
236
+ group.annotate(SomeKey, value); // on the group itself
237
+ group.annotateMerge(context); // merge Context into the group
238
+ group.annotateRpcs(SomeKey, value); // on every rpc currently in the group
239
+ group.annotateRpcsMerge(context); // merge Context into every rpc
240
+ ```
241
+
242
+ `annotateRpcs*` does **not** override an annotation already set on an individual rpc — the implementation merges as `Context.merge(context, rpc.annotations)`, so the rpc's own value wins. Use this deliberately: set per-rpc exceptions first, then apply the group default.
243
+
244
+ ### Handler-conversion surface (pointer)
245
+
246
+ `RpcGroup` also carries the methods that turn a contract into a server implementation — `toLayer(handlers | Effect<handlers>)`, `toLayerHandler(tag, handler)`, `toHandlers(handlers)`, `accessHandler(tag)`, and the identity type-checker `of(handlers)`. Their option signatures and wiring belong to the `effect-rpc-server` skill; what matters at the contract level is that the handler **shape** is fully derived from the rpc definitions (section 8), so a contract change is a compile error in every handler and client.
247
+
248
+ ---
249
+
250
+ ## 5. Errors, Defects, and the Exit Schema
251
+
252
+ An rpc's effective error union is **derived**, not declared in one place:
253
+
254
+ ```
255
+ Rpc.Error<R> = declared error schema | every attached middleware's error schema
256
+ ```
257
+
258
+ On top of that, clients add transport errors (`RpcClientError`) and any middleware `clientError` types — see the `effect-rpc-client` skill. Never re-declare a middleware's error in the rpc's `error` option; attaching the middleware adds it automatically.
259
+
260
+ Use schema error classes so errors are tagged, yieldable, and serializable:
261
+
262
+ ```ts
263
+ export class RateLimited extends Schema.TaggedError<RateLimited>()('RateLimited', {
264
+ retryAfterMillis: Schema.Number
265
+ }) {}
266
+
267
+ export class OrderError extends Schema.TaggedError<OrderError>()('OrderError', {
268
+ reason: Schema.Literals(['empty-cart', 'payment-failed'])
269
+ }) {}
270
+
271
+ const PlaceOrder = Rpc.make('PlaceOrder', {
272
+ payload: { cartId: Schema.String },
273
+ success: Schema.String,
274
+ error: Schema.Union([OrderError, RateLimited]) // unions take an array in v4
275
+ });
276
+ ```
277
+
278
+ ### `Rpc.exitSchema`
279
+
280
+ `Rpc.exitSchema(rpc)` builds the `Schema.Exit` that servers use to encode results and clients use to decode them:
281
+
282
+ - success side: the success schema — or `Schema.Void` for streaming rpcs
283
+ - failure side: `Schema.Union([...])` of the rpc error schema, the stream error schema (if streaming), and every middleware error schema
284
+ - defect side: the rpc's `defectSchema`
285
+
286
+ The result is cached per rpc value (WeakMap). Useful for serialization tests and generic envelope tooling:
287
+
288
+ ```ts
289
+ import { Exit } from 'effect';
290
+
291
+ const schema = Rpc.exitSchema(PlaceOrder);
292
+ const encoded = Schema.encodeSync(schema)(Exit.fail(new RateLimited({ retryAfterMillis: 100 })));
293
+ const roundTripped = Schema.decodeSync(schema)(encoded);
294
+ ```
295
+
296
+ ---
297
+
298
+ ## 6. Middleware Definition — `RpcMiddleware.Service`
299
+
300
+ A middleware is declared as a `Context.Service` class with two parameter slots:
301
+
302
+ ```ts
303
+ import { Context, Schema } from 'effect';
304
+ import { RpcMiddleware } from 'effect/unstable/rpc';
305
+
306
+ export class CurrentUser extends Context.Service<CurrentUser, User>()('CurrentUser') {}
307
+
308
+ export class Unauthorized extends Schema.TaggedError<Unauthorized>()('Unauthorized', {}) {}
309
+
310
+ export class AuthMiddleware extends RpcMiddleware.Service<AuthMiddleware, {
311
+ provides: CurrentUser; // services injected into wrapped handlers
312
+ requires: never; // services this middleware needs from the outer context
313
+ clientError: never; // error type only the client-side wrapper can produce
314
+ }>()('AuthMiddleware', {
315
+ error: Unauthorized, // wire-error schema; default Schema.Never
316
+ requiredForClient: true // default false
317
+ }) {}
318
+ ```
319
+
320
+ Exact shape, verified in source:
321
+
322
+ - The **config type parameter** carries `{ requires?, provides?, clientError? }` — all optional, all defaulting to `never`. A bare `RpcMiddleware.Service<TimingMiddleware>()('TimingMiddleware')` is a pure observer.
323
+ - The **options object** has exactly two keys: `error` (a schema; becomes part of the rpc error union, section 5) and `requiredForClient` (boolean). There is no `optional`, `wrap`, `failure`, or `provides` option key — those were v3 spellings.
324
+ - The class statics expose `.error` and `.requiredForClient`, and the class itself is the `Context.Key` you attach with `rpc.middleware(AuthMiddleware)` / `group.middleware(AuthMiddleware)`.
325
+
326
+ ### What attaching does to types
327
+
328
+ `rpc.middleware(M)` computes `Requires_new = Exclude<Requires_old, Provides<M>> | Requires<M>` (`RpcMiddleware.ApplyServices`). So:
329
+
330
+ - handlers for the rpc receive `Provides<M>` in their environment for free (`CurrentUser` above)
331
+ - a middleware whose `requires` names a service is satisfied by attaching a *provider middleware after it* — unfulfilled `requires` surface as extra requirements on the handlers layer
332
+ - `requiredForClient: true` adds `RpcMiddleware.ForClient<M>` to the client's required context, making a missing client implementation a **compile-time** error. Without it, clients silently skip middleware that has no client layer provided.
333
+
334
+ ### The server-side function shape (for reference)
335
+
336
+ The service value is a function `(effect, options) => Effect` — options: `{ client, requestId, rpc, payload, headers }` (`rpc` is typed `Rpc.AnyWithProps`). It wraps handler execution: it cannot change the success value (opaque `SuccessValue`), but it can provide services, fail with the wire error, and observe the exit. Execution order: middlewares apply in attachment order with the **last attached outermost**, on both server and client. The `Layer` implementation (`Layer.succeed(AuthMiddleware)(AuthMiddleware.of(...))`) belongs to the `effect-rpc-server` skill.
337
+
338
+ ### The client-side counterpart — `RpcMiddleware.layerClient`
339
+
340
+ Defined alongside the contract so client packages can provide it. The function receives `{ rpc, request, next }` and **must** call `next` (with the original or a modified request):
341
+
342
+ ```ts
343
+ import { Headers } from 'effect/unstable/http';
344
+
345
+ export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) =>
346
+ next({
347
+ ...request,
348
+ headers: Headers.set(request.headers, 'authorization', `Bearer ${getToken()}`)
349
+ })
350
+ );
351
+ // Layer<RpcMiddleware.ForClient<AuthMiddleware>>
352
+ ```
353
+
354
+ `layerClient` also accepts an `Effect` that builds the function (for middleware needing services); the layer's environment is captured and merged into every invocation. Failures here surface as the middleware's `error` type or the `clientError` type from the config.
355
+
356
+ ---
357
+
358
+ ## 7. Custom Constructors — `Rpc.custom`
359
+
360
+ `Rpc.custom` builds an `Rpc.make`-alike that transforms every rpc's success/error schemas — encode a convention once (paginated lists, response envelopes) instead of repeating it per rpc:
361
+
362
+ ```ts
363
+ import { Schema } from 'effect';
364
+ import { Rpc } from 'effect/unstable/rpc';
365
+
366
+ // Type-level definition: how success/error transform
367
+ export interface RpcWithPagination extends Rpc.Custom {
368
+ readonly out: Rpc.Custom.Out<Paginated<this['success']>, this['error']>;
369
+ }
370
+
371
+ export interface Paginated<S extends Schema.Top> extends
372
+ Schema.Struct<{
373
+ readonly offset: Schema.Number;
374
+ readonly total: Schema.Number;
375
+ readonly results: Schema.$Array<S>;
376
+ }>
377
+ {}
378
+
379
+ // Value-level implementation: receives { success, error, defect }, returns the transformed set
380
+ export const makePaginated = Rpc.custom<RpcWithPagination>((schemas) => ({
381
+ ...schemas,
382
+ success: Schema.Struct({
383
+ offset: Schema.Number,
384
+ total: Schema.Number,
385
+ results: Schema.Array(schemas.success)
386
+ })
387
+ }));
388
+
389
+ // Used exactly like Rpc.make — payload, stream, primaryKey all still work
390
+ export const ListUsers = makePaginated('listUsers', { success: User });
391
+ // success type: { offset: number, total: number, results: readonly User[] }
392
+ ```
393
+
394
+ The transformation applies to `success`/`error`/`defect` only; `payload`, `stream`, and `primaryKey` pass through unchanged. With `stream: true`, the transformed success becomes the stream **element** schema.
395
+
396
+ ---
397
+
398
+ ## 8. The Handler Contract and Wrappers
399
+
400
+ The contract fully determines the handler signature (`Rpc.ToHandlerFn`):
401
+
402
+ ```ts
403
+ (
404
+ payload: Rpc.Payload<R>,
405
+ options: {
406
+ readonly client: Rpc.ServerClient; // client.id: number; client.annotate(key, value)
407
+ readonly requestId: RequestId;
408
+ readonly headers: Headers;
409
+ readonly rpc: R;
410
+ }
411
+ ) => Rpc.WrapperOr<Rpc.ResultFrom<R, Services>>
412
+ ```
413
+
414
+ `Rpc.ResultFrom` — what a handler may return:
415
+
416
+ - **non-stream rpc**: `Effect<Success | Deferred<Success, Error>, Error, R>`. Succeeding with a `Deferred` defers the terminal exit until the deferred completes — invisible on the wire, useful when the result arrives from another fiber/webhook later.
417
+ - **stream rpc**: `Stream<Element, StreamError, R>` or `Effect<Queue.Dequeue<Element, StreamError | Cause.Done>, ..., R>` (end-of-stream is `Cause.Done` in the queue's error channel).
418
+
419
+ `Scope` is always available to handlers (the server scopes each request), and services in any attached middleware's `provides` are excluded from the handler's requirements.
420
+
421
+ ### Wrappers — `Rpc.fork`, `Rpc.uninterruptible`, `Rpc.wrap`
422
+
423
+ Execution-mode hints are **wrappers around the handler's return value**, not `Rpc.make` options:
424
+
425
+ ```ts
426
+ GetCount: () => Ref.get(count).pipe(Rpc.fork); // bypass server concurrency limit
427
+ Charge: (payload) => chargeOnce(payload).pipe(Rpc.uninterruptible); // must complete
428
+ Both: (payload) => work(payload).pipe(Rpc.wrap({ fork: true, uninterruptible: true }));
429
+ ```
430
+
431
+ `wrap` on an already-wrapped value inherits unspecified options from the existing wrapper. Introspection helpers: `Rpc.isWrapper(u)`, `Rpc.unwrap(value)`, `Rpc.wrapMap(value, f)` (maps the inner value, preserving options).
432
+
433
+ ---
434
+
435
+ ## 9. Wire-Format Awareness — `RpcMessage`
436
+
437
+ You rarely touch `RpcMessage` directly, but the envelope explains several contract-level facts:
438
+
439
+ | Direction | Decoded messages | Purpose |
440
+ |---|---|---|
441
+ | client → server | decoded `Request`, `Ack`, `Interrupt`, `Eof`; encoded `RequestEncoded`, `AckEncoded`, `InterruptEncoded`, `Ping`, `Eof` | call, stream backpressure ack, cancellation, end-of-input, keepalive |
442
+ | server → client | decoded `ResponseChunk`, `ResponseExit`, `ResponseDefect`, `ClientEnd`; encoded `ResponseChunkEncoded`, `ResponseExitEncoded`, `ResponseDefectEncoded`, `Pong`, `ClientProtocolError`, `RequestEncoded` | stream elements, terminal exit, defects, lifecycle, and server-originated requests/notifications |
443
+
444
+ Key facts:
445
+
446
+ - A `RequestEncoded` carries `{ _tag: 'Request', id: string | number, tag: string, payload: unknown, headers: Array<[string, string]>, isNotification?, traceId?, spanId?, sampled? }`. The rpc's `_tag` **is the wire identity** — renaming or re-prefixing an rpc is a breaking protocol change for deployed clients.
447
+ - `RequestId` is a branded `string | number`; construct it with `RequestId(1)` or `RequestId('1')` (from `effect/unstable/rpc/RpcMessage`). You need it when invoking handlers manually in tests via `accessHandler`. `bigint` is not accepted.
448
+ - Terminal results travel as `ExitEncoded` — `Success` with a value, or `Failure` with a cause array of `Fail` (your error union, encoded), `Die` (via `defectSchema`), and `Interrupt` entries. This is exactly what `Rpc.exitSchema` encodes/decodes.
449
+ - Stream elements travel as batched `Chunk` messages, acknowledged by `Ack` on ack-capable protocols (sockets, workers); the HTTP protocol declares `supportsAck: false`. Incremental delivery requires a serialization with framing (e.g. ndjson) — with non-framed json over HTTP the chunks are buffered and returned in one final batch (see serialization notes in the `effect-rpc-cluster` skill).
450
+ - Servers represent server-originated calls and notifications with `RequestEncoded` in `FromServerEncoded`. Set `isNotification: true` for a notification; JSON-RPC serialization then omits `id`. Buffered, unframed JSON-RPC HTTP cannot deliver notifications and drops them, while framed HTTP, sockets, stdio, and workers support them.
451
+
452
+ ---
453
+
454
+ ## 10. Type Helpers for Shared Contract Packages
455
+
456
+ `Rpc.*` type extractors (all take the rpc type, e.g. `typeof GetUser`):
457
+
458
+ ```ts
459
+ Rpc.Tag<R>; // 'GetUser'
460
+ Rpc.Payload<R>; // decoded payload type
461
+ Rpc.PayloadConstructor<R>; // input accepted by payload construction
462
+ Rpc.Success<R>; // decoded success (Stream<...> for streaming rpcs)
463
+ Rpc.SuccessEncoded<R>; // encoded success
464
+ Rpc.SuccessChunk<R>; // stream element type (never for non-stream)
465
+ Rpc.Error<R>; // declared error | middleware errors (decoded)
466
+ Rpc.Exit<R>; // Exit<SuccessExit, ErrorExit> — stream rpcs have void success
467
+ Rpc.Middleware<R>; // middleware service identifiers
468
+ Rpc.MiddlewareClient<R>; // ForClient<...> for requiredForClient middleware
469
+ Rpc.Services<R>; // schema en/decoding services (both sides)
470
+ Rpc.ServicesClient<R>; // client-side schema services
471
+ Rpc.ServicesServer<R>; // server-side schema services
472
+ Rpc.ExtractTag<R, 'GetUser'>; // select one rpc from a union
473
+ Rpc.ToHandler<R>; // the Handler service type servers require
474
+ ```
475
+
476
+ For generic utilities, constrain on `Rpc.Any` (tag + annotations only) or `Rpc.AnyWithProps` (all schemas/middleware visible — the type of `options.rpc` in middleware); groups use `RpcGroup.Any`. Niche helpers (`SuccessSchema`/`ErrorSchema`, `SuccessExit`/`ErrorExit`, `IsStream`, `Prefixed`, `AddError`, `AddMiddleware`, `ExtractProvides`/`ExtractRequires`/`ExcludeProvides`) also live in `Rpc.ts` — read the source when writing generic tooling.
477
+
478
+ `RpcGroup.*` helpers:
479
+
480
+ ```ts
481
+ RpcGroup.Rpcs<typeof UsersGroup>; // union of the group's rpc definitions
482
+ RpcGroup.HandlersFrom<R>; // { [tag]: handler fn } object type
483
+ RpcGroup.HandlerFrom<R, Tag>; // one handler fn type
484
+ ```
485
+
486
+ The canonical use — typing a client service in the shared package without constructing anything:
487
+
488
+ ```ts
489
+ import type { RpcClient } from 'effect/unstable/rpc';
490
+ import type { RpcClientError } from 'effect/unstable/rpc/RpcClientError';
491
+
492
+ export type UsersClient = RpcClient.RpcClient<
493
+ RpcGroup.Rpcs<typeof UsersGroup>,
494
+ RpcClientError
495
+ >;
496
+ ```
497
+
498
+ ---
499
+
500
+ ## Key Patterns
501
+
502
+ ### A complete shared contract module
503
+
504
+ Everything client and server need, with zero runtime wiring:
505
+
506
+ ```ts
507
+ // contracts/users.ts — imported by both server and client packages
508
+ import { Context, Schema } from 'effect';
509
+ import { Rpc, RpcGroup, RpcMiddleware } from 'effect/unstable/rpc';
510
+
511
+ // --- domain schemas ---
512
+ export class User extends Schema.Class<User>('User')({
513
+ id: Schema.String,
514
+ name: Schema.String,
515
+ createdAt: Schema.DateTimeUtc
516
+ }) {}
517
+
518
+ // --- errors ---
519
+ export class UserNotFound extends Schema.TaggedError<UserNotFound>()('UserNotFound', {
520
+ id: Schema.String
521
+ }) {}
522
+
523
+ export class Unauthorized extends Schema.TaggedError<Unauthorized>()('Unauthorized', {}) {}
524
+
525
+ // --- middleware contract ---
526
+ export class CurrentUser extends Context.Service<CurrentUser, User>()('CurrentUser') {}
527
+
528
+ export class AuthMiddleware extends RpcMiddleware.Service<AuthMiddleware, {
529
+ provides: CurrentUser;
530
+ }>()('AuthMiddleware', {
531
+ error: Unauthorized,
532
+ requiredForClient: true
533
+ }) {}
534
+
535
+ // --- procedures ---
536
+ export class GetUser extends Rpc.make('GetUser', {
537
+ payload: { id: Schema.String },
538
+ success: User,
539
+ error: UserNotFound
540
+ }) {}
541
+
542
+ export class CreateUser extends Rpc.make('CreateUser', {
543
+ payload: { name: Schema.String },
544
+ success: User,
545
+ primaryKey: ({ name }) => name // idempotent under cluster persistence
546
+ }) {}
547
+
548
+ export class WatchUsers extends Rpc.make('WatchUsers', {
549
+ payload: { since: Schema.DateTimeUtc },
550
+ success: User, // stream element
551
+ error: UserNotFound, // stream error
552
+ stream: true
553
+ }) {}
554
+
555
+ // Public group: everything requires auth
556
+ export const UsersGroup = RpcGroup.make(GetUser, CreateUser, WatchUsers)
557
+ .middleware(AuthMiddleware);
558
+ ```
559
+
560
+ The server package implements `UsersGroup.toLayer(...)` and a `Layer.succeed(AuthMiddleware)(...)` (see `effect-rpc-server`); the client package builds `RpcClient.make(UsersGroup)` plus `RpcMiddleware.layerClient(AuthMiddleware, ...)` (see `effect-rpc-client`).
561
+
562
+ ### Versioning and namespacing with `prefix` + `merge`
563
+
564
+ ```ts
565
+ const V1 = RpcGroup.make(GetUser, CreateUser).prefix('v1.');
566
+ const V2 = RpcGroup.make(GetUserV2, CreateUser, WatchUsers).prefix('v2.');
567
+
568
+ // One group served at one endpoint; tags are 'v1.GetUser', 'v2.GetUser', ...
569
+ export const ApiGroup = V1.merge(V2);
570
+ ```
571
+
572
+ Handler objects key by the **prefixed** tag (quoted keys: `'v1.GetUser': (payload) => ...`). Remember `merge` is last-wins on tag collisions — prefix before merging to make collisions impossible.
573
+
574
+ ### Trimming a group for a restricted surface
575
+
576
+ ```ts
577
+ // Internal group has admin procedures; public surface omits them
578
+ export const AdminGroup = RpcGroup.make(GetUser, CreateUser, DeleteUser, PurgeAll);
579
+ export const PublicGroup = AdminGroup.omit('DeleteUser', 'PurgeAll').middleware(AuthMiddleware);
580
+ ```
581
+
582
+ ### Marking a group for the cluster
583
+
584
+ Contracts double as entity protocols — annotate, then hand to `Entity.fromRpcGroup`:
585
+
586
+ ```ts
587
+ import { ClusterSchema } from 'effect/unstable/cluster';
588
+
589
+ export const DurableUsers = UsersGroup.annotateRpcs(ClusterSchema.Persisted, true);
590
+ // Entity.fromRpcGroup('Users', DurableUsers) — see the effect-rpc-cluster skill
591
+ ```
592
+
593
+ ### Exit-schema round-trip test for a contract
594
+
595
+ ```ts
596
+ import { assert, it } from '@effect/vitest';
597
+ import { Exit, Schema } from 'effect';
598
+ import { Rpc } from 'effect/unstable/rpc';
599
+
600
+ it('GetUser exits round-trip', () => {
601
+ const schema = Rpc.exitSchema(GetUser);
602
+ const exit = Exit.fail(new UserNotFound({ id: 'u1' }));
603
+ const decoded = Schema.decodeSync(schema)(Schema.encodeSync(schema)(exit));
604
+ assert(Exit.isFailure(decoded));
605
+ });
606
+ ```
607
+
608
+ ## Common Mistakes
609
+
610
+ 1. **Importing from `@effect/rpc`.** The package does not exist in v4 — everything is `effect/unstable/rpc` (deep subpaths like `effect/unstable/rpc/RpcMessage` also work).
611
+ 2. **Reaching for `Rpc.fromTaggedRequest` / `Schema.TaggedRequest`.** Neither exists in v4. The tag is `Rpc.make`'s first argument; the payload schema carries no `_tag` field — the wire envelope transports the tag separately.
612
+ 3. **Expecting `error` to stay the Effect error with `stream: true`.** It becomes the *stream* error schema and the rpc's `errorSchema` is set to `Schema.Never`. Likewise `success` becomes the *element* schema.
613
+ 4. **Passing `primaryKey` with a schema payload.** It is typed `never` unless `payload` is inline struct fields — wrap the fields inline or drop `primaryKey`.
614
+ 5. **Using v3 middleware option keys.** `RpcMiddleware.Service` options are exactly `{ error?, requiredForClient? }`; `provides`/`requires`/`clientError` go in the second *type parameter*, and `optional`/`wrap`/`failure` do not exist (v4 middleware always wraps).
615
+ 6. **Re-declaring a middleware's error in the rpc's `error` option.** Attaching the middleware already unions its `error` schema into `Rpc.Error` and `exitSchema` — declaring it twice bloats the wire union.
616
+ 7. **Assuming `group.middleware(...)` / `annotateRpcs(...)` cover later additions.** They snapshot the rpcs currently in the group; rpcs added afterwards via `.add(...)` are unaffected.
617
+ 8. **Expecting `annotateRpcs` to override per-rpc annotations.** Per-rpc values win (`Context.merge(context, rpc.annotations)`); the group call only fills in missing keys.
618
+ 9. **Relying on `merge` to detect tag collisions.** It is silent last-wins for both rpc tags and group annotation keys — `prefix` before merging.
619
+ 10. **Treating `Rpc.fork`/`Rpc.uninterruptible` as `Rpc.make` options.** They are wrappers applied to a handler's *return value*: `effect.pipe(Rpc.fork)`.
620
+ 11. **Mutating in place.** `annotate`, `prefix`, `middleware`, `setSuccess`, etc. all return new `Rpc`/`RpcGroup` values; discarding the return value is a no-op.
621
+ 12. **Renaming or re-prefixing rpcs after deployment.** `_tag` is the wire identity; old clients will send tags the server no longer knows. Treat tag changes like breaking schema changes.
622
+ 13. **Passing a union to `Schema.Union` variadically.** v4 takes an array: `Schema.Union([OrderError, RateLimited])`, not `Schema.Union(OrderError, RateLimited)`.
623
+ 14. **Declaring errors as plain `Schema.Struct`s.** Use `Schema.TaggedError` (or `Schema.Error` with a `Schema.tag` field) so errors are yieldable, `catchTag`-able, and carry a stable `_tag` on the wire. The former `Schema.TaggedErrorClass` / `Schema.ErrorClass` names were removed in beta.104.
624
+ 15. **Expecting stack traces in remote defects.** The default `Schema.Defect()` strips stacks; opt in per rpc with `defect: Schema.Defect({ includeStack: true })`.