Skip to content

Feature Entitlements, Bundles & Grey-Out ​

Canonical reference for Oktivo's feature-gating model: what a customer is entitled to, how features depend on each other, how bundles and role presets package them, and how un-entitled sections are hidden or greyed out.

This is the "single map" that ties together three systems that are easy to confuse. Read TEAM-RBAC.md for the per-team-member permission layer and CUSTOMER-ONBOARDING.md for how a new account is seeded.


1. The three layers (keep these separate) ​

A capability is reachable only when ALL layers that apply to it pass:

  1. ENTITLEMENT — does the account have the feature at all? Resolved as org.plan.features ∩ user.features. A customer effectively holds a feature only when BOTH the org plan and the user's feature set include it. Team members have no features of their own — they inherit the account owner's set (effectiveFeatures). Source: useFeature.ts (frontend), feature.middleware.ts (backend).
  2. RBAC — what may a team member do inside a held feature? Per-resource view/create/edit/delete + capabilities + sub-section + widget grants, resolved from assigned roles (+ optional per-user override). Owners / admins / superadmins bypass RBAC. Source: permissions.types.ts, rbac.middleware.ts. See TEAM-RBAC.md.
  3. WIDGET DATA — is a dashboard/marketing/segment widget's data returned? Widget grants gate individual cards. A subset is DATA-enforced server-side (API_ENFORCED_WIDGETS): the endpoint omits that widget's slice for a team member who lacks the grant, not just hides it client-side.

Entitlement decides whether a module exists for the account; RBAC decides what a team member does with it. They are orthogonal axes.


2. Source of truth (files) ​

ConcernFrontendBackend
Feature registryapp/src/lib/features.ts (FEATURES)types/index.ts (AVAILABLE_FEATURES)
Picker dependencies (silent auto-add/remove)FEATURE_DEPENDENCIES + toggleFeatureWithDependencies—
Module dependencies (block-on-save)MODULE_DEPENDENCIES + validateModuleSelectionMODULE_DEPENDENCIES + validateModuleDependencies (mirror, drift-guarded)
Module groups (admin grids + signup)SIDEBAR_FEATURE_GROUPS—
Sub-sectionsMODULE_SUBSECTIONS + effectiveSubSectionKeysmirror
Bundlesapp/src/lib/bundles.ts— (frontend-only presets)
Role presetsapp/src/types/permissions.tsrepositories/role.repository.ts (SYSTEM_ROLE_TEMPLATES)
Widget data enforcementAPI_ENFORCED_WIDGETSAPI_ENFORCED_WIDGETS (mirror, drift-guarded)
Grey-out policySidebarLayout.tsxOrganization.uiConfig.lockedModuleDisplay + SystemConfig global

The frontend/backend mirrors (MODULE_DEPENDENCIES, API_ENFORCED_WIDGETS, MODULE_SUBSECTIONS, connecter providers) are kept in lockstep by drift-guard tests.


3. Two dependency systems (they behave oppositely) ​

  • FEATURE_DEPENDENCIES — PICKER semantics (silent). Ticking a dependent auto-adds its prerequisite; unticking a prerequisite auto-removes its dependents. e.g. call_analysis → call_transcription, crm_segments → crm_leads, gmail → crm_leads, tickets.* → tickets, whatsapp.* → whatsapp, monitor.activity.orders/payments → commerce.

  • MODULE_DEPENDENCIES — BLOCK-ON-SAVE semantics (explicit 400 + warning; never silently mutates the selection). Current rules:

    • tickets → crm_leads
    • workflow → voice_campaign
    • whatsapp.automation → voice_campaign
    • call_flow_builder → voice_campaign
    • monitor.activity.orders → commerce, monitor.activity.payments → commerce

    Note: crm_leads → voice_campaign was REMOVED (Phase 1) so CRM is sellable without Call. MODULE_DEPENDENCIES is write-time only; existing stored docs that violate a rule keep working until the next save.

"If a customer asks for X, turn on …" (dependencies resolved) ​

  • Call only: voice_campaign, web_call, phone_to_phone_call, ai_call, web_agent, recorded_message, inbound_calls, csv_upload, call_flow_builder, benchmark. Add-ons (billed per use): call_recording, call_transcription, call_analysis.
  • CRM only: crm_leads + CRM sub-sections (crm.contacts/companies/opportunities/ tasks), crm_segments, crm.marketing, crm.agentic, lead_analytics. No voice_campaign required.
  • Commerce only: commerce + commerce.stores/inventory/orders/transactions (+ monitor order/payment tabs, each requiring commerce).
  • CRM + Tickets: CRM set above plus tickets (+ ticket sub-sections). tickets still requires crm_leads.
  • Call + WhatsApp/Marketing: voice_campaign + whatsapp (+ channels); WhatsApp automation additionally requires voice_campaign; the Marketing dashboard rides crm.marketing → crm_leads.

4. The three native call entitlements (Phase 1) ​

The Lead Detail → Call tab's three modes are separate paid features, gated both client-side (LeadCallPanel via useFeature) and server-side:

ModeEndpointFeature key
Web callPOST /api/leads/:leadId/calls/webweb_call
Phone-to-phonePOST /api/leads/:leadId/calls/phone-to-phonephone_to_phone_call
AI agentPOST /api/leads/:leadId/calls/agentai_call

Each route layers requireFeatureAccess(<key>) above crm.view. A CRM-only customer with no call feature can work leads but cannot place calls.

Back-compat: holding the legacy voice_campaign feature implies ai_call at entitlement-resolution time (expandImpliedFeatures in feature.middleware.ts and useFeature.hasFeature), so existing customers never lose AI calling on deploy. No DB migration.


5. Bundles (Phase 2) ​

A bundle is a named, dependency-resolved quick-start feature set (app/src/lib/bundles.ts). Applying one sets the admin grid / signup selection via the grid's bulk-change; the block-on-save validateModuleSelection still governs the write, so a bundle can never bypass validation.

BundleIntentNotes
call_onlyCalling platformAll three call modes + campaigns + IVR. Recording/transcription/analysis are add-ons, NOT bundled.
crmCRM suiteExcludes voice_campaign — sellable without Call.
commerceEcommerceStores/inventory/orders/transactions + activity-monitor tabs.
whatsapp_marketingWhatsApp + adsFull WhatsApp stack + chatbot + ads + Marketing dashboard (pulls in voice_campaign + crm_leads via real dependencies).
full_suiteEverything grantableComputed (every non-legacy, non-role-managed key minus the pure-rate call_transcription_stt), so it never drifts as FEATURES grows.

Helpers: resolveBundle(id), applyBundleToSelection(current, id), isBundleActive(current, id) (highlight an applied bundle), removeBundleFromSelection(current, id) (toggle off, preserving overlap with other still-active bundles). Covered by app/src/lib/bundles.test.ts, including an orphan guard asserting every grantable feature belongs to a SIDEBAR_FEATURE_GROUPS group (so the admin grid's "Other Platform Features" bucket stays empty).


6. Role presets (Phase 2) ​

Four system roles are seeded per account owner — see TEAM-RBAC.md for the full matrix: Org Admin (full), Manager (CRUD-minus-delete + sensitive caps + all widgets), Agent (view/create, scope="assigned", operational widgets only), Viewer (read-only, overview widget only). Seeded idempotently on owner creation; editing a preset mutates only that owner's copy.


7. Grey-out vs hide (Phase 3, org-configurable) ​

For features a customer is NOT entitled to, the sidebar can either HIDE the item (default, unchanged for existing orgs) or GREY IT OUT as a locked row with an upgrade/contact message. Policy resolution: **org override > superadmin global default

hide**.

  • Superadmin global default: SystemConfig (__default__), set in superadmin/SystemConfigPage.tsx.
  • Per-org override: Organization.uiConfig.lockedModuleDisplay: 'hide' | 'grey_out' (default null = inherit global), set in the org management UI.
  • Resolver: backend/.../utils/uiConfig.utils.ts (pure, tested). The resolved policy is threaded to the client on login / getMe and SidebarLayout honours it by reusing the EXISTING locked-upsell renderer (no second renderer).

8. "Common to all" (never gated) ​

Dashboard, Account/Settings (Profile, KYC, Security, Billing, API Tokens), Credits, and Notifications have no feature gate — every customer gets them regardless of plan. Keep them ungated when adding new gating.


9. Known gaps / recorded (not yet enforced) ​

From the Phase 2 Item-C audit (.agents/tasks/platform-feature-map/phase2-itemC-audit.md), recorded for a product decision rather than silently changed:

  • POST /api/leads/push-to-meta, /send-to-meta — gated on facebook_meta as of Phase 3; /send-to-google-ads gated on google_ads. (These were the recorded holes; Phase 3 closed them with an authoritative gate — verify ads-feature grants before deploy, as it is behaviour-changing for a manager who had the data permission but not the ads feature.)
  • analysisSchema.routes.ts — no module feature gate (cross-feature primitive; no single module key). Structural; still RBAC-gated on analysis.*.
  • WhatsApp Automation mutations share one whatsapp_automation.view grant (no create/edit split). Connecter writes gated by connecter.view (not a write-specific grant).

  • TEAM-RBAC.md — the per-team-member permission layer and the 4 role presets.
  • CUSTOMER-ONBOARDING.md — how a new account's features are seeded (signup config + bundles).
  • CRM-CALL-INTEGRATION.md / CRM-CALLING-ARCHITECTURE.md — the call systems the three call entitlements gate.
  • ADDON-FEATURES-BILLING.md — how recording/transcription/analysis add-ons bill.

Last updated: