Skip to content

feat: Freemium tier gating framework for GUI capabilities #631

@amiable-dev

Description

@amiable-dev

Summary

Introduce a framework for controlling which capabilities and features are available in a free tier vs a paid (Pro) tier. This is an infrastructure concern — the framework itself, not the specific tier boundaries (those are product decisions that will evolve).

Goals

  1. Gate features declaratively — a single source of truth for what's free vs Pro, not scattered if (isPro) checks throughout components
  2. Graceful degradation — free-tier users see that a feature exists but is locked, not a broken UI with missing pieces
  3. Upgrade nudge, not frustration — locked features show a brief explanation and upgrade path, not error messages
  4. Easy to adjust — moving a feature between tiers should be a config change, not a code change

Proposed Architecture

Entitlement Registry

A central module defining all gated capabilities and their tier requirements:

// entitlements.ts

export type Tier = 'free' | 'pro';

export interface Entitlement {
  id: string;
  label: string;           // Human-readable name
  tier: Tier;               // Minimum tier required
  description?: string;     // Shown on upgrade nudge
  limit?: number;           // For quantity-gated features (e.g., max mappings)
}

const ENTITLEMENTS: Record<string, Entitlement> = {
  'mappings.unlimited':     { id: 'mappings.unlimited',     label: 'Unlimited mappings',       tier: 'pro' },
  'chat.unlimited':         { id: 'chat.unlimited',         label: 'Unlimited chat messages',   tier: 'pro' },
  'chat.provider.all':      { id: 'chat.provider.all',      label: 'All LLM providers',         tier: 'pro' },
  'usage.analytics':        { id: 'usage.analytics',        label: 'Usage analytics',           tier: 'pro' },
  'profiles.export':        { id: 'profiles.export',        label: 'Profile export',            tier: 'free' },
  'plugins.wasm':           { id: 'plugins.wasm',           label: 'WASM plugins',              tier: 'pro' },
  'developer.mode':         { id: 'developer.mode',         label: 'Developer mode',            tier: 'free' },
  // ... etc
};

Entitlement Store

// stores/entitlements.js
import { writable, derived } from 'svelte/store';

export const currentTier = writable('free');  // Set by license validation

export function canUse(entitlementId: string): Readable<boolean> {
  return derived(currentTier, $tier => {
    const ent = ENTITLEMENTS[entitlementId];
    if (!ent) return true;  // Unknown = ungated
    return tierLevel($tier) >= tierLevel(ent.tier);
  });
}

export function getLimit(entitlementId: string): Readable<number | null> {
  return derived(currentTier, $tier => {
    // Return tier-specific limit or null (unlimited)
  });
}

Gating Components

A reusable Svelte wrapper for gated UI elements:

<!-- ProGate.svelte -->
<script>
  export let entitlement;  // e.g., 'chat.unlimited'
  const allowed = canUse(entitlement);
</script>

{#if $allowed}
  <slot />
{:else}
  <slot name="locked">
    <div class="pro-gate">
      <span class="pro-badge">Pro</span>
      <span class="gate-label">{getEntitlement(entitlement).label}</span>
    </div>
  </slot>
{/if}

Usage:

<ProGate entitlement="usage.analytics">
  <UsageAnalyticsPanel />
  <div slot="locked">
    <UpgradeNudge feature="Usage Analytics" />
  </div>
</ProGate>

Quantity Limits

Some features may be free up to a limit:

Feature Free Pro
Mappings per mode 10? Unlimited
Chat messages/day 20? Unlimited
LLM providers 1 (default) All 5
Modes 3? Unlimited
Saved profiles 2? Unlimited

(Exact limits TBD — product decisions, not engineering ones.)

License Validation

Out of scope for this issue, but the framework should support:

  • Offline-first: Tier determination must work without network. License key validated locally.
  • No phone-home for core functionality: The daemon + TOML config always works regardless of tier (Epic: Settings Persistence Architecture (ADR-017) #559 principle — "daemon + TOML is self-contained")
  • GUI-only gating: Tier restrictions are enforced in the GUI layer only. The daemon has no concept of tiers. A power user with conductorctl gets full capability — that's acceptable.

What This Issue Does NOT Cover

  • Specific tier boundaries (which features are free vs Pro) — product decision
  • Payment integration, license key generation, or account management
  • Cloud sync or server-side entitlement checking
  • Pricing

Related Issues

Priority

P3 — Infrastructure that needs to be in place before any Pro-tier features ship, but no urgency until tier boundaries are defined.

Acceptance Criteria

  • Central entitlement registry with declarative tier assignments
  • Svelte store providing reactive canUse(entitlement) checks
  • ProGate wrapper component for gated UI sections
  • Quantity limit support (e.g., max N mappings in free tier)
  • Locked features show upgrade nudge, not broken UI
  • Moving a feature between tiers requires only a registry change
  • No daemon-side tier awareness — gating is GUI-only
  • Framework works offline (no network dependency for tier checks)

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions