API Reference Overview
The public Store REST API and the authenticated Commerce RPC API at a glance.
The boilerplate exposes two distinct API families, plus a handful of smaller endpoints that back one feature each:
- The Store REST API (
/api/store/*) is a public, read-only, cache-friendly view of the catalog. It is meant for external clients — native apps, alternate frontends, integrations — that need to read products and categories without touching internal storage details. - The Commerce RPC API (
/api/commerce/*) is the authenticated backend the app itself calls when it runs on a SQL backend (Postgres/Prisma, SQLite, or Supabase). It is an internal transport, not a public API, and every non-public method is authenticated by the configured identity provider.
On the default Firebase store the client talks to Firestore directly for shopper-side reads and writes, so most calls never reach this route. Admin mutations are the exception and always come back through it, so they can be re-authorized server-side and written to the audit trail with the server's own clock. On a SQL backend every call goes through it. See backends for how the commerce layer is wired, and audited admin writes for the Firebase exception.
The two API families
| Store REST API | Commerce RPC API | |
|---|---|---|
| Base path | /api/store | /api/commerce/<service>/<method> |
| Style | RESTful, resource URLs | RPC over POST |
| Method | GET (read-only) | POST |
| Auth | None (public) | The configured identity provider (public/user/admin) |
| Audience | External clients, native apps | The app's own client, SQL backends |
| Caching | CDN Cache-Control headers | None — live data on every call |
| Version | v1 | Unversioned internal bridge |
| Writes | Never | Yes (carts, quotes, orders, admin) |
Store REST API
The Store API reshapes data that is already publicly readable (products,
categories) into a stable wire format so callers never depend on internal
storage details. Inventory counts, for example, are collapsed into an inStock
flag so exact stock levels are never exposed.
GET /api/store is a discovery document that lists the available endpoints and
the API version:
curl https://your-store.example.com/api/store{
"name": "Your Store",
"version": "v1",
"endpoints": [
"/api/store/products",
"/api/store/products/{id}",
"/api/store/categories"
]
}Key characteristics:
- Read-only. Every endpoint is a
GET. There are no public write paths. - Cursor pagination.
GET /api/store/productsreturns{ products, nextCursor, totalCount }; pass the returnednextCursorback as?cursor=to page forward. See Store: Products. - CDN cache headers. Responses carry a public
Cache-Controlheader so a CDN or the browser can cache them (details below). - Stable wire shape. Products are returned as a narrowed
PublicProduct(id,productName,brand,price,categories,description,primaryImageURL,pdfLink,inStock).
Commerce RPC API
POST /api/commerce/<service>/<method> is an RPC-style bridge from the client to
a server-side commerce adapter. The client posts one request per commerce
call:
POST /api/commerce/products/list
Authorization: Bearer <firebase-id-token>
Content-Type: application/json
{ "args": [] }and receives { "result": <return value> }, or { "error": string } with a
4xx/5xx status. Dates in the result serialize to ISO strings and are revived
by the client.
Access levels
The caller is identified by getAuthUser(), which resolves whichever provider
AUTH_PROVIDER names — a Firebase ID token by default, an Auth.js session
cookie on authjs. The route never learns which; it needs a uid and nothing
more. See authentication. Each
whitelisted method carries one of three access levels:
| Level | Requirement |
|---|---|
public | No auth. Catalog reads and discount lookup. |
user | Valid token required; the method's uid argument (args[0]) must equal the token's uid, so a signed-in user can only act on their own cart/quotes/orders/profile. |
admin | Valid token required and admin.isAdmin(uid) must return true. |
quotes.submit is the one exception to the user rule: it allows a null
userId for guest submissions (no token needed then), but a non-null userId
must still match the token.
A method that is not on the whitelist responds 404, indistinguishable from a
non-existent route. See Commerce RPC for the full
whitelist and per-service argument shapes.
The Commerce RPC route runs on the Node.js runtime and requires a configured SQL
backend (getServerCommerce()); if the backend is not configured, every call
responds 500 with { "error": "Commerce backend is not configured." }.
Feature endpoints
The rest of /api backs one feature each. Most degrade to a no-op (or a 503)
when their integration is not configured, so the default quote-based store runs
without any of them.
| Endpoint | Method | Feature | When unconfigured |
|---|---|---|---|
/api/checkout | POST | Stripe Checkout (Checkout) | 503 { "error": "checkout not configured" } without STRIPE_SECRET_KEY |
/api/checkout/config | GET | Whether the cart renders a Checkout button | 200 { "enabled": false } |
/api/webhooks/stripe | POST | Stripe order recording (Webhooks) | 503 { "error": "webhook not configured" } without STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET |
/api/quotes/notify | POST | Resend quote emails (Notifications) | 200 { "skipped": true } without RESEND_API_KEY |
/api/notifications/status | POST | Resend status emails (Notifications) | 200 { "skipped": true } without RESEND_API_KEY |
/api/newsletter | POST | Mailing-list signup (Storefront) | Always live. In demo mode the address is accepted and discarded — { "ok": true, "stored": false } says so |
/api/auth/[...nextauth] | GET/POST | Auth.js sign-in, callback, session, sign-out (Authentication) | Never called unless AUTH_PROVIDER=authjs |
See environment variables for the flags that switch these on.
Common error shape
Both API families return errors as a single JSON object:
{ "error": "Human-readable message" }The Store API uses jsonError(status, message) and the Commerce RPC route uses
an equivalent error(message, status) helper, so the shape is identical across
the surface. Request data is never echoed back in error messages.
Standard status codes
| Status | Meaning |
|---|---|
400 | Bad request — malformed input (e.g. invalid cursor, unknown category, short search term, or a body that is not { args: [...] }). |
401 | Authentication required — a protected Commerce RPC method was called without a valid token. |
403 | Forbidden — token is valid but not permitted (acting on another user's data, or missing admin privileges). |
404 | Not found — no such product, or an unknown Commerce RPC service.method. |
429 | Too many requests — rate limit hit (checkout, the two notification endpoints, and newsletter signup). A Retry-After header is included. |
500 | Internal server error — unexpected failure, or the Commerce backend is not configured. |
503 | Service unavailable — an optional feature (e.g. Stripe checkout) is not configured. |
Cache-Control policies
The Store API sets one of two Cache-Control headers depending on how quickly
the underlying data changes. Both are defined in src/lib/api/store.ts:
/** Cache-Control for product endpoints (list, search, detail). */
export const PRODUCT_CACHE_CONTROL =
"public, s-maxage=60, stale-while-revalidate=300";
/** Cache-Control for slow-changing config-backed endpoints (categories). */
export const CONFIG_CACHE_CONTROL =
"public, s-maxage=3600, stale-while-revalidate=86400";| Policy | Header | Applied to |
|---|---|---|
PRODUCT_CACHE_CONTROL | public, s-maxage=60, stale-while-revalidate=300 | /api/store/products, /api/store/products/{id} (product list, search, and detail) |
CONFIG_CACHE_CONTROL | public, s-maxage=3600, stale-while-revalidate=86400 | /api/store (discovery) and /api/store/categories |
s-maxage is the shared-cache (CDN) freshness window in seconds;
stale-while-revalidate lets a CDN serve stale content for that many additional
seconds while it revalidates in the background. Products stay fresh for 60s;
category and config data, which changes rarely, for an hour.
The Commerce RPC route sets no cache headers — every RPC call resolves live
data. Nothing under /api is prerendered at all: route handlers have been
dynamic by default since Next 15, so instead of each route restating that with
a force-dynamic export, tests/api-routes-dynamic.test.ts asserts the built
route table still agrees (see testing).