Ecommerce Boilerplate
API Reference

Commerce RPC API

Authenticated RPC endpoint for SQL backends: POST /api/commerce/[service]/[method].

The Commerce RPC endpoint is an RPC-style bridge from the client to a server-side commerce adapter (Postgres/Prisma, SQLite, or Supabase). It exists only when the store runs on a SQL backend: the client httpCommerce adapter (src/lib/commerce/http.ts) posts one HTTP request per commerce call, and this route dispatches it to the resolved server backend.

On the default Firebase/Firestore setup the client talks to the backend directly and this route is not used. It is the network boundary for the SQL backends described in SQL backends & the commerce RPC bridge. Who the caller is stays a separate choice from where the data lives — see authentication.

Endpoint

POST /api/commerce/<service>/<method>
Authorization: Bearer <firebase-id-token>
Content-Type: application/json

{ "args": [ ... ] }
  • Path — <service> and <method> name a whitelisted commerce call, e.g. products.getById → /api/commerce/products/getById.
  • Credential — whatever the configured identity provider reads. On the Firebase default that is an ID token in Authorization: Bearer <token>, as shown above; on AUTH_PROVIDER=authjs it is a session cookie and there is no header at all. Public methods ignore it; user and admin methods require it.
  • Body — strictly { "args": unknown[] }: an object whose args is an array holding the method's positional arguments.

The route pins the Node.js runtime:

src/app/api/commerce/[service]/[method]/route.ts
export const runtime = "nodejs";

firebase-admin, the pg pool, and the Supabase client cannot run on the Edge runtime. It declares no caching mode — route handlers are dynamic by default, and every RPC call resolves live data.

Response

On success the route returns HTTP 200 with the method's return value under result (a null/undefined return serializes as null):

{ "result": <return value> }

On any failure it returns a 4xx/5xx status with a plain error string:

{ "error": "Authentication required." }

Date values in result become ISO 8601 strings on the wire (via JSON.stringify / Date.prototype.toJSON). The client adapter revives the known timestamp fields back into real Dates. In the other direction, the route revives Date-bearing arguments (see Date handling).

Access model

Every whitelisted method carries one of three access levels. Public methods skip auth entirely; the others resolve the caller through getAuthUser() (src/lib/auth) and then apply an ownership or role check. The route does not know which provider answered, and must not: it needs a uid, nothing more.

AccessRequirement
publicNo authentication. Catalog reads and discount lookup.
userA valid token is required, and 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, and profile.
adminA valid token is required, and admin.isAdmin(uid) must return true for the backend in use.

quotes.submit is the one special case: it is a user method but allows a null userId for guest submissions (no token needed in that case). If the userId argument is non-null it must still match the token's uid.

admin.isAdmin is intentionally exposed as a user method so the client can check its own admin status.

Status codes

StatusWhen
200Success — { result }.
400Body is not valid JSON, or is not { args: unknown[] }.
401A non-public method was called without a valid token ("Authentication required.").
403Token valid but the ownership/admin check failed (e.g. "You may only act on your own account.", "Admin privileges required.", "You may only submit quotes as yourself.").
404<service>.<method> is not in the whitelist ("Unknown commerce method: <service>.<method>").
500Backend not configured ("Commerce backend is not configured.") or the adapter method threw ("<service>.<method> failed.").

The request is processed in order: whitelist lookup → body parse → backend resolution → access control → dispatch. An unknown service/method is deliberately indistinguishable from a non-existent route (both are 404).

Method whitelist

Only the methods below are callable. Anything else responds 404. The whitelist covers all eight services of CommerceServices except for four deliberate omissions:

  • carts.listen (a live subscription) and carts.mergeCarts (a pure local function) have no HTTP surface and are implemented client-side in http.ts.
  • orders.recordPaidSession writes a paid order and decrements stock with no payment check of its own — the signature verification lives in the Stripe webhook, which calls the server adapter directly. Exposed here it would be an order-minting endpoint, so it 404s even for an admin.
  • audit.record is withheld for the same class of reason (below).

products

MethodAccess
listpublic
listByCategorypublic
countByCategorypublic
getByIdpublic
searchpublic
listFeaturedpublic
createadmin
updateadmin
deleteadmin
featureadmin
unfeatureadmin

carts

MethodAccess
getuser
setuser
updateItemuser
clearuser

carts.listen and carts.mergeCarts are not callable over HTTP — they live only in the client adapter.

quotes

MethodAccess
submituser (allows null userId for guests)
listForCustomeruser
listAlladmin
updateStatusadmin
listPageadmin
countByStatusadmin

orders

MethodAccess
listForCustomeruser
createFromQuoteadmin
listAlladmin
updateStatusadmin
listPageadmin
countByStatusadmin

discounts

MethodAccess
findUsablepublic
listAutomaticpublic
listadmin
saveadmin
removeadmin

listAutomatic is public where the full list is not: an automatic promotion needs no secret code — the cart applies it to everyone — so there is nothing in it to withhold. list returns every code, including the ones that are secret.

customers

MethodAccess
getuser
createuser
updateuser
listAddressesuser
addAddressuser
updateAddressuser
deleteAddressuser
listadmin
listPageadmin
countadmin

admin

MethodAccess
isAdminuser

audit

MethodAccess
listPageadmin

audit.record is not on this list, and answers 404 even to an admin.

The trail exists to record what an admin did. An HTTP-callable write taking a caller-supplied actorUid would let that same admin forge entries naming someone else, or bury a real one under noise — so the method is never exposed, and the route calls the server adapter in-process after a privileged mutation succeeds, with the actor taken from the verified token and the timestamp from the database clock.

The audited set is the admin mutations only — products.create / update / delete / feature / unfeature, quotes.updateStatus, orders.createFromQuote, orders.updateStatus, discounts.save, and discounts.remove. Reads are excluded: logging every list call would bury the record of who actually changed something.

Entries are paginated newest-first with an opaque cursor. Order is created DESC, id DESC — the id tiebreaker matters, because created is not unique and a keyset on a non-unique key skips or repeats rows at page boundaries. As everywhere else in this seam, cursor is null only when the page came back empty: a full final page still returns one, and the caller learns the trail is exhausted from the next, empty page.

Date handling

Some methods take arguments that carry Dates. Because those Dates crossed the wire as ISO strings, the route revives them before handing the args to the adapter (which expects real Dates). This is targeted per method — not a blind deep-walk — so schemaless string fields on products/customers are never misread as timestamps.

Service.methodArgument field(s) revived (on args[0])
discounts.savecreated, expiresAt (when non-null)
quotes.updateStatuscreated
orders.createFromQuotecreated
orders.updateStatuscreated

Examples

Public call — products.getById

No Authorization header needed for a public method:

Terminal
curl -X POST https://your-store.example.com/api/commerce/products/getById \
  -H "Content-Type: application/json" \
  -d '{ "args": ["prod_123"] }'
{
  "result": {
    "id": "prod_123",
    "name": "Widget",
    "featured": false
  }
}

Authenticated user call — carts.get

A user method: send the Firebase ID token and pass the caller's uid as args[0]. It must equal the token's uid or the request is rejected with 403.

Terminal
curl -X POST https://your-store.example.com/api/commerce/carts/get \
  -H "Authorization: Bearer $FIREBASE_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "args": ["uid_of_signed_in_user"] }'
{ "result": { "items": [], "updated": "2026-07-03T12:00:00.000Z" } }

If the token is missing or invalid:

{ "error": "Authentication required." }

If the uid in args[0] does not match the token:

{ "error": "You may only act on your own account." }

Guest quote submission — quotes.submit

quotes.submit accepts a null userId with no token, so guests can submit a quote request:

Terminal
curl -X POST https://your-store.example.com/api/commerce/quotes/submit \
  -H "Content-Type: application/json" \
  -d '{ "args": [null, { "items": [], "contact": { "email": "guest@example.com" } }] }'

A signed-in customer submits the same way but passes their own uid as args[0] and includes the token — a non-null userId that does not match the token yields 403 ("You may only submit quotes as yourself.").

On this page