# Defining RPCs

Every RPC starts as a **definition**: a tag that names the procedure plus the
schemas that describe the values crossing the wire. `Rpc.make` produces one such
definition, and `RpcGroup.make` collects definitions into a group that clients
and servers interpret. Because the definition is plain data, you can share it
between packages — the backend imports it to implement handlers, the frontend
imports the very same group to call them.

```ts
import { Schema } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

// Typed, schema-encoded errors. Modeling failures as data (rather than thrown
// exceptions) means they survive the trip across the wire and show up in the
// client's error channel.
class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()(
  "UserNotFound",
  { id: Schema.String }
) {}

class EmailTaken extends Schema.TaggedErrorClass<EmailTaken>()("EmailTaken", {
  email: Schema.String
}) {}

// A reusable success shape for a user record.
const User = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  email: Schema.String
})

// `RpcGroup.make` collects procedures under their tags. Extending a class gives
// the group a stable identity you can import and reference everywhere.
export class UserRpcs extends RpcGroup.make(
  // A request/response procedure: payload in, `User` out, `UserNotFound` on
  // failure.
  Rpc.make("GetUser", {
    payload: { id: Schema.String },
    success: User,
    error: UserNotFound
  }),

  // Multiple fields in the payload become a `Schema.Struct` automatically.
  // This RPC can fail in two different ways; both are tracked in the type.
  Rpc.make("CreateUser", {
    payload: { name: Schema.String, email: Schema.String },
    success: User,
    error: Schema.Union([EmailTaken, UserNotFound])
  }),

  // A procedure with no payload and no result. `success` defaults to
  // `Schema.Void` and `error` defaults to `Schema.Never`, so this one can't
  // fail in the RPC error channel.
  Rpc.make("Ping")
) {}
```

## How `Rpc.make` reads its options

`Rpc.make(tag, options)` takes a string tag and an optional object. Each field
has a sensible default, so you only specify what a given procedure needs:

- **`payload`** — what the client sends. Pass plain **struct fields**
  (`{ id: Schema.String }`) and they are wrapped in a `Schema.Struct` for you,
  or pass a full schema directly. Defaults to `Schema.Void` (no payload).
- **`success`** — the value the handler returns on success. Defaults to
  `Schema.Void`.
- **`error`** — the typed failure schema, surfaced in the client's error
  channel. Defaults to `Schema.Never` (the call cannot fail with a domain
  error). Use `Schema.Union([...])` of tagged errors for several failure modes.
- **`defect`** — the schema used for unexpected failures (bugs, thrown
  exceptions). Defaults to `Schema.Defect`. A custom defect schema must be a
  `Rpc.DefectSchema`: it must decode and encode **without services**
  (`DecodingServices` and `EncodingServices` are `never`).
- **`stream`** — set `true` to make the success a stream of values rather than a
  single value (see below).
- **`primaryKey`** — only valid when `payload` is struct fields. Provide a
  function `(payload) => string` and the payload becomes a `Schema.Class` with a
  derived `PrimaryKey`, enabling request deduplication and batching in runtimes
  that key requests (for example Effect Cluster).
**Note:** Domain errors declared in `error` are **expected** failures that the client
handles. Unexpected defects — bugs, thrown exceptions — travel separately on the
defect channel and default to `Schema.Defect`, so you rarely configure them.

### Payloads with a primary key

When several callers issue the same logical request, a primary key lets the
runtime collapse them into one. `primaryKey` turns the struct-field payload into
a payload class whose `PrimaryKey` is derived from the value.

```ts
import { Schema } from "effect"
import { Rpc } from "effect/unstable/rpc"

// `primaryKey` is only allowed when `payload` is struct fields. The resulting
// payload is a Schema.Class with a PrimaryKey of `user:<id>`, so two concurrent
// `GetUser({ id: "1" })` calls can be deduplicated.
const GetUser = Rpc.make("GetUser", {
  payload: { id: Schema.String },
  success: Schema.Struct({ id: Schema.String, name: Schema.String }),
  primaryKey: (payload) => `user:${payload.id}`
})
```

## Streaming RPCs

Set `stream: true` and the `success` and `error` schemas describe the *elements*
of a stream rather than a single response. The client receives a
[`Stream`](https://effect.plants.sh/streaming/), and the server handler returns one. This is ideal for
log tailing, progress events, or any result that arrives incrementally.

```ts
import { Schema } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

class StreamClosed extends Schema.TaggedErrorClass<StreamClosed>()(
  "StreamClosed",
  {}
) {}

export class EventRpcs extends RpcGroup.make(
  // `success` is the type of each emitted element; `error` is the type a failing
  // stream terminates with. The client will see a `Stream<string, StreamClosed>`.
  Rpc.make("Subscribe", {
    payload: { channel: Schema.String },
    success: Schema.String,
    error: StreamClosed,
    stream: true
  })
) {}
```

Under the hood `stream: true` rewrites the schemas: the element and stream-error
schemas are stored inside an `RpcSchema.Stream<success, error>` marker on the
success schema, and the RPC's ordinary `error` schema becomes `Schema.Never`.
**Caution:** **Streaming exit semantics.** A streaming RPC still has a normal RPC *exit*, but
its immediate exit success is `void` — the chunks and stream errors travel
separately on the chunk stream, not inside the exit. So the success that
resolves the call is `void`; the actual `string` values arrive as stream
elements. The exit's error channel still carries both the stream error and any
RPC/middleware errors.

## Composing and refining groups

Groups are immutable; every transformation returns a new group whose type tracks
the change. This lets you split a large protocol into feature groups and merge
them, or apply cross-cutting changes to the procedures defined so far.

```ts
import { Schema } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

class Account extends RpcGroup.make(
  Rpc.make("GetBalance", { success: Schema.Number })
) {}

class Billing extends RpcGroup.make(
  Rpc.make("Charge", { payload: { cents: Schema.Number } })
) {}

// `merge` combines protocols; the resulting group exposes every procedure from
// both, fully typed.
export class Api extends Account.merge(Billing) {}

// You can also refine a single definition before adding it. The instance methods
// return a new, more specific `Rpc`.
const GetBalance = Rpc.make("GetBalance")
  .setSuccess(Schema.Number) // narrow the success schema
  .prefix("account.") // tag becomes "account.GetBalance"
```

Other group methods follow the same pattern: `add` appends procedures, `omit`
removes them by tag, `prefix` namespaces every tag, and `middleware` /
`annotateRpcs` attach behavior or metadata to the procedures added so far.
Composition order matters — `middleware` and `annotateRpcs` affect only the RPCs
already in the group, and a duplicate tag from `add` or `merge` replaces the
earlier definition.

## What the group does *not* do

A group records the protocol; it does not provide an implementation or a
transport. The schemas still carry whatever services they need to encode and
decode, but grouping alone never satisfies those requirements. To actually run
the procedures you implement handlers and pick a transport, which is the subject
of [Client and server](https://effect.plants.sh/rpc/client-and-server/).

---

## Reference

The common case is `Rpc.make` + `RpcGroup.make`. This section enumerates the
rest of the surface: the refinement methods on an `Rpc`, the `Rpc.custom`
constructor factory, every `RpcGroup` method, and the helper types you reach for
when writing client and handler code.

### Refining a definition

Each instance method on an `Rpc` returns a **new** definition with one piece
changed; the original is untouched. Use them to build a definition up
incrementally or to adjust a definition you imported.

#### `setPayload`

Replaces the payload schema. Accepts either struct fields (wrapped into a
`Schema.Struct` for you) or a full schema.

```ts
import { Schema } from "effect"
import { Rpc } from "effect/unstable/rpc"

const GetUser = Rpc.make("GetUser").setPayload({ id: Schema.String })
// => payload schema is now Schema.Struct({ id: Schema.String })
```

#### `setSuccess`

Replaces the success schema.

```ts
import { Schema } from "effect"
import { Rpc } from "effect/unstable/rpc"

const GetCount = Rpc.make("GetCount").setSuccess(Schema.Number)
// => success schema is now Schema.Number (was Schema.Void)
```

#### `setError`

Replaces the error schema. Use `Schema.Union([...])` for several failure modes.

```ts
import { Schema } from "effect"
import { Rpc } from "effect/unstable/rpc"

class NotFound extends Schema.TaggedErrorClass<NotFound>()("NotFound", {}) {}

const GetUser = Rpc.make("GetUser").setError(NotFound)
// => error schema is now NotFound (was Schema.Never)
```

#### `middleware`

Attaches an `RpcMiddleware` service to the procedure. Middleware can add to the
error channel and require services; both are tracked in the type. See
[Client and server](https://effect.plants.sh/rpc/client-and-server/) for defining and applying
middleware.

```ts
import { Rpc } from "effect/unstable/rpc"
// declare const Authenticated: RpcMiddleware service tag

const GetUser = Rpc.make("GetUser")
// .middleware(Authenticated) // procedure now requires the middleware
```

#### `prefix`

Prepends a string to the tag: `Tag` becomes `` `${Prefix}${Tag}` ``.

```ts
import { Rpc } from "effect/unstable/rpc"

const GetUser = Rpc.make("GetUser").prefix("users.")
// => GetUser._tag is "users.GetUser"
```

#### `annotate`

Attaches a single annotation (a `Context.Key` and its value) to the procedure.
Annotations are metadata read by higher-level runtimes; they do not affect the
wire.

```ts
import { Context } from "effect"
import { Rpc } from "effect/unstable/rpc"

const Audit = Context.Reference<boolean>("Audit", { defaultValue: () => false })

const DeleteUser = Rpc.make("DeleteUser").annotate(Audit, true)
// => DeleteUser carries the Audit=true annotation
```

#### `annotateMerge`

Merges a whole `Context` of annotations onto the procedure at once.

```ts
import { Context } from "effect"
import { Rpc } from "effect/unstable/rpc"

const Audit = Context.Reference<boolean>("Audit", { defaultValue: () => false })

const annotations = Context.make(Audit, true)
const DeleteUser = Rpc.make("DeleteUser").annotateMerge(annotations)
// => DeleteUser carries every annotation from `annotations`
```

### Custom RPC constructors (`Rpc.custom`)

`Rpc.custom` builds a **reusable constructor** that transforms the success and
error schemas before the RPC is created. This lets you encode a cross-cutting
shape — pagination, envelope wrappers, result metadata — once and apply it like
`Rpc.make`. The constructor function receives the original `{ success, error,
defect }` schemas and returns the transformed schemas through `out`.

```ts
import { Schema } from "effect"
import { Rpc } from "effect/unstable/rpc"

// Create a custom Rpc wrapper definition by transforming the success and error
// schemas. The `out` type describes how the constructor reshapes them.
export interface RpcWithPagination extends Rpc.Custom {
  readonly out: Rpc.Custom.Out<
    Paginated<this["success"]>,
    this["error"]
  >
}

// The type definition for the transformed success schema.
export interface Paginated<S extends Schema.Top> extends
  Schema.Struct<{
    readonly offset: Schema.Number
    readonly total: Schema.Number
    readonly results: Schema.$Array<S>
  }>
{}

// Implement the schema transformation with `Rpc.custom`. The value-level
// transform must match the type-level `out`.
export const makePaginated = Rpc.custom<RpcWithPagination>((schemas) => ({
  ...schemas,
  success: Schema.Struct({
    offset: Schema.Number,
    total: Schema.Number,
    results: Schema.Array(schemas.success)
  })
}))

// Use the custom constructor exactly like `Rpc.make`. The declared `success`
// (here `Schema.String`) is the *element* type; the actual success schema
// becomes the paginated wrapper around it.
export const listAllRpc = makePaginated("listAll", {
  success: Schema.String
})
// => listAllRpc success: { offset: number; total: number; results: string[] }
```

The type-level contract:

- **`Rpc.Custom`** — the interface a custom definition extends. It exposes the
  incoming `success`, `error`, and `defect` schemas and the produced `out`.
- **`Rpc.Custom.Out<Success, Error>`** — the transformed `{ success, error,
  defect }` shape your `out` returns.
- **`Rpc.Custom.OutDefault`** — `Out<Schema.Top, Schema.Top>`; the erased
  default output shape and the argument type of the transform function.
- **`Rpc.Custom.Kind<Def, Success, Error>`** — applies a custom definition to
  concrete `Success`/`Error` schemas to compute the resulting `out`. This is how
  `Rpc.custom` derives the final success/error schemas at each call site.

### Composing groups (`RpcGroup`)

`RpcGroup.make(...rpcs)` is the entry point. Every other method returns a new,
immutable group whose type reflects the change.

#### `RpcGroup.make`

Creates a group from one or more RPC definitions, keyed by their tags.

```ts
import { Schema } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const Group = RpcGroup.make(
  Rpc.make("Ping"),
  Rpc.make("Echo", { payload: { msg: Schema.String }, success: Schema.String })
)
// => RpcGroup with tags "Ping" and "Echo"
```

#### `add`

Appends one or more procedures, returning a wider group type.

```ts
import { Schema } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const Base = RpcGroup.make(Rpc.make("Ping"))
const Group = Base.add(Rpc.make("Pong", { success: Schema.String }))
// => tags "Ping" | "Pong"
```

#### `merge`

Combines this group with one or more other groups. Annotations from the merged
groups are carried over.

```ts
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const A = RpcGroup.make(Rpc.make("A"))
const B = RpcGroup.make(Rpc.make("B"))
const AB = A.merge(B)
// => tags "A" | "B"
```

#### `omit`

Removes procedures by tag. The resulting type narrows via `Exclude`, so the
removed tags are no longer callable.

```ts
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const Group = RpcGroup.make(
  Rpc.make("Public"),
  Rpc.make("Internal")
)

const PublicOnly = Group.omit("Internal")
// => tags narrowed from "Public" | "Internal" to just "Public"
```

#### `prefix`

Adds a string prefix to every procedure tag in the group.

```ts
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const Group = RpcGroup.make(Rpc.make("Get"), Rpc.make("Set")).prefix("kv.")
// => tags "kv.Get" | "kv.Set"
```

#### `middleware`

Attaches middleware to **all procedures added so far**. Because it only touches
the current members, call it after the procedures it should cover.

```ts
import { Rpc, RpcGroup } from "effect/unstable/rpc"
// declare const Authenticated: RpcMiddleware service tag

const Group = RpcGroup.make(Rpc.make("GetUser"))
// .middleware(Authenticated) // GetUser now requires the middleware
```

#### `annotate`

Annotates the **group itself** with a single `Context.Key`/value pair. Useful
for runtime-level metadata (for example, the entity name for a cluster).

```ts
import { Context } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const ServiceName = Context.Reference<string>("ServiceName", {
  defaultValue: () => "unknown"
})

const Group = RpcGroup.make(Rpc.make("Ping")).annotate(ServiceName, "users")
// => the group carries ServiceName="users"
```

#### `annotateMerge`

Merges a whole `Context` of annotations onto the group.

```ts
import { Context } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const ServiceName = Context.Reference<string>("ServiceName", {
  defaultValue: () => "unknown"
})

const Group = RpcGroup.make(Rpc.make("Ping")).annotateMerge(
  Context.make(ServiceName, "users")
)
// => the group carries every annotation in the provided Context
```

#### `annotateRpcs`

Annotates **each procedure added so far** (not the group) with a single
annotation. Like `middleware`, it only affects current members.

```ts
import { Context } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const Tier = Context.Reference<string>("Tier", { defaultValue: () => "free" })

const Group = RpcGroup.make(Rpc.make("Ping"), Rpc.make("Pong")).annotateRpcs(
  Tier,
  "premium"
)
// => both Ping and Pong carry Tier="premium"
```

#### `annotateRpcsMerge`

Merges a `Context` of annotations onto each procedure added so far.

```ts
import { Context } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const Tier = Context.Reference<string>("Tier", { defaultValue: () => "free" })

const Group = RpcGroup.make(Rpc.make("Ping")).annotateRpcsMerge(
  Context.make(Tier, "premium")
)
// => Ping carries every annotation in the provided Context
```
**Caution:** Composition order matters. `middleware`, `annotateRpcs`, and `annotateRpcsMerge`
apply only to the RPCs **already** in the group at the point you call them — add
the affected procedures first. A duplicate tag introduced by `add` or `merge`
**replaces** the earlier definition with the same tag.

### Useful `Rpc` helper types

These type-level helpers extract the derived TypeScript shapes from an `Rpc`.
You reach for them when writing client calls or handler implementations by hand.

```ts
import type { Rpc } from "effect/unstable/rpc"
import { Rpc as R } from "effect/unstable/rpc"
import { Schema } from "effect"

const GetUser = R.make("GetUser", {
  payload: { id: Schema.String },
  success: Schema.Struct({ id: Schema.String, name: Schema.String })
})
type GetUser = typeof GetUser

type P = Rpc.Payload<GetUser> // => { readonly id: string }
type S = Rpc.Success<GetUser> // => { readonly id: string; readonly name: string }
type E = Rpc.Error<GetUser> // => never (no error schema set)
type X = Rpc.Exit<GetUser> // => Exit<{ id: string; name: string }, never>
type T = Rpc.Tag<GetUser> // => "GetUser"
```

- **`Rpc.Payload<R>`** — the decoded payload type the handler receives.
- **`Rpc.Success<R>`** — the decoded success value type.
- **`Rpc.Error<R>`** — the decoded error type, including middleware errors.
- **`Rpc.Exit<R>`** — the `Exit` produced for the RPC, using its exit success
  and exit error types (for streams, exit success is `void`).
- **`Rpc.Tag<R>`** — the procedure's tag string.
- **`Rpc.ToHandler<R>`** — the `Rpc.Handler` type the RPC maps to (server side).
- **`Rpc.ToHandlerFn<Current, R>`** — the function signature you implement for a
  handler: `(payload, options) => result`, where `result` matches the RPC's
  success/error shape.

For groups, two helpers describe handler implementations:

- **`RpcGroup.Rpcs<Group>`** — the union of `Rpc` definitions in a group.
- **`RpcGroup.HandlersFrom<R>`** — the object type mapping each tag to its
  handler function (the shape `toLayer`/`toHandlers` expect).
- **`RpcGroup.HandlerFrom<R, Tag>`** — the single handler function type for one
  tag.

```ts
import type { RpcGroup } from "effect/unstable/rpc"
import { Rpc, RpcGroup as G } from "effect/unstable/rpc"
import { Schema } from "effect"

const Group = G.make(
  Rpc.make("GetCount", { success: Schema.Number })
)
type Group = typeof Group

type AllRpcs = RpcGroup.Rpcs<Group> // => the "GetCount" Rpc
type Handlers = RpcGroup.HandlersFrom<RpcGroup.Rpcs<Group>>
// => { readonly GetCount: (payload, options) => Effect<number, ...> | ... }
```

### `RpcSchema` helpers

The streaming marker and its guard live in `RpcSchema`:

- **`RpcSchema.Stream(success, error)`** — builds the stream schema marker that
  `Rpc.make(..., { stream: true })` installs for you. It stores the element
  schema as `success` and the stream-error schema as `error`.
- **`RpcSchema.isStreamSchema(schema)`** — returns `true` when a schema is a
  stream marker; protocol code branches on it to handle one-shot vs streaming
  responses.

```ts
import { Schema } from "effect"
import { RpcSchema } from "effect/unstable/rpc"

const elements = RpcSchema.Stream(Schema.String, Schema.Never)
RpcSchema.isStreamSchema(elements) // => true
RpcSchema.isStreamSchema(Schema.String) // => false
```