consumer — Module Spec
1. Purpose
consumer is the platform-level person account (Model B) — module #23, the Consumer Layer's first real v2 build, schema-locked 2026-07-11 (superseding a stale v1-carryover placeholder count that was never a real v2 build). One consumer identity spans every Vrida-tenant nursery they interact with. Non-tenant-scoped (the third non-tenant schema after platform and shared) — 8 of its 10 tables are consumer-scoped RLS instead, reachable only by the consumer themselves via a dedicated Postgres role. See PROJECT_DECISIONS #56.
2. Ownership
Owns — 10 tables, 104 columns:
| Table | Cols | Role |
|---|---|---|
consumer |
17 | Identity root — social-login auth seam, status lifecycle |
consumer_identifier |
9 | Multi-identifier model (multiple emails/phones/social IDs per consumer) |
consumer_address |
13 | Home/shipping/billing addresses |
consumer_interest |
8 | Plant-category/specific/care-topic interests (recommendation engine) |
consumer_consent |
9 | Append-only, PLATFORM-level consent (distinct from crm.customer_consent's merchant-level record) |
identity_merge_event |
9 | Append-only audit of a stub-claim or identifier-match merge |
identity_map |
6 | Resolves an anonymous_id to a consumer_id once known |
consumer_feature |
11 | RFM / recommendation-engine derived-feature cache (batch-computed) |
consumer_merchant_link |
12 | Tenant-scoped exception — which nurseries a consumer is linked to |
event |
10 | Tenant-scoped exception, append-only — the engagement spine (purchase/cart/view/email/loyalty/offer/search) |
| Total | 104 |
Does NOT own: the nursery's own view of a customer relationship (crm.customer — link, don't merge; crm.customer.consumer_id is a deferred forward-ref, still unwired at this build); merchant-level consent (crm.customer_consent); loyalty/offer definitions (rewards/offers own those, consumer_id is a dimension on their tables).
3. Layer & Dependencies
Platform-level identity foundation for the Consumer Layer (rewards, offers, future consumer_app). Depends on: platform (tenant, for the 2 tenant-scoped exceptions only), shared (country), multi_loc (site, via event.site_id), crm (loose, unenforced crm_customer_id ref on consumer_merchant_link). Depended on by: rewards (loyalty_account.consumer_id), offers (offer/offer_code/offer_assignment/offer_redemption/customer_discount_exposure, all .consumer_id).
4. The consumer_authenticated Role Mechanism
Investigated a Supabase Custom Access Token Auth Hook (PostgREST/GoTrue's automatic JWT-role-claim switching) — structurally viable in this Supabase project in general, but not applicable here: apps/api (NestJS) owns its Postgres connection directly and never routes through PostgREST's auto-role-switching layer. Decided: a parallel consumerDB() helper, mirroring tenantDB()'s own SET LOCAL ROLE + set_config() pattern, against a new consumer_authenticated role (NOLOGIN NOINHERIT). The 8 consumer-scoped tables are REVOKE ALL'd from the merchant authenticated role at the ACL layer (not merely un-granted) and reachable only via consumer_authenticated. Cross-tenant reads (a consumer's balances/offers across every linked nursery) go through consumer.get_cross_tenant_activity(p_consumer_id uuid), a SECURITY DEFINER function taking the id as a parameter, never an ambient session GUC.
5. Capabilities — honest Part D framing
No agent surface in this schema. Identity merge events (identity_merge_event) audit but do not prevent mis-merges — the real conflict-resolution judgment (a recycled email/phone claimed by a second party) lives in the not-yet-built service layer, not the DB. consumer_feature is a batch-computed cache (RFM/recommendation), recomputed wholesale, not itself autonomy-tracked.
6. Service Contract — ConsumerService
Not built this pass. Binding future requirement: all cross-tenant reads route through consumer.get_cross_tenant_activity(); no service method may construct an ad hoc cross-tenant JOIN. consumerDB() (the connection helper) is the only sanctioned way to reach the 8 consumer-scoped tables.
7. Design Rationale
- Boundary split, not schema-wide RLS. Only 8 of 10 tables are consumer-scoped;
consumer_merchant_link/eventare deliberate tenant-scoped exceptions, since a merchant legitimately needs its own POS-loyalty-lookup and engagement-event views. consumer_identifier, real table not JSONB. Identifiers must be individually filterable/joinable (SCHEMA_CONVENTIONS.md §19). Partial-unique on(identifier_type, identifier_value) WHERE superseded_at IS NULL— fail-closed by design: a recycled identifier locks to its first active claimant until a documented release flow exists.event.consumer_idnullable + the binding read contract. Anonymous events (pre-login browsing) carry onlyanonymous_id; an append-only row'sconsumer_idcan never be backfilled onceidentity_maplater resolves it. Every reader needing a consumer's complete history must resolveanonymous_id → consumer_idviaidentity_mapin addition to filtering on the row's ownconsumer_id— a read-time convention, not DB-enforced (mirrorsai.ai_request's own disclosed pattern).- Disclosed arithmetic correction:
consumer_identifierbuilt as 9 cols, not the design doc's stated 10 — its own itemized column list only ever named 9.
8. Agent Authority Mapping
None. This schema carries no *_actor_id/autonomy-pack columns at all — a consumer acts for themselves, not through the tenant-side identity.actor model.
9. Cross-Module Seams
- consumer → multi_loc:
event.site_id → multi_loc.site(id, tenant_id)(composite, nullable). - consumer → shared:
consumer_address.country_code → shared.country.iso_alpha2. - consumer → platform:
consumer_merchant_link.tenant_id/event.tenant_id → platform.tenant.id. - crm ↔ consumer:
crm.customer.consumer_idstays a deferred, unwired forward-ref;consumer_merchant_link.crm_customer_idis a deliberate loose ref (no FK either direction — different RLS domains). - rewards/offers → consumer: see their own module specs; both are real, enforced plain FKs →
consumer.consumer.id.
10. Deferred / Future Items
ConsumerService (the HTTP controller layer) — the next build step. Unclaimed-stub point handling, reward-expiry mechanics, and the specific social-login provider library remain undecided (see PROJECT_DECISIONS #56's "Record for design time" note).
11. v1 Exclusions Re-Confirmed
None disclosed as dropped — the 10-table shape preserves v1's identity/address/interest tables and extends them with the multi-identifier model, platform-level consent, and the engagement/identity-resolution pair v1 lacked.