Customer Module
HTTP surface for the customer-side address book (storefront) and an admin customer picker. The customer identity itself lives in Better-Auth's user table; this module owns the…
HTTP surface for the customer-side address book (storefront) and an admin customer picker. The customer identity itself lives in Better-Auth's user table; this module owns the shopping-related customer data on top of that.
Source:
api-modules/customer(registered viaCustomerModule.forRoot()inapps/api/src/app.module.ts).The address book is read by the order module at place-order (for the
shippingAddress/billingAddresssnapshot) and by the cart for the address-bound shipping rate. The customer picker is consumed by admin features that need to pin a customer (e.g. admin-create review).
Conventions
Authentication
| Endpoint group | Auth | Permission |
|---|---|---|
GET/POST/PATCH/DELETE /store/addresses/** | required (customer) | — |
GET /admin/customers | required | user: list |
Storefront endpoints scope every read and write to the session's user id. Cross-user ids return 404 Not Found, never 403 — so existence of someone else's address can't be inferred.
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}The admin picker uses the picker envelope (items[] + pinned[]) — see catalog.md.
Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Domain types
AddressResponse
type AddressResponse = {
id: string;
firstName: string;
lastName: string;
fullAddress: string; // 1..500 chars
city: string; // 1..80 chars
pincode: string; // Indian 6-digit, /^[1-9]\d{5}$/
state: string; // 1..80 chars
phone: string; // Indian, /^(\+91)?[6-9]\d{9}$/
country: string; // ISO-3166 alpha-2; default "IN"
isDefault: boolean;
createdAt: string; // ISO
updatedAt: string;
};Phone and pincode validation is India-specific today; internationalization swaps these for a country-driven validator.
CustomerListItem
type CustomerListItem = {
id: string;
email: string;
name: string;
image: string | null;
createdAt: string; // ISO
};Default address rule
A customer has at most one default address. The address book invariants:
| Trigger | Behavior |
|---|---|
| First-ever address (book is empty) | Force-promoted to isDefault: true regardless of input |
Subsequent create with isDefault: true | New row becomes default; prior default flips to false |
POST /:id/default | Same — flip swap |
| Delete default | No auto-promotion — caller is left without a default until they explicitly pick one. By design — silent re-promotion can ship orders to a stale address |
Storefront — /store/addresses
GET /store/addresses — List my addresses
Default address comes first; the rest are sorted by most-recently-touched.
Query
| Name | Type | Default | Constraints |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | 20 | 1..100 |
Response 200 — paginated envelope of AddressResponse[].
GET /store/addresses/:id — Get one of my addresses
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Address does not exist or belongs to another user |
POST /store/addresses — Create an address
Body
{
"firstName": "Ada",
"lastName": "Lovelace",
"fullAddress": "221B Baker Street",
"city": "London",
"pincode": "110001", // 6-digit, first digit 1–9
"state": "Delhi",
"phone": "+919876543210", // +91 prefix optional
"country": "IN", // ISO-3166 alpha-2; default "IN"
"isDefault": false
}Response 201 — AddressResponse. If this is the caller's first address, the response has isDefault: true even when the body sent false.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod (phone/pincode regex, missing required field) |
PATCH /store/addresses/:id — Update an address
Body — partial CreateAddressInput. At least one field is required (no-op writes are rejected at the zod layer to avoid burning a 200).
{ "city": "Mumbai", "pincode": "400001" }Response 200 — updated AddressResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Empty body; any field fails its individual schema |
| 404 | NOT_FOUND | Address not owned by caller |
DELETE /store/addresses/:id — Delete an address
Hard delete. Deleting the default address leaves the customer without a default until they explicitly pick one — no auto-promotion.
Response 204 No Content.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Address not owned by caller |
POST /store/addresses/:id/default — Set as default
Flips the chosen address to isDefault: true and the prior default (if any) to false. Idempotent — already-default is a no-op 200.
Response 200 — the new default AddressResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Address not owned by caller |
Admin
GET /admin/customers — Customer picker
Required permission: user: list. Substring ILIKE search across email and name. Pass selectedIds=<csv> to receive a pinned block of already-applied customers (see picker responses in catalog.md).
Query — standard QueryDto (page, limit, search, selectedIds).
Response 200
{
"data": {
"items": [
{
"id": "01J9...",
"email": "ada@example.com",
"name": "Ada Lovelace",
"image": "https://cdn.example/u/ada.png",
"createdAt": "2026-01-12T08:30:00.000Z"
}
],
"pinned": []
},
"metadata": { "total": 1234, "items": 1, "perPage": 20, "currentPage": 1, "lastPage": 62 }
}Related modules
auth— owns theusertable that the address book references viauserId.order— snapshotsshippingAddress/billingAddressfrom this module at place-order. Seeorder.md.cart— uses the customer's default address for the shipping-rate quote when no override is supplied at checkout.
Catalog Module
HTTP surface for the product catalog — vendor-facing CRUD over products, variants, options, tabs; vendor-side "request a new taxonomy entry" workflow; admin CRUD + approval over…
Discount Module
Admin-facing HTTP endpoints for managing platform-wide coupon/discount rules — creation, lifecycle (active → archived → soft-deleted), include/exclude filters across the catalog,…