# oRPC

> A middleware that adds the caller's repositories to the oRPC context.

Source: https://bettersupabase.com/docs/frameworks/orpc

```ts title="src/router.ts"
import { os } from "@orpc/server";
import { createOrpc, type OrpcRequestContext } from "better-supabase/orpc";
import * as v from "valibot";
import { betterSupabase } from "./lib/supabase";

export const bs = createOrpc(betterSupabase);

const base = os.$context<OrpcRequestContext>();
const authed = base.use(bs.middleware());
const open = base.use(bs.middleware({ allow: ["user", "anon"] }));

export const router = {
  customers: {
    list: authed.handler(({ context }) =>
      bs.unwrap(context.db.customers.findMany({ select: ["id", "name"] })),
    ),
    rename: authed
      .input(
        v.object({ id: v.string(), name: v.pipe(v.string(), v.minLength(2)) }),
      )
      .handler(({ input, context }) =>
        bs.unwrap(context.db.customers.update(input.id, { name: input.name })),
      ),
  },
  health: open.handler(() => ({ ok: true })),
};
```

Serve the router with `bs.fetchHandler`. It passes the request as the
initial context, answers 404 when no procedure matches, and adds the
request's cookies to the response: refreshed session cookies with
`middleware({ refresh: true })`, and `bs-primary-until` after a write when
[read replicas](/docs/guides/read-replicas) are configured.

```ts
import { RPCHandler } from "@orpc/server/fetch";

export default {
  fetch: bs.fetchHandler(new RPCHandler(router), { prefix: "/rpc" }),
};
```

Calling `handler.handle(request, { context: { request } })` yourself also
works, but then those cookies never reach the client.

On runtimes that stop the invocation after the response, pass `waitUntil` to
`createOrpc`; `fetchHandler` hands it the
[event sink sends](/docs/standards/events#sends-after-the-response) a
procedure started.

## Entry arrays with `toOrpc` [#entry-arrays-with-toorpc]

`toOrpc(entries, handler, { prefix })` runs an `@supabase/middleware` entry
array around an oRPC fetch handler. The procedures' initial context is
`{ request }` plus every contributed key, and the response passes back
through the entries, so refreshed cookies reach the client without
`bs.middleware()`:

```ts
import { RPCHandler } from "@orpc/server/fetch";
import { toOrpc } from "better-supabase/orpc";
import { createServer, withBetterSupabase } from "better-supabase/server";

const bs = createServer(betterSupabase);
// The router's procedures read context.db.
const base = os.$context<{
  request: Request;
  db: ReturnType<typeof bs.admin>;
}>();

export default {
  fetch: toOrpc([withBetterSupabase(bs)], new RPCHandler(router), {
    prefix: "/rpc",
  }),
};
```

A refused caller gets the guard's 401 Problem Details before oRPC runs. Use
`bs.unwrap` from `createOrpc` to turn a `Result` into an `ORPCError`.

## Contract-first routers [#contract-first-routers]

With a contract from `@orpc/contract`, apply the same middleware to the
implementer. The client package imports only the contract, and the server
serves it over HTTP with `OpenAPIHandler`. This example mounts it on Hono:

```ts title="src/contract.ts"
import { oc } from "@orpc/contract";
import { openapi } from "@orpc/openapi";
import * as v from "valibot";

const customer = v.object({ id: v.string(), name: v.string() });

export const contract = {
  customers: {
    get: oc
      .meta(openapi({ method: "GET", path: "/customers/{id}" }))
      .input(v.object({ id: v.pipe(v.string(), v.uuid()) }))
      .output(customer),
  },
};
```

```ts title="src/server.ts"
import { OpenAPIHandler } from "@orpc/openapi/fetch";
import { implement } from "@orpc/server";
import type { OrpcRequestContext } from "better-supabase/orpc";
import { Hono } from "hono";

import { contract } from "./contract";

const os = implement(contract)
  .$context<OrpcRequestContext>()
  .use(bs.middleware());

const router = os.router({
  customers: {
    get: os.customers.get.handler(({ context, input }) =>
      bs.unwrap(
        context.db.customers.findById(input.id, { select: ["id", "name"] }),
      ),
    ),
  },
});

const handle = bs.fetchHandler(new OpenAPIHandler(router), { prefix: "/api" });

export const app = new Hono().all("/api/*", (c) => handle(c.req.raw));
```

`OpenAPIHandler` sends the error's status code, so a missing row answers
404 and a write that RLS rejects answers 403, each with the Problem Details
in the body's `data`. The `.meta(openapi(...))` form needs no import for its
side effects; `.route(...)` works too after `import "@orpc/openapi/extensions/route"`.
The [oRPC example](https://github.com/ScaleDockHQ/better-supabase/tree/main/apps/examples/orpc-api)
has both styles.

## Context [#context]

`bs.middleware()` resolves the caller and rejects anyone not in `allow`
(default `['user']`). It adds:

* `context.db`: repositories bound to the caller, so RLS applies.
* `context.auth`: the resolved `AuthState`.
* `context.bs`: the full server context.

With [`.claims(schema)`](/docs/auth#typed-claims) on the definition,
`context.auth` and `context.bs` are typed by the schema's output:

```ts
export const role = authed.handler(({ context }) =>
  context.auth.kind === "user" ? context.auth.claims.user_role : null,
);
```

## Permissions [#permissions]

`createOrpc` takes the server's `authorizer` option. `bs.middleware()` and
`bs.authed()` take the guard options `allow`, `aal` and `scopes`, and the
checks `requireTenant`, `roles`, `permission` and `authorize`. A
`permission` is decided by that authorizer, with a resource of
`{ type: "route" }`:

```ts title="src/router.ts"
import { createOrpc } from "better-supabase/orpc";
import * as v from "valibot";
import { authorizer } from "./lib/authorizer";
import { betterSupabase } from "./lib/supabase";

export const bs = createOrpc(betterSupabase, { authorizer });

export const remove = bs
  .authed({ requireTenant: true, permission: "customers.delete" })
  .input(v.object({ id: v.string() }))
  .handler(({ input, context }) =>
    bs.unwrap(context.db.customers.delete(input.id)),
  );
```

A refused caller gets an `ORPCError` with code `FORBIDDEN` and the Problem
Details in `data`: `NO_TENANT`, `MISSING_ROLE`, `PERMISSION_DENIED`,
`APPROVAL_REQUIRED` or `NOT_AUTHORIZED`. Without an authorizer, a procedure
with a `permission` refuses every caller. See
[Authorizers](/docs/extending/authorizers) for the contract and
`defineAuthorizer`.

## Errors [#errors]

`bs.unwrap(result)` returns the data, or throws an `ORPCError` when the
`Result` failed. It accepts a `Result`, an `AsyncResult` or a promise of
either, and the procedure's output type is the data type. The middleware also
converts thrown `DbException`s.

The error code comes from the error's HTTP status. For example `not_found`
becomes `NOT_FOUND`, `conflict` becomes `CONFLICT`, `validation` becomes
`UNPROCESSABLE_CONTENT` and `stale` becomes `PRECONDITION_FAILED`. The
error's `data` is the RFC 9457 Problem Details, so clients get the same
`kind`, `code`, `constraint` and `issues` fields as the HTTP adapters send.

```ts
import { safe } from "@orpc/client";

const [error, data] = await safe(client.customers.rename({ id, name }));
if (error?.data?.kind === "conflict") showTaken(error.data.constraint);
```