Supercommerce API Docs
Admin API

Customer Module — Admin

HTTP surface for the admin customer-management portal (list, inspect, ban/unban, set-role, delete, set-password, session revocation, and storefront impersonation of non-staff accounts) plus the customer picker and lifetime-stats recompute. The storefront address book lives in the same module but is documented in store/customer.md.

HTTP surface for platform staff to manage customer accounts — every non-staff user (shoppers, including shoppers who are also vendors). Covers the management portal (list/detail + lifecycle actions + impersonation), the lightweight customer picker for form selects, and the lifetime-stats recompute job. The customer's storefront address book lives in the same module but is out of scope here (see store/customer.md).

Sources: api-modules/customer/src/controllers/admin-customer-management.controller.ts, admin-customer-actions.controller.ts, admin-customer-impersonation.controller.ts, admin-customer.controller.ts.

The customer identity lives in Better-Auth's user table; this module owns the shopping-related customer data and the admin lifecycle surface on top of it.


Scope & enforcement model

  • Who is a "customer": any account whose role is not admin/superAdmin, has no dynamicRole, and is not an anonymous guest. Staff accounts are excluded from every endpoint here and their ids resolve to 404 (existence is never revealed).
  • Vendor-linked customers are included. A shopper who also became a vendor is the same user row; they appear here, badged isVendor. Banning/deleting/impersonating affects their seller access too — surfaced as a warning in the UI.
  • Authorization is dynamicRole-driven. The built-in admin role grants nothing on its own; superAdmin retains full access. Every action below is independently grantable to staff via a dynamicRole and enforced by PermissionsGuard (built-in OR dynamic). delete, set-password, and impersonate are intentionally grantable (no superAdmin-only tier).
  • Target protection: all mutations refuse staff targets server-side (404), independent of the caller's grants. set-role additionally refuses assigning admin/superAdmin (the portal cannot mint staff).
  • Audit: ban/unban/set-role/delete/set-password/session-revoke/impersonate-start/impersonate-stop are written to user_admin_audit (actor, target, before→after, source, reason) in the same transaction as the change.

Authentication & permissions

EndpointPermission
GET /admin/customersuser: list
GET /admin/customers/:iduser: list
GET /admin/customers/:id/ordersuser: list
GET /admin/customers/:id/sessionsuser: list
GET /admin/customers/:id/addressesuser: list
GET /admin/customers/:id/audituser: list
POST /admin/customers/:id/banuser: ban
POST /admin/customers/:id/unbanuser: ban
POST /admin/customers/:id/set-roleuser: set-role
POST /admin/customers/:id/set-passworduser: set-password
DELETE /admin/customers/:iduser: delete
POST /admin/customers/:id/sessions/revoke-allsession: revoke
DELETE /admin/customers/:id/sessions/:tokensession: revoke
POST /admin/customers/:id/impersonateuser: impersonate
POST /admin/customers/impersonate/stopnone (must be in an impersonation session)
GET /admin/customers/pickeruser: list
POST /admin/customers/recalculate-statsuser: update
GET /admin/customers/recalculate-stats/statususer: update

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{ "data": <payload>, "message": "Success", "statusCode": 200, "metadata": { /* pagination if present */ } }

Paginated lists use the canonical metadata: { total, limit, offset, hasMore } shape.

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND (unknown id, or a staff/non-customer target)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Management portal

GET /admin/customers — Customer list

Paginated. Substring ILIKE search across email, name, phone (searchValue). Facets: banned, emailVerified, isVendor, hasOrdered (each true/false, omit for any). Sort: sortBycreatedAt | totalSpent | lastOrderedAt, sortDirectionasc | desc. Offset pagination (limit, offset). Excludes admin/superAdmin and anonymous guests.

Row shape (CustomerSummary): id, name, email, emailVerified, image, phoneNumber, phoneNumberVerified, banned, isVendor, createdAt, totalSpent, deliveredOrderCount, lastOrderedAt.

GET /admin/customers/:id — Customer detail

Returns CustomerDetail: profile + status (banned, banReason, banExpires) + isVendor + vendorMemberships[] ({ vendorId, vendorName, vendorSlug, memberRole }) + stats (lifetime aggregates, null until first order). 404 for unknown/staff ids.

GET /admin/customers/:id/orders — Orders (paginated)

limit/offset. Items: { id, orderNumber, status, paymentStatus, grandTotal, placedAt } (money in subunits), newest-first.

GET /admin/customers/:id/sessions — Active sessions

Array of { id, token, ipAddress, userAgent, isImpersonation, createdAt, expiresAt }.

GET /admin/customers/:id/addresses — Saved addresses

Array of the storefront address shape (see store/customer.md).

GET /admin/customers/:id/audit — Admin-action audit trail (paginated)

limit/offset. Items: { id, action, actorId, source, changes, createdAt }, newest-first.

POST /admin/customers/:id/ban — Ban

Body: { reason?: string, banExpiresInDays?: number } (omit banExpiresInDays for a permanent ban). Sets banned, revokes all the customer's sessions, audit-logged. Returns the refreshed CustomerDetail.

POST /admin/customers/:id/unban — Lift ban

Clears banned/banReason/banExpires. Returns refreshed CustomerDetail.

POST /admin/customers/:id/set-role — Set role

Body: { role: string | null } (null clears). Rejects admin/superAdmin with 403. Returns refreshed CustomerDetail.

POST /admin/customers/:id/set-password — Set password

Body: { newPassword: string (≥8), revokeSessions?: boolean (default true) }. Hashes with Better-Auth's configured hasher and updates the customer's credential account; 400 if the account has no password login (OAuth-only). Returns refreshed CustomerDetail.

DELETE /admin/customers/:id — Delete

Permanently deletes the account (cascades child rows; order.customerId set null). The deletion's audit row survives. Returns { success: true }.

POST /admin/customers/:id/sessions/revoke-all & DELETE /admin/customers/:id/sessions/:token

Revoke all or one session. Return the customer's remaining sessions.

POST /admin/customers/:id/impersonate — Impersonate (storefront)

Mints a storefront impersonation session bound to the acting admin (impersonatedBy = caller) and sets the session cookie on the response. The admin then browses the storefront as the customer. Vendor-context impersonation is not here — it lives in vendor management. Body: none. Response: { impersonatedUserId, impersonatedUserEmail, expiresAt }.

POST /admin/customers/impersonate/stop — Stop impersonating

Restores the acting admin's own session. No permission required — it only works from within an impersonation session (enforced by the admin_session cookie + impersonatedBy).


Customer picker

GET /admin/customers/picker — Customer picker (form selects)

Required permission: user: list. Substring ILIKE search across email and name. Pass selectedIds=<csv> to receive a pinned block of already-applied customers. Uses the picker envelope (data[] + pinned[]) — see Picker responses in catalog.md.

CustomerListItem: { id, email, name, image, createdAt, totalSpent, averageSpent, deliveredOrderCount, lastDeliveredAt, lastOrderedAt }. Lifetime stats live in customer_order_stats (maintained from source on order.placed/order.delivered) and are embedded via one batched lookup (no N+1).


Lifetime-stats recompute

POST /admin/customers/recalculate-stats — Recompute lifetime stats

Required permission: user: update. Enqueues a background BullMQ job that rebuilds customer_order_stats for all customers from source and backfills order.delivered_at. Deduped on a stable jobId. Response 202: { status: "queued" | "running", jobId }.

GET /admin/customers/recalculate-stats/status — Recompute status

Required permission: user: update. Poll until state is completed/failed. Returns { state, result, error, finishedAt }.


  • admin-rbac — gates every endpoint; the built-in admin role grants nothing, so staff access flows through dynamicRole. The current admin's effective permissions are served by GET /admin/rbac/me/permissions (used by the UI to gate actions). See admin-rbac.md.
  • auth — owns the user/session/account tables and Better-Auth's impersonation primitives; the impersonation session is minted via @sc/auth's mintImpersonationSession and stopped via Better-Auth's native stopImpersonating.
  • order, reviews — admin flows that consume the picker to attach a customer onto an admin-side action.

On this page