Overview
Litestore's server answers calls through server actions, oRPC and REST endpoints, and each kind of caller uses exactly one of them.
Litestore's server answers calls through REST endpoints, oRPC and server actions, and each kind of caller uses exactly one of them. Read the sections below before you add an endpoint, so your code uses the transport its caller already speaks.
Request
Nothing in this repository calls oRPC to write admin data, and oRPC has no admin write procedures.
Transports
Each transport has its own place in the repository. Pick the tab for the caller you are serving.
Server actions sit in the module whose data they write. The admin screens and storefront components in this repository call them.
modules/<area>/admin/actions.ts
modules/<area>/storefront/actions.tsServer actions
Every admin and storefront write is a zsa action. Admin actions are built on the permission procedures in platform/permissions/procedures.ts, and permissionProcedure runs four checks before your handler does.
Session
The procedure reads the signed-in user's session.
Permission context
It works out which permissions the user holds.
Admin role check
If the user has no admin role, the request stops here.
Ban check
It checks once more whether the account is banned, with a fresh database read through assertAccountNotBanned.
The fourth check looks redundant, because the admin layout already stops a banned account from navigating. It is still needed. A server action is a direct POST that never renders the layout, so the procedure has to enforce the ban itself.
modulePermission(module)(action) then narrows the check to one <module>:<action> permission string:
const couponPermission = modulePermission(AdminModule.coupons)
export const deleteCoupons = couponPermission("delete")
.createServerAction()
.input(deleteCouponsSchema)
.handler(async ({ input, ctx }) => {
await releaseCouponsForDeletion(input.ids)
await couponCrud.delete(input.ids, { user: ctx.user })
})Storefront actions use the matching procedure, customerProcedure in platform/permissions/customer.ts. It finds the signed-in customer and creates the Customer row when a logged-in user doesn't have one yet. It then puts ctx.customerId and ctx.userId on the context.
Both sides are zsa procedures, so admin and storefront actions return the same [data, error] tuple. Your calling code can handle a failure the same way without checking which half of the app it called.
When a module extends BaseCRUD, Litestore generates three actions from its declaration. The module exports the ones it uses, and you write create, update and anything that does more than save data by hand in the same file.
deleteAction
restoreAction
permanentDeleteActionoRPC
The comment at the top of the router explains how narrow it is and why:
/**
* 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 `modules/<area>/{admin,storefront}/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,
}webRouter in platform/api/storefront-router.ts has one entry, webhooks (from modules/integrations/webhooks/router.ts), with six procedures. Each one is built with serviceProcedureWithScope from platform/api/orpc.ts, which authenticates a ServiceApiKey through requireServiceKey() and then checks the key's scope.
| Procedure | Scope it needs |
|---|---|
list | read |
get | read |
create | write:webhooks |
update | write:webhooks |
delete | write:webhooks |
rotateSecret | write:webhooks |
A key without the right scope gets an ORPCError with FORBIDDEN. The valid scopes are listed in modules/team/api-keys/scopes.ts:
read
read:cart
write:cart
write:orders
write:webhooks
admin:mcpThe list only contains scopes that some request path checks. A scope that no request path checks would let an operator grant a permission that nothing enforces.
platform/api/orpc.ts also defines baseProcedure, which adds db and a revalidate helper to the context. The file has no session-auth or permission procedures, so it can't serve admin writes.
The route handler wraps the router in an oRPC RPCHandler with the prefix /api/rpc. It logs failures through the api-rpc-route logger and answers GET, POST, PUT, PATCH and DELETE. When no procedure matches, it returns a 404.
REST endpoints
app/api/openapi.json/route.ts returns an OpenAPI 3.0.3 document that we write by hand. It takes its title from config.site.name, and Litestore serves it with Cache-Control: public, max-age=3600.
The document describes eight paths under five tags. Six of them need no authentication.
| Path | Tag | Who can call it |
|---|---|---|
GET /.well-known/ai-commerce | Discovery | Anyone |
GET /api/capabilities | Discovery | Admin session |
GET /api/products | Products | Anyone |
GET /api/products/feed | Products | Anyone |
GET /api/products/{id} | Products | Anyone |
GET /api/recommendations | Recommendations | Anyone |
POST /api/track | Tracking | Anyone |
POST /ai/v1/suggest-categories | AI | Admin session (no longer exists, see below) |
The only security scheme the document declares is sessionAuth, which is an apiKey sent in the session cookie.
/api/products accepts six query parameters:
| Parameter | What it does |
|---|---|
page | Picks the page of results. |
limit | Sets how many products come back. |
category | Filters by category. |
collection | Filters by collection. |
q | Searches by text. |
format | Chooses json or jsonld. |
/api/products/feed returns the whole catalog as a Schema.org ProductCollection, which Google Merchant Center and agents can read.
Excluded routes
We picked the spec's contents by hand for agents, and it doesn't list every route in the app. Litestore has 40 route handlers under app/api and two under app/ai/v1 (admin-chat and fill-product). The document still lists /ai/v1/suggest-categories, which no longer exists, and names neither of the two that do.
The spec leaves out the service-key REST endpoints. app/api/webhooks/route.ts authenticates through authenticateWebhookRequest(request, scope), yet neither the spec nor the oRPC router's scope comment mentions it.
It leaves out the MCP endpoints too. app/api/mcp/route.ts serves shoppers, and app/api/mcp/admin/route.ts serves the operator tool kit behind the admin:mcp scope.
The spec is maintained by hand
Nothing generates app/api/openapi.json from the route handlers or their zod schemas. The TrackEventRequest schema has the comment "Mirrors trackEventSchema in app/api/track/route.ts — keep in sync", and that comment is the only thing keeping them aligned. If a route changes shape or disappears, no check will fail.
Adding endpoints
Choose by who calls the endpoint, not by the shape of the data. Throughout the codebase, we follow the rule written in the router comment: one transport story per consumer.
Related
CRUD
BaseCRUD in platform/crud/ gives every admin module its write path, with archive versus permanent delete, optimistic concurrency, activity logging and cache invalidation.
Contracts
The shared Zod field validators in platform/field-schemas.ts, such as Email, OptionalPhone, Url and Slug, are the building blocks for a new module's schema.
MCP
MCP connects AI assistants to the store, with public shopper tools at /api/mcp and the admin's gated tool kit at /api/mcp/admin behind an admin:mcp key.