Address Backend Documentation
Backend architecture, data model, services, cache, and operational behavior for the Address module.
Address - Backend Documentation
1. Documentation Evidence
| Area | Files Inspected | Verified Details |
|---|---|---|
| Module wiring | apps/api/src/modules/address/ module files | Leaf composition, mobile registration |
| Controllers | customer/address-customer.controller.ts | Routes, guards, idempotency scopes |
| Services | customer/address-customer.service.ts | Default invariant, advisory lock, serviceability-on-read |
| DTOs | customer/dto/*.ts | Phone/coordinate/ward validation |
| Schema | packages/db/src/schema/address/customer-address.ts | CHECKs, partial unique default, CASCADE |
| Phone | apps/api/src/utils/phone/phone.util.ts | NP region, E.164 |
| Error registry | apps/api/src/common/types/error-codes.ts (// CUSTOMER ADDRESS) | CUSTOMER_ADDRESS_* codes |
2. Backend Scope and Boundaries
Owns
- The customer address book (seven customer routes; no admin surface — the table holds names, phones and GPS coordinates, and a cross-customer read path should not exist until something concretely needs one).
- The default-address invariant and the archive/restore lifecycle.
- Phone normalization for
recipient_phonevia the shared phone utility.
Does Not Own
- Order addresses — and this is the single most important sentence in this module's docs: orders must SNAPSHOT the delivery address, never foreign-key to
customer_address.fk_customer_address_customer_idisON DELETE CASCADE(packages/db/src/schema/address/customer-address.ts). When a customer is genuinely erased, every address they own is erased with them — including archived ones. That is correct privacy behaviour (addresses are PII that must not outlive their owner) and the accepted reason this deviates from the usual no-cascade-onto-soft-deletable rule. An order referencing an address row would lose its shipping destination at that moment; an order's address must stay truthful for as long as the order does. Copy the resolved fields — recipient name, phone, province, district, municipality name, ward, street, landmark, postal code, coordinates, delivery instructions — onto the order at checkout time. - The same applies to the shipping fee: the order must snapshot the quoted fee. An admin repricing a district must never change what an existing order was charged.
- Serviceability — computed on read from the live shipping configuration, never stored.
Source of Truth
| Concern | Source of Truth | Notes |
|---|---|---|
| Addresses | customer_address rows | Scoped to the customer in every WHERE |
| Default | Partial unique index on active rows | Serialized by advisory lock |
| Serviceability | Live shipping configuration | Computed per read, batched |
| Order addresses | Order snapshot | Never a reference |
3. Module Composition
| Module | Type | Path | Controllers | Providers | Exports | Responsibility |
|---|---|---|---|---|---|---|
AddressCustomerModule | Leaf | customer/ | AddressCustomerController | Service | — | The address book |
| Mobile composition | — | mobile.module.ts | — | — | — | Mounted under /api/mobile/addresses |
4. File and Directory Map
apps/api/src/modules/address/
customer/
address-customer.controller.ts
address-customer.service.ts
dto/ create-address.dto.ts update-address.dto.ts
list-addresses.query.dto.ts address-response.dto.ts
packages/db/src/schema/address/
customer-address.ts
apps/api/src/utils/phone/phone.util.ts # shared, NP default regionKey files:
| File | Purpose | Key Exports | Notes |
|---|---|---|---|
customer/address-customer.service.ts | The book's logic | AddressCustomerService | Default invariant, lock, serviceability |
utils/phone/phone.util.ts | Phone normalization | normalizeToE164 | NP default; shared with customers.phone |
schema/address/customer-address.ts | The table | customerAddress | CASCADE, partial unique default, CHECKs |
5. Data Model
5.1 Schema Source
packages/db/src/schema/address/customer-address.ts5.2 Tables
customer_address
| Column | Type | Nullable | Index/Constraint | Relation | Notes |
|---|---|---|---|---|---|
id / public_id | serial / uuid v7 | No | PK / UNIQUE | — | |
customer_id | uuid | No | FK ON DELETE CASCADE | customers.id | Privacy: addresses never outlive their owner |
recipient_name | varchar | No | — | — | |
recipient_phone | varchar | No | CHECK — must normalize to E.164 | — | Same rule as customers.phone; both must produce byte-identical E.164 |
district_id | integer | No | FK RESTRICT | district.id | Resolved via GeoLookupService |
municipality_id | integer | Yes | — | municipality.id | Always null — the reference table ships empty by design |
municipality_name | varchar | No | — | — | Free text the customer types |
ward | smallint | No | CHECK 1..40 | — | Generous; Kathmandu Metropolitan has 32 |
street / landmark / postal_code | varchar | Yes | — | — | |
latitude / longitude | double precision | Yes | CHECK: both or neither; inside Nepal (26.0–30.6, 79.9–88.3) | — | Pair rule catches transposed coords |
delivery_instructions | text | Yes | — | — | |
is_default | boolean | No | Partial unique index WHERE is_default AND archived_at IS NULL | — | At most one default among active rows |
archived_at | timestamptz | Yes | index | — | Soft delete; restore clears it |
Key CHECKs: chk_customer_address_ward_in_range, coordinate pair/range checks, recipient_phone E.164 normalization check. Active cap (20) is service-enforced; archived rows do not count.
5.3 Relationship Diagram
6. Services and Responsibilities
6.1 AddressCustomerService
| Method | Called By | Reads | Writes | Side Effects | Errors |
|---|---|---|---|---|---|
create() | POST | active count, district | address row | — | CUSTOMER_ADDRESS_LIMIT_REACHED, CUSTOMER_ADDRESS_DISTRICT_NOT_FOUND, phone/coords codes |
list() / findOne() | GET | own rows | — | — | CUSTOMER_ADDRESS_NOT_FOUND (scoped — also the answer for someone else's address; a 403 would be an existence oracle) |
update() | PATCH | active row | fields | — | CUSTOMER_ADDRESS_ALREADY_ARCHIVED, not-found |
archive() | DELETE | active row | archived_at | default promotion | CUSTOMER_ADDRESS_ALREADY_ARCHIVED |
restore() | POST restore | archived row | archived_at cleared | default recompute (only if no other active) | CUSTOMER_ADDRESS_NOT_ARCHIVED |
setDefault() | PUT /default | active rows | default flags | — | CUSTOMER_ADDRESS_ALREADY_ARCHIVED |
Set-default concurrency: a per-customer pg_advisory_xact_lock serializes concurrent calls so two set-defaults cannot race past the partial unique index; the previous default is cleared in the same transaction. restore is idempotency-guarded (customer-address-restore); create too (customer-address-create).
Phone: recipient_phone goes through normalizeToE164 with NP as the default region — local 9812345678 becomes +9779812345678. Both customers.phone and customer_address.recipient_phone must produce byte-identical E.164, or the address CHECK accepts one and rejects the other for the same input.
7. Runtime Flows
7.1 Set default
7.2 Create with serviceability
8. Cache
No address rows are cached — they are per-customer PII and cheap to read. Serviceability rides the shipping cache domain (batched behind one key), so an admin editing a district refreshes every address list on the next read.
9. Jobs and Workers
None. The address book is synchronous.
10. Security and Authorization
- Every route is scoped to the authenticated customer inside the service's WHERE clauses — there is no admin surface over this table, deliberately.
CUSTOMER_READ60/min andCUSTOMER_WRITE20/min, both keyed on the account (keyStrategy: "user"), so customers behind one carrier NAT do not exhaust each other's quota.- Cross-customer reads return 404
CUSTOMER_ADDRESS_NOT_FOUND— never 403, which would be an existence oracle. - Create and restore carry
Idempotency-Keyrequirements (scopescustomer-address-create,customer-address-restore).
11. Operational Notes
- The municipality reference ships empty — do not treat
municipality.id: nullas a bug; it is the designed state, andmunicipality.nameis the truth. - Restoring an archived address never restores its old default flag — the flag is recomputed, and the address becomes default only when the customer has no other active addresses.
- A customer with zero active addresses has no default — legal, not an error.