{
  "id": "2026-05-06-b2bea-org-entitlement-model-spec-4d13d65ab9",
  "scope": "redkey",
  "source_of_truth": "repo",
  "source_path": "docs/specs/2026-05-06-b2bea-org-entitlement-model-spec.md",
  "source_kind": "markdown",
  "visibility": "internal",
  "renderer_id": "design_doc.dreamborn-forge.generated.v1",
  "design_system": "dreamborn-design-system:forge",
  "generated_at": "2026-05-09T13:00:55.711Z",
  "artifact_type": "design_doc",
  "schema_version": "design_doc.generated.v1",
  "title": "B2BEA.org V1 Entitlement Model Spec",
  "summary": "B2BEA.org V1 Entitlement Model Spec Source of record: RedKey Supabase Studio artifact. Project: B2BEA.org Rebuild Project ID: a820dd0c 6cef 4133 bfbd d802fd806e44 Artifact: entitlement model spec Artifact ID: 355b3249 3af9 45a4 9c45 67777bd2d72d Version: 1 Status: draft Updated: 2026 05 06T23:32:25.756783+00:00 Live Supabase Check Checked against the active ...",
  "format_source": "markdown",
  "sections": [
    {
      "title": "B2BEA.org V1 Entitlement Model Spec",
      "level": 1,
      "body": "Source of record: RedKey Supabase Studio artifact.\n\n- Project: `B2BEA.org Rebuild`\n- Project ID: `a820dd0c-6cef-4133-bfbd-d802fd806e44`\n- Artifact: `entitlement-model-spec`\n- Artifact ID: `355b3249-3af9-45a4-9c45-67777bd2d72d`\n- Version: `1`\n- Status: `draft`\n- Updated: `2026-05-06T23:32:25.756783+00:00`"
    },
    {
      "title": "Live Supabase Check",
      "level": 2,
      "body": "Checked against the active B2BEA Supabase project and current `B2BEA-org-new` code signals.\n\n- Supabase host: `czqxkykbhoyyjccckpqq.supabase.co`\n- Source repo: `/Users/justinking/Vaults/Projects/B2BEA-org-new`\n- Tables checked: `people`, `person_roles`, `membership_tiers`, `memberships`, `membership_seats`, `vendor_memberships`, `organization_members`, `courses`, `course_enrollments`, `events`, `content_embeddings`\n\nCurrent entitlement-related row counts:\n\n| Table | Rows | Notes |\n|---|---:|---|\n| `membership_tiers` | 3 | Seed tiers: Registered, Pro, Vendor. |\n| `memberships` | 1 | One active Pro membership. |\n| `membership_seats` | 0 | Seat assignment base exists but is unused. |\n| `vendor_memberships` | 0 | Vendor commercial/access layer exists but is unused. |\n| `organization_members` | 0 | Company workspace membership base exists but is unused. |\n| `courses` | 9 | Course catalog has mixed access/pricing fields. |\n| `course_enrollments` | 4 | Current learner participation records. |\n| `person_roles` | 8 | Role grants include Pro/vendor/admin-style roles. |\n\nCurrent tiers:\n\n| Tier | Category | Billing | Seat Model | Access Summary |\n|---|---|---|---|---|\n| Registered | practitioner | free | individual | Baseline account/profile access; no premium courses, reports, events, or vendor portal. |\n| Pro | practitioner | subscription | individual | All courses, reports, and events; no vendor portal; no enhanced directory. |\n| Vendor | vendor | invoice | company | Vendor portal write access and enhanced directory; no practitioner course/report/event grants by default. |\n\nCode signals:\n\n- `src/assets/js/authbridge.js` still checks the legacy `persons` table when evaluating Pro access.\n- Reports and guides call client-side `authbridge.checkProAccess()` for Pro gating.\n- Courses combine `courses.access_tier`, `courses.is_included_with_pro`, pricing fields, and `course_enrollments`.\n- Admin user tooling edits `people.is_pro` and `person_roles`, which can drift from active memberships.\n- `membership_tiers.access_rules` exists but is not yet the single enforced entitlement contract."
    },
    {
      "title": "Decisions",
      "level": 2,
      "body": "| ID | Topic | Decision |\n|---|---|---|\n| `ENT-DEC-001` | Canonical entitlement truth | Active `memberships`, `membership_tiers.access_rules`, and `membership_seats` are the canonical source for paid/product access. Roles are permissions and operational responsibility, not paid entitlement truth. |\n| `ENT-DEC-002` | `people.is_pro` | `people.is_pro` may remain as a denormalized display/cache flag during migration, but it must not be treated as authoritative access truth. |\n| `ENT-DEC-003` | Server-side enforcement | Protected reads, protected downloads, enrollments, mutations, exports, and portal actions require a server-side entitlement check. Client-side checks may only improve UX. |\n| `ENT-DEC-004` | Company workspace | Practitioner company workspace uses organization-held memberships plus `membership_seats` for employee access to academy/careers benefits. Company public profiles are excluded from V1. |\n| `ENT-DEC-005` | Vendor portal | Vendor portal access requires a vendor relationship role and an active/approved vendor commercial entitlement when commercial gating is required. Vendor public profiles remain V1. |\n| `ENT-DEC-006` | Course enrollment | `course_enrollments` represents participation/progress after eligibility is granted. Eligibility to enroll comes from entitlement policy, purchase, admin grant, or company seat assignment. |\n| `ENT-DEC-007` | HubSpot boundary | HubSpot remains CRM primary for sales and renewals. B2BEA Supabase stores product entitlements and operational access state; HubSpot can mirror status but does not enforce site access. |"
    },
    {
      "title": "Target Principles",
      "level": 2,
      "body": "- Every protected capability resolves through one entitlement evaluator with auditable inputs and deterministic outputs.\n- Entitlement decisions are explicit: subject, action, resource, source, status, start time, end time, and reason.\n- Roles grant authority to administer or act on behalf of an account; memberships and seats grant product access.\n- A user can hold multiple contexts at once: individual Pro, vendor admin, company admin, employee seat, and internal platform admin.\n- Entitlement state should be explainable in the UI for support: why access exists, where it came from, when it expires, and who assigned it."
    },
    {
      "title": "Entitlement Subjects",
      "level": 2,
      "body": "| Subject | Examples | Current Sources |\n|---|---|---|\n| Person | Registered account, Pro member, course learner, company employee seat, vendor team member, platform admin | `people`, `person_roles`, `memberships.held_by_person_id`, `membership_seats.assigned_person_id`, `course_enrollments` |\n| Organization | Private practitioner company workspace, company academy/careers account | `organizations`, `organization_members`, `memberships.held_by_org_id`, `membership_seats` |\n| Vendor | Public vendor profile, vendor management portal account, enhanced directory listing | `vendors`, `vendor_memberships`, `person_roles`, `vendor_*` portal tables |"
    },
    {
      "title": "Entitlement Keys",
      "level": 2,
      "body": "| Key | Area | Grants |\n|---|---|---|\n| `account.registered` | Identity | Baseline authenticated account and profile access. |\n| `membership.pro` | Membership | Individual Pro status and Pro-only product eligibility. |\n| `resource.report.read.pro` | Resources | Read/download Pro reports and guides. |\n| `academy.course.enroll.included` | Academy | Enroll in courses included with the member tier or company seat. |\n| `academy.course.purchase` | Academy | Enroll in a course through a direct purchase/admin grant path. |\n| `academy.certification.earn` | Academy | Attempt certification requirements and receive certificates. |\n| `event.register.member` | Events | Register for member-included or member-priced events. |\n| `vendor.portal.read` | Vendor | View vendor portal dashboard and own profile/submission state. |\n| `vendor.portal.write` | Vendor | Submit vendor profile edits, case studies, media, and portal updates. |\n| `vendor.analytics.export` | Vendor | Export bounded own-vendor analytics. |\n| `company.workspace.read` | Company | Access private company workspace. |\n| `company.workspace.admin` | Company | Manage company users, jobs, academy assignments, and workspace settings. |\n| `company.academy.assign` | Company | Assign included academy content to employees. |\n| `company.careers.submit_job` | Company | Create job postings for admin review. |\n| `company.analytics.export` | Company | Export bounded own-company reporting. |\n| `survey.create` | Survey | Create survey definitions and assignments. |\n| `survey.respond` | Survey | Take assigned/public surveys. |\n| `survey.results.read` | Survey | View permitted survey results and dashboards. |\n| `admin.content.publish` | Internal admin | Publish standard Sanity pages or imported custom HTML. |\n| `admin.platform.manage` | Internal admin | Manage users, vendors, content review queues, and platform settings. |"
    },
    {
      "title": "Target Data Model",
      "level": 2,
      "body": "Use and harden the existing tables:\n\n| Table | Role |\n|---|---|\n| `membership_tiers` | Defines tier category, seat model, billing model, and structured `access_rules`. |\n| `memberships` | Canonical commercial/product entitlement holder for person-held, organization-held, and vendor-held memberships. |\n| `membership_seats` | Seat assignment from an org/vendor/company membership to an individual person. |\n| `person_roles` | Operational authority and relationship roles such as `platform_admin`, `vendor_admin`, `company_admin`, `author`. |\n| `course_enrollments` | Learning participation/progress after eligibility is granted. |\n\nAdd or harden:\n\n| Table | Purpose | Core Fields |\n|---|---|---|\n| `entitlement_grants` | Optional normalized/materialized grant table for fast and auditable access decisions. | `id`, `subject_type`, `subject_id`, `entitlement_key`, `source_type`, `source_id`, `status`, `starts_at`, `ends_at`, `metadata`, timestamps |\n| `entitlement_audit_events` | Audit log for grants, revocations, seat assignments, admin overrides, purchases, cancellations, and policy denials. | `id`, `actor_person_id`, `subject_type`, `subject_id`, `entitlement_key`, `event_type`, `source_type`, `source_id`, `reason`, `metadata`, `created_at` |\n| `course_assignments` | Company/admin course assignment layer separate from course participation. | `id`, `course_id`, `assigned_to_person_id`, `assigned_by_person_id`, `organization_id`, `source_type`, `status`, `due_at`, timestamps |\n| `entitlement_policy_tests` | Machine-readable policy fixtures for production gating regressions. | `id`, `scenario_key`, `subject_fixture`, `resource_fixture`, `expected_decision`, `created_at` |"
    },
    {
      "title": "Enforcement Contract",
      "level": 2,
      "body": "Input shape:\n\n| Field | Meaning |\n|---|---|\n| `subject` | `person_id` plus active organization/vendor context when applicable. |\n| `action` | Named capability/action key such as `academy.course.enroll` or `vendor.portal.write`. |\n| `resource` | Resource type and ID when relevant, such as `course_id`, `vendor_id`, `report_id`, `organization_id`. |\n| `context` | Request metadata, impersonation/admin mode, source surface, and time. |\n\nOutput shape:\n\n| Field | Meaning |\n|---|---|\n| `allowed` | Boolean decision. |\n| `entitlement_key` | Resolved grant key or required key. |\n| `reason_code` | Machine-readable allow/deny reason. |\n| `source_refs` | Membership, role, seat, purchase, admin grant, or policy refs used for the decision. |\n| `expires_at` | Expiration timestamp when access is time-bound. |\n\nHard rules:\n\n- No protected mutation trusts only `localStorage`, client profile state, or `people.is_pro`.\n- RLS policies and server routes/functions must agree on the same entitlement keys.\n- Admin override must be logged with actor, subject, reason, and expiration when temporary.\n- Exports must be filtered by own vendor/company context before data leaves Supabase."
    },
    {
      "title": "V1 Access Matrix",
      "level": 2,
      "body": "| Area | Capability | Required Access | Source |\n|---|---|---|---|\n| Public site | Read public content/vendor profiles/jobs/events | Anonymous/public unless explicitly gated | Sanity + Supabase public projections |\n| Member profile | Update own profile | `account.registered` + owns profile | `people` + auth user |\n| Member profile | Change password/account settings | Authenticated own account | Auth provider + `people` |\n| Resources | Read/download Pro report or guide | `resource.report.read.pro` or admin override | Memberships + tier rules + protected resource metadata |\n| Academy | Enroll included course | `academy.course.enroll.included` or course purchase/admin grant | Membership tier, seat, purchase/grant, course policy |\n| Academy | Continue enrolled course | Active `course_enrollments` row and not revoked | `course_enrollments` + entitlement status |\n| Academy | Create/manage course | `admin.platform.manage` or academy admin role | `person_roles`/`platform_admins` |\n| Vendor portal | Update vendor profile | `vendor.portal.write` + vendor relationship | Vendor entitlement + vendor ownership role |\n| Vendor portal | Create case study/content submission | `vendor.portal.write` | Vendor entitlement + `vendor_content_submissions` |\n| Vendor portal | Export own analytics | `vendor.analytics.export` | Vendor role + own vendor analytics filters |\n| Company workspace | Invite/manage employees | `company.workspace.admin` | `organization_members` + membership held by org |\n| Company workspace | Assign academy courses | `company.academy.assign` + available seats/content grant | `membership_seats` + `course_assignments` |\n| Company workspace | Submit job posting | `company.careers.submit_job` | Organization role + admin review workflow |\n| Surveys | Create survey | `survey.create` or `admin.platform.manage` | Future survey model + roles |\n| Surveys | Take survey | `survey.respond` + assignment/public rule | Future survey assignment/audience model |\n| Surveys | Display/export results | `survey.results.read` + audience/ownership filter | Future survey response/reporting model |\n| Internal admin | Publish Sanity standard page or custom HTML import | `admin.content.publish` | Small internal admin allowlist/role for Brett, Sarah, Justin |"
    },
    {
      "title": "Gaps",
      "level": 2,
      "body": "| ID | Severity | Area | Current | Required |\n|---|---:|---|---|---|\n| `ENT-GAP-001` | P0 | Legacy identity table | `authbridge.js` checks `persons` for Pro access while live schema uses `people`. | Replace `persons` usage with `people` or create/test an intentional compatibility view before launch. |\n| `ENT-GAP-002` | P0 | Fragmented Pro truth | `people.is_pro`, `person_roles.pro_member`, active memberships, and course fields can disagree. | Centralize Pro decision logic and backfill/derive display fields from canonical entitlement state. |\n| `ENT-GAP-003` | P0 | Client-side gating | Some reports/guides/course paths rely on client `authbridge` checks. | Server-side checks for protected reads/downloads/enrollments/mutations with RLS/function parity. |\n| `ENT-GAP-004` | P0 | Course access consistency | Courses have price fields, member prices, `access_tier` values, and `is_included_with_pro` flags that are not normalized. | Define a course access policy contract and migrate course rows to consistent values. |\n| `ENT-GAP-005` | P0 | Company seats | `membership_seats` and `organization_members` exist but are empty. | Implement org-held membership, employee role, seat assignment, revocation, and academy/careers benefit checks. |\n| `ENT-GAP-006` | P0 | Vendor membership | `vendor_memberships` is empty and not clearly tied to portal access. | Choose canonical vendor commercial source and wire vendor portal read/write/analytics checks to it. |\n| `ENT-GAP-007` | P1 | Auditability | No central entitlement audit event model observed. | Record grants, revocations, admin overrides, seat changes, purchases/cancellations, and denials. |\n| `ENT-GAP-008` | P1 | Surveys/forms | Generalized survey/form capability is missing. | New survey and form models must declare entitlement keys before implementation. |\n| `ENT-GAP-009` | P1 | Exports | Analytics tables exist but export entitlement rules are not centralized. | Implement own-account bounded export checks for vendors and companies. |"
    },
    {
      "title": "Migration Sequence",
      "level": 2,
      "body": "1. Inventory and remove/replace all `persons` table references in production code.\n2. Define entitlement key enum/constants shared by server functions, RLS, and UI display.\n3. Normalize `membership_tiers.access_rules` into versioned machine-readable rules.\n4. Implement entitlement evaluator and policy test fixtures before wiring new protected features.\n5. Backfill current Pro member state from `memberships` and reconcile `people.is_pro`/`person_roles` drift.\n6. Normalize course access fields and write course enrollment eligibility tests.\n7. Wire vendor portal and company workspace gates to memberships/seats/roles.\n8. Add entitlement audit events and support/admin visibility.\n9. Add survey/company/vendor export entitlement checks as those modules are built."
    },
    {
      "title": "Acceptance Criteria",
      "level": 2,
      "body": "- A developer can answer \"why does this person have access?\" from one entitlement decision path.\n- Protected resources cannot be downloaded by editing client state or bypassing the UI.\n- Pro, vendor, company, and admin access are separately testable and can coexist on one person account.\n- Seat assignment and revocation changes access immediately or within a documented cache window.\n- All entitlement-changing operations create an audit event.\n- The old `persons`-table access path is gone or intentionally shimmed and covered by tests."
    },
    {
      "title": "Next Specs",
      "level": 2,
      "body": "- `company-workspace-data-spec`\n- `survey-system-spec`\n- `notification-spec`\n- `academy-certification-spec`\n- `vendor-portal-workflow-spec`"
    }
  ],
  "html_path": "artifacts/2026-05-06-b2bea-org-entitlement-model-spec-4d13d65ab9.html",
  "json_path": "artifacts/2026-05-06-b2bea-org-entitlement-model-spec-4d13d65ab9.json"
}