# oRPC conventions

Share Zod contracts and typed clients while keeping authorization on the server.

## Procedure selection [#procedure-selection]

| Procedure | Use |
| --- | --- |
| publicProcedure | Public boot or health data, with captured-scope checks when supplied |
| protectedProcedure | An authenticated User's personal data |
| orgProcedure | Shared data in the membership-checked Active Organization |
| orgProcedure.use(requireOrgRole(...)) | The same, limited to Members with one of the listed Org roles; available, not applied to any procedure yet |
| adminProcedure | Global reads requiring the current platform admin role |
| adminWriteProcedure | Admin writes through Better Auth's audited capability |

Each feature lives in `packages/api/src/features/<feature>/`: the leaf contract (`<feature>.contract.ts`), the procedures (`<feature>.procedures.ts`), the service and its errors. Register the procedures in `platform/rpc/app-router.ts`. Browser and mobile clients use the type-only `api/client` export and the feature's contract subpath (`api/<feature>/contract`). The [backend conventions](/docs/development/backend-conventions) give the full shape.

## Add an operation [#add-an-operation]

1. Define input and output schemas, including bounded pagination and text limits.
2. Choose the procedure according to the resource owner.
3. Apply Organization scope in every relevant query and mutation.
4. Keep the mutation and Audit Event in one transaction when the operation is audited.
5. Add any cross-package schema export explicitly, then consume it through a hook.

## Pagination [#pagination]

Each list endpoint uses one strategy. Choose it by what the interface needs:

| Use | When | Shape | Examples |
| --- | --- | --- | --- |
| Offset | The interface shows a total, page numbers or facet counts over a bounded, slowly changing set | `page` from 1 and `pageSize` capped at 100; the output carries `total` and `pageCount` | `todo.list`, the admin audit table and user list |
| Cursor | The list grows without bound or changes while it is read (feeds, timelines, "load more") | an opaque cursor of the stable sort key, such as `{ createdAt, id }`, plus `limit` capped at 100; the output carries the next cursor | Activity Timeline, integration delivery activity, Document Text pages |
| Bounded window | The server caps the result by design | a plain array or a fixed window, no paging | Board lanes (`limitPerLane`), the Calendar date window, document search |

Rules for every list:

- Every paginated query has a total `ORDER BY` with a unique tie-breaker (`id`), so equal sort values cannot repeat or skip rows.
- Every limit has a maximum in the contract. No endpoint returns an unbounded page.
- A cursor is opaque to clients. A malformed cursor is `INPUT_VALIDATION` (HTTP 400), never a 500.
- One endpoint offers one strategy; it does not accept both `page` and `cursor`.

The HTTP endpoint is `/api/rpc`. The session-authenticated OpenAPI reference is at `/api/rpc/api-reference`, with its specification at `/api/rpc/api-reference/spec.json`.

## Client state [#client-state]

TanStack Query owns server data, query keys and invalidation. Forms use TanStack Form. URL state expresses Todo views, filters and sorting. Components render the hook's state; they do not recreate authentication or authorization decisions.

Validate external payloads at the boundary. Use the shared error vocabulary and translated resolver rather than returning internal exceptions. Do not create a parallel REST API for a domain operation already represented by oRPC.


## Key files [#key-files]

- [`packages/api/src/platform/rpc/procedures.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/platform/rpc/procedures.ts)
- [`packages/api/src/platform/rpc/context.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/platform/rpc/context.ts)
- [`packages/api/src/features/todo/todo.procedures.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/features/todo/todo.procedures.ts)
- [`packages/api/src/features/todo/todo.service.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/features/todo/todo.service.ts)
- [`packages/api/src/features/todo/todo.contract.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/features/todo/todo.contract.ts)
- [`apps/web/src/platform/rpc/orpc.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/platform/rpc/orpc.ts)

