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:
/**
* 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.
| Procedure | Scope required | What it does |
|---|---|---|
web.webhooks.list | read | Lists webhooks with id, url, events, isActive, name and createdAt |
web.webhooks.get | read | Returns one webhook by id, or null |
web.webhooks.create | write:webhooks | Creates a webhook and returns its secret once |
web.webhooks.update | write:webhooks | Updates the url, events and name |
web.webhooks.delete | write:webhooks | Removes the webhook |
web.webhooks.rotateSecret | write:webhooks | Issues a new secret and returns it once |
ping | none | Returns { 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:
baseProcedureaddsdband arevalidatehelper to the context.serviceProcedureresolves the service key and turns anAppErrorinto anORPCError.serviceProcedureWithScopeadds the scope check.
The third layer does nothing except check the scope:
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?
Related
Overview
Learn which of Litestore's three transports to use — zsa server actions for admin and storefront writes, oRPC at /api/rpc for service-key consumers, and the REST endpoints described by /api/openapi.json.
Keys
Learn how service API keys are minted, hashed and revoked, and what each of the six enforced scopes actually permits.
Webhooks
Learn how outbound deliveries are signed and retried, when an endpoint is auto-disabled, and how the inbound Stripe and Resend routes differ.