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.
Litestore ships three server transports and gives each consumer exactly one.
Admin screens and the storefront call zsa server actions. External service-key consumers call oRPC. Machine and agent clients call REST route handlers.
The split is deliberate rather than historical. Nothing in this repository calls oRPC to write admin data, and the oRPC admin write surface that once existed was deleted rather than left in place for a caller that might arrive later.
Where each transport lives
Server actions live beside the domain they write:
server/admin/<domain>/actions.ts
server/web/<domain>/actions.tsTheir callers are the admin screens and storefront components in this repository.
oRPC is one router, mounted by one route handler:
server/router.ts → app/api/rpc/[[...rest]]/route.tsIts callers are external integrations and AI agents holding a ServiceApiKey.
REST is the widest surface:
app/api/**/route.ts
app/ai/v1/**/route.tsIts callers are AI agents, feed consumers, webhook senders, and the browser's own fetch calls.
Server actions
Every admin and storefront write is a zsa action. Admin actions build on the
permission procedures in lib/permissions/procedures.ts.
permissionProcedure does four things before a handler body runs:
- Loads the session
- Resolves the permission context
- Rejects a request with no admin role
- Re-checks the ban state
The fourth step looks redundant, because the admin layout already blocks navigation for a banned account. It is not. A server action is a direct POST that never renders the layout, so the ban has to be enforced again at the procedure.
modulePermission(module)(action) then narrows the gate to a single
<module>:<action> permission string:
const tagPermission = modulePermission(AdminModule.tags)
export const bulkImportTags = tagPermission("create")
.createServerAction()
.input(z.object({ slugs: z.array(z.string().min(1)) }))
.handler(async ({ input }) => { /* … */ })Storefront actions use the mirror procedure, customerProcedure in
lib/permissions/customer.ts. It resolves the signed-in customer — creating the
Customer row when a logged-in user has none — and exposes ctx.customerId and
ctx.userId.
Because both sides are zsa procedures, admin and storefront actions return the
same [data, error] tuple. A caller does not have to know which half of the
application it is talking to in order to handle a failure.
Modules that extend BaseCRUD get six actions generated from their declaration:
createAction
updateAction
duplicateAction
deleteAction
restoreAction
permanentDeleteActionA module re-exports the ones it uses. Anything beyond plain persistence is written by hand in the same file.
oRPC at /api/rpc
The router's own header states the scope, and the reasoning behind it:
/**
* 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,
}webRouter in server/web/router.ts contains one entry, webhooks, with six procedures:
list
create
get
update
delete
rotateSecretEach is built with serviceProcedureWithScope from lib/orpc.ts. That procedure
authenticates a ServiceApiKey through requireServiceKey() and then checks the
scope — read for the two reads, write:webhooks for the four writes. A key
without the scope gets an ORPCError with FORBIDDEN.
The valid scopes are listed in lib/api-keys/constants.ts:
read
read:cart
write:cart
write:orders
write:webhooks
admin:mcpThat file's comment records the rule that produced the list. A scope no request path enforces was removed, because advertising a scope lets an operator grant power that is never checked — the grant looks like a boundary and is not one.
lib/orpc.ts also defines baseProcedure, which injects db and a revalidate
helper into context. The deleted admin surface is named in that file too: the
session-auth and permission procedures that once mirrored the server actions went
with it.
The route handler wraps the router in an oRPC RPCHandler with the prefix
/api/rpc, logs failures through the api-rpc-route logger, and answers GET,
POST, PUT, PATCH and DELETE. It returns a 404 when no procedure matches.
REST endpoints
app/api/openapi.json/route.ts returns a hand-written OpenAPI 3.0.3 document,
titled from config.site.name and served with Cache-Control: public, max-age=3600.
It documents eight paths:
GET /.well-known/ai-commerce
GET /api/capabilities
GET /api/products
GET /api/products/feed
GET /api/products/{id}
GET /api/recommendations
POST /api/track
POST /ai/v1/suggest-categoriesThose eight sit under five tags: Discovery for the first two, Products for the next three, then Recommendations, Tracking and AI.
Six of the eight are unauthenticated. /api/capabilities and
/ai/v1/suggest-categories require an admin session, and sessionAuth is the
only security scheme the document declares — an apiKey carried in the session
cookie.
/api/products takes the query parameters:
page
limit
category
collection
q
formatformat is json or jsonld. /api/products/feed returns the whole catalog as
a Schema.org ProductCollection for Google Merchant Center and agents.
What the spec leaves out
The spec is a curated agent contract, not an index of the app's routes.
Litestore ships more than forty route handlers under app/api and seven under
app/ai/v1, of which the document names one.
The service-key REST endpoints are among the omissions.
app/api/webhooks/route.ts authenticates through
authenticateWebhookRequest(request, scope) and appears in neither the spec nor
the oRPC router's scope description.
The MCP surfaces are also outside it:
app/api/mcp/route.tsfor shoppersapp/api/mcp/admin/route.tsfor the operator tool kit behind theadmin:mcpscope
The spec is maintained by hand
Nothing generates app/api/openapi.json from the route handlers or their zod
schemas. The TrackEventRequest schema carries the comment "Mirrors
trackEventSchema in app/api/track/route.ts — keep in sync", which is the whole
enforcement mechanism. A route that changes shape will not fail a check.
Which transport a new endpoint belongs to
Start from the caller, not from the shape of the data.
If the caller is an admin screen or a storefront component in this repository,
write a zsa server action next to its domain. It is the only transport that gets
the permission procedures and the [data, error] tuple for free.
If the caller is an outside integration holding a ServiceApiKey and it is
managing webhooks, it belongs on the oRPC router, behind the scope that already
exists for it.
If the caller is an agent, a feed consumer, a provider posting a webhook, or the browser fetching from a page, write a REST route handler. If that route is meant to be discoverable by agents, remember that the OpenAPI document is a literal someone has to edit — the route will work without being listed, and no check will tell you it is missing.
The rule the codebase is enforcing throughout is the one written in the router header:
One transport story per consumer.
Related
CRUD
Learn how BaseCRUD in lib/crud/ gives every admin module its write path — hooks, archive versus permanent delete, optimistic concurrency, activity logging and getRelatedCacheTags invalidation.
Contracts
Learn what the shared Zod schemas in contracts/ validate — Email, Phone, Url, Slug, DateRange, PaginationInput — and how domain schemas compose them.
MCP
Learn how to connect AI assistants to your store over MCP — eight public shopper tools at /api/mcp, and the admin's gated tool kit at /api/mcp/admin behind an admin:mcp key.
Caching
Learn how Litestore tags cached reads from CACHE_TAGS, how the CRUD layer invalidates them, and which paths are never cached.
Actions
Learn how admin and storefront writes are declared as zsa server actions, how permission procedures gate them, and how the CRUD layer generates the standard six.