Litestore · Developer guide
API

oRPC

This page helps you call the oRPC endpoint at /api/rpc with a service key to manage webhooks.

Litestore mounts oRPC at /api/rpc, and outside services call it with a service key. Use it when your integration needs to manage webhooks or check that the store is up.

We kept it small on purpose. It offers a router for managing webhooks and a health ping. Admin and storefront writes don't go through oRPC, and we don't plan to add them.

Each kind of caller has one way in. An integration with a service key uses oRPC. The admin and the storefront use zsa server actions. The published contract in app/api/openapi.json describes the REST endpoints. When the same operation exists in two transports, the two versions can drift apart.

The comment at the top of the router file states this boundary:

server/router.ts
/**
 * Scope is deliberately narrow: oRPC is the EXTERNAL service-key API surface
 * (AI agents / integrations managing webhooks), plus a health ping. Admin and
 * storefront writes are zsa server actions in `server/{admin,web}/<domain>/actions.ts`
 * — the one transport story per consumer. A parallel oRPC admin write surface
 * was deleted: it had no client in this repo and is not part of the published
 * OpenAPI contract (app/api/openapi.json describes the REST endpoints instead).
 */

export const appRouter = {
  ping,
  web: webRouter,
}

What is mounted

webRouter has exactly one router today.

ProcedureScope requiredWhat it does
web.webhooks.listreadLists webhooks with id, url, events, isActive, name and createdAt
web.webhooks.getreadReturns one webhook by id, or null
web.webhooks.createwrite:webhooksCreates a webhook and returns its secret once
web.webhooks.updatewrite:webhooksUpdates the url, events and name
web.webhooks.deletewrite:webhooksRemoves the webhook
web.webhooks.rotateSecretwrite:webhooksIssues a new secret and returns it once
pingnoneReturns { status: "ok", timestamp }

Create and rotate return the signing secret in the response body and say so clearly, because you can't read it back later.

Build your client around this. If your code throws away the create response, it loses the secret for good and has to rotate to get a new one.

How a procedure is built

lib/orpc.ts stacks three procedures, and each one builds on the one before:

  1. baseProcedure adds db and a revalidate helper to the context.
  2. serviceProcedure resolves the service key and turns an AppError into an ORPCError.
  3. serviceProcedureWithScope adds the scope check.

The third layer does nothing except check the scope:

lib/orpc.ts
export function serviceProcedureWithScope(scope: ApiScope) {
  return serviceProcedure.use(async ({ next, context }) => {
    if (!hasScope(context.apiKey.scopes, scope)) {
      throw new ORPCError("FORBIDDEN", {
        message: `Requires ${scope} scope`,
      })
    }
    return next()
  })
}

Because the layers stack, a procedure can't get a database handle until the key has been resolved. Every procedure is authenticated, and none can opt out.

There is no admin path through this transport

We deleted the session-auth and permission procedures that once copied the admin actions, together with the unused admin router.

You therefore can't reach an admin write through oRPC. This doesn't depend on convention or code review. The procedures that would accept a session don't exist here.

The Webhook entity is admin-owned

The oRPC router imports the webhook lifecycle from server/admin/webhooks/lifecycle. It has a discipline-ok(no-admin-in-storefront) annotation that explains the import: this is the API-key service surface over the same admin-owned entity, not storefront code. Without the annotation, the discipline check would reject the import.

Which transport to use

Use oRPC when an outside integration with a service key needs to manage webhooks or run a health check. Those are the only jobs it covers.

Use a zsa server action for an admin or storefront write. Those writes belong there, and an oRPC copy would bring back the parallel surface we deleted.

Use the REST endpoints when the caller needs the published contract described in app/api/openapi.json.

Before you add a procedure here, don't ask whether it would be convenient over RPC. Ask this instead:

Does this operation belong to a service key, and does the published contract not already cover it?

On this page