Projects BibleWeb Navigation UX Tier & paywall system
Done

Tier & paywall system

Area: Navigation UX

1. Context

BibleWeb gates advanced features behind paid tiers. The tier system is the single source of truth for what each user can access. It must be usable both in server-side route guards and in client-side UI to show or hide features and prompt upgrades where appropriate.

2. Functional

Three tiers are defined: free, pro, premium.

Each tier maps to a TierFeatures record:

Feature key Type Free Pro Premium
maxNotes number | null 5 unlimited unlimited
dutchTranslation boolean true true true
parallelGospel boolean true true true
interlinear boolean false true true
commentaries boolean false true true
crossRefGraph boolean false true true
offlineDownload boolean false true true
noteCrossLinking boolean false true true
noteExport boolean false false true
aiChat boolean false false true
personalTranslation boolean false false true

null for maxNotes means unlimited. hasFeature() treats null and true as positive, false and 0 as negative.

Beta tier system (separate but related — see beta-activation-system):

  • Beta tiers (beta_high, beta_low, admin) sit alongside free/pro/premium and are resolved by getBetaTier() in beta-codes.ts.
  • AI daily limits are controlled by beta tier, not the TierFeatures system. The two systems are currently independent.

3. UX & Design

  • The pricing page (/pricing) is the primary surface for communicating tier differences — see the pricing-page feature for the full presentation.
  • Feature gates in the UI should use hasFeature(tier, key) to decide whether to render a feature or show an upgrade prompt.
  • No TierGate component exists yet — gating is expected to be implemented per-feature using the exported helpers directly.

4. Technical

Core module: apps/web/src/lib/tier.ts

Exports:

type Tier = 'free' | 'pro' | 'premium'

interface TierFeatures {
  maxNotes: number | null;
  dutchTranslation: boolean;
  parallelGospel: boolean;
  interlinear: boolean;
  commentaries: boolean;
  crossRefGraph: boolean;
  offlineDownload: boolean;
  aiChat: boolean;
  personalTranslation: boolean;
  noteExport: boolean;
  noteCrossLinking: boolean;
}

function getTierFeatures(tier: Tier): TierFeatures
function hasFeature(tier: Tier, feature: keyof TierFeatures): boolean

hasFeature logic: returns true if the feature value is true, null, or a number > 0.

Usage pattern:

  • Import hasFeature and the user's current tier (from user.currentTier in the user store).
  • Call hasFeature(tier, 'interlinear') etc. to conditionally render UI or redirect to /pricing.
  • The same TierFeatures type is imported by the pricing page to render the comparison table, ensuring the UI and the logic stay in sync.

Current tier source:

  • Authenticated users: resolved from Supabase session or app_metadata.beta_tier.
  • Unauthenticated: 'free' (or beta tier from cookie, handled by beta system).
  • Stripe integration is not yet built — user.currentTier always resolves to free for non-beta users in the current build.

5. Status

The tier definitions and helper functions are fully implemented. The pricing page correctly reflects the matrix. Per-feature gating in the reader/notes UI needs to be connected — hasFeature() is ready to use but individual features may not yet be checking it. Stripe integration (actual paid tier assignment) is not yet built.