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 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. Firebase Auth remains the identity provider in every configuration.
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. Authorizationheader — a Firebase ID token, sent asBearer <token>when a user is signed in. Public methods ignore it;userandadminmethods require it. The header is matched case-insensitively against^Bearer\s+(.+)$.- Body — strictly
{ "args": unknown[] }: an object whoseargsis an array holding the method's positional arguments. The token is verified through the firebase-admin helper (src/lib/firebase/admin.ts).
The route runs on the Node.js runtime and is always dynamic:
export const runtime = "nodejs";
export const dynamic = "force-dynamic";The Node.js runtime is required because firebase-admin, the pg pool, and the Supabase client cannot run on the Edge runtime.
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 verify the ID token and then apply an ownership or role check.
| Access | Requirement |
|---|---|
public | No authentication. Catalog reads and discount lookup. |
user | A 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. |
admin | A 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
| Status | When |
|---|---|
200 | Success — { result }. |
400 | Body is not valid JSON, or is not { args: unknown[] }. |
401 | A non-public method was called without a valid token ("Authentication required."). |
403 | Token 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>"). |
500 | Backend 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 mirrors the CommerceServices type exactly, except for two CartService methods with no HTTP surface — carts.listen (a live subscription) and carts.mergeCarts (a pure local function) — which are deliberately absent and implemented client-side in http.ts.
products
| Method | Access |
|---|---|
list | public |
getById | public |
search | public |
listFeatured | public |
create | admin |
update | admin |
delete | admin |
feature | admin |
unfeature | admin |
carts
| Method | Access |
|---|---|
get | user |
set | user |
updateItem | user |
clear | user |
carts.listen and carts.mergeCarts are not callable over HTTP — they live only in the client adapter.
quotes
| Method | Access |
|---|---|
submit | user (allows null userId for guests) |
listForCustomer | user |
listAll | admin |
updateStatus | admin |
orders
| Method | Access |
|---|---|
listForCustomer | user |
createFromQuote | admin |
listAll | admin |
updateStatus | admin |
discounts
| Method | Access |
|---|---|
findUsable | public |
list | admin |
save | admin |
remove | admin |
customers
| Method | Access |
|---|---|
get | user |
create | user |
update | user |
listAddresses | user |
addAddress | user |
updateAddress | user |
deleteAddress | user |
list | admin |
admin
| Method | Access |
|---|---|
isAdmin | user |
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.method | Argument field(s) revived (on args[0]) |
|---|---|
discounts.save | created, expiresAt (when non-null) |
quotes.updateStatus | created |
orders.createFromQuote | created |
orders.updateStatus | created |
Examples
Public call — products.getById
No Authorization header needed for a public method:
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.
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:
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.").