Acme Store Docs
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 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.
  • Authorization header — a Firebase ID token, sent as Bearer <token> when a user is signed in. Public methods ignore it; user and admin methods require it. The header is matched case-insensitively against ^Bearer\s+(.+)$.
  • Body — strictly { "args": unknown[] }: an object whose args is 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:

src/app/api/commerce/[service]/[method]/route.ts
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.

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 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

MethodAccess
listpublic
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

orders

MethodAccess
listForCustomeruser
createFromQuoteadmin
listAlladmin
updateStatusadmin

discounts

MethodAccess
findUsablepublic
listadmin
saveadmin
removeadmin

customers

MethodAccess
getuser
createuser
updateuser
listAddressesuser
addAddressuser
updateAddressuser
deleteAddressuser
listadmin

admin

MethodAccess
isAdminuser

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