Overview
This page helps you pick the right way to call Litestore's server for the code you are writing.
Litestore's server can be called in three ways, and each kind of caller uses exactly one of them. Read this page before you add an endpoint, so your code lands on the transport its caller already uses.
The admin screens and the storefront call zsa server actions. Outside services that hold a service key call oRPC. Machines and AI agents call REST route handlers.
We chose this split on purpose. Nothing in this repository calls oRPC to write admin data. An oRPC admin write surface once existed, and we deleted it instead of keeping it for a caller that might arrive later.
Request
Where each transport lives
Server actions live next to the domain they write:
server/admin/<domain>/actions.ts
server/web/<domain>/actions.tsThe admin screens and storefront components in this repository call them.
oRPC is a single router, and a single route handler mounts it:
server/router.ts → app/api/rpc/[[...rest]]/route.tsOutside integrations and AI agents that hold a ServiceApiKey call it.
REST covers the most ground:
app/api/**/route.ts
app/ai/v1/**/route.tsAI agents, feed readers, services sending webhooks and the browser's own fetch calls all use it.
Server actions
Every admin and storefront write is a zsa action. Admin actions are built on the permission procedures in lib/permissions/procedures.ts.
Before a handler runs, permissionProcedure does four things:
- Loads the session.
- Resolves the permission context.
- Rejects the request if the user has no admin role.
- Checks again whether the account is banned.
The fourth step 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 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 matching procedure, customerProcedure in lib/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 knowing which half of the app it called.
When a module extends BaseCRUD, Litestore generates six actions from its declaration:
createAction
updateAction
duplicateAction
deleteAction
restoreAction
permanentDeleteActionThe module re-exports the ones it uses. Anything that does more than save data is written by hand in the same file.
oRPC at /api/rpc
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 `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 has one entry, webhooks, with six procedures:
list
create
get
update
delete
rotateSecretEach procedure is built with serviceProcedureWithScope from lib/orpc.ts. It authenticates a ServiceApiKey through requireServiceKey() and then checks the key's scope. The two reads need read, and the four writes need write:webhooks. A key without the right 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:mcpThe comment in that file records the rule behind the list. We removed every scope that no request path checks, because advertising a scope lets an operator grant power that is never checked. The grant looks like a boundary, but it isn't one.
lib/orpc.ts also defines baseProcedure, which adds db and a revalidate helper to the context. That file also names the deleted admin surface. The session-auth and permission procedures that once copied the server actions were removed along with it.
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. Its title comes from config.site.name, and it is served with Cache-Control: public, max-age=3600.
The document describes 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-categoriesThe paths are grouped under five tags. The first two are tagged Discovery and the next three Products. The last three are tagged Recommendations, Tracking and AI.
Six of the eight paths need no authentication. /api/capabilities and /ai/v1/suggest-categories need an admin session. The only security scheme the document declares is sessionAuth, which is an apiKey sent in the session cookie.
/api/products accepts these query parameters:
page
limit
category
collection
q
formatformat is either json or jsonld. /api/products/feed returns the whole catalog as a Schema.org ProductCollection, which Google Merchant Center and agents can read.
What the spec leaves out
The spec is a hand-picked contract for agents. It doesn't list every route in the app.
Litestore has more than forty route handlers under app/api and seven under app/ai/v1, and the document names only one of the seven.
The service-key REST endpoints are left out. app/api/webhooks/route.ts authenticates through authenticateWebhookRequest(request, scope), yet neither the spec nor the oRPC router's scope comment mentions it.
The MCP endpoints are left out too:
app/api/mcp/route.tsserves shoppers.app/api/mcp/admin/route.tsserves 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 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, no check will fail.
Which transport a new endpoint belongs to
Decide by who calls the endpoint, not by the shape of the data.
If an admin screen or a storefront component in this repository calls it, write a zsa server action next to its domain. Only this transport gives you the permission procedures and the [data, error] tuple without extra work.
If an outside integration with a ServiceApiKey calls it to manage webhooks, add it to the oRPC router behind the scope that already exists for that.
If an agent, a feed reader, a provider posting a webhook or the browser fetching from a page calls it, write a REST route handler. If agents should be able to find the route, add it to the OpenAPI document yourself. The route works without being listed, and no check will tell you it is missing.
Throughout the codebase, we follow the rule written in the router comment:
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.