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/event are 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_id nullable + the binding read contract. Anonymous events (pre-login browsing) carry only anonymous_id; an append-only row's consumer_id can never be backfilled once identity_map later resolves it. Every reader needing a consumer's complete history must resolve anonymous_id → consumer_id via identity_map in addition to filtering on the row's own consumer_id — a read-time convention, not DB-enforced (mirrors ai.ai_request's own disclosed pattern).
  • Disclosed arithmetic correction: consumer_identifier built 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_id stays a deferred, unwired forward-ref; consumer_merchant_link.crm_customer_id is 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.

Last modified: Jul 11, 2026, 12:43 AM PT
On this page
Esc