Subscription System Developer
Context Files (Read First)
For schema and plan details, read from Docs/context/:
Docs/context/db-schema-short.md- Quota tables and structuresDocs/context/supabase-map.md- RPC functions for quotas
Capabilities
- Design and implement feature gating with plan-based access
- Create database migrations for quota tables and usage tracking
- Build React hooks for subscription state management
- Implement Edge Function quota enforcement
- Create upgrade modal flows and UI components
- Extend quota system for new feature types (tokens, counts, size limits)
Reference Documentation
Primary: Docs/13-SUBSCRIPTION-SYSTEM.md
Shared Package Architecture
All subscription logic lives in packages/shared-subscription/ and is imported via the @shared-subscription alias.
Package Structure
packages/shared-subscription/src/
├── components/
│ ├── PlanBadge.tsx # Plan tier badge
│ ├── PremiumTag.tsx # Premium feature indicator (Crown icon + upgrade dialog)
│ ├── PlansComparison.tsx # Plans comparison cards
│ ├── UpgradeModal.tsx # Upgrade flow modal
│ └── useSubscriptionOptional.ts # Optional context hook (no provider required)
├── context/
│ ├── SubscriptionContext.tsx # React context definition
│ └── SubscriptionProvider.tsx # Context provider (wraps app)
├── lib/
│ ├── cn.ts # Utility
│ └── stripe-mock.ts # Mock Stripe utilities
├── index.ts # Public API
└── types.ts # PlanKey, UsageInfo, PlanInfo, etc.
Import Pattern
// CORRECT — use @shared-subscription alias
import { SubscriptionProvider } from "@shared-subscription/context/SubscriptionProvider";
import { PremiumTag } from "@shared-subscription/components/PremiumTag";
import { UpgradeModal } from "@shared-subscription/components/UpgradeModal";
import type { PlanKey } from "@shared-subscription/types";
// WRONG — old paths, do NOT use
// import type { PlanKey } from "@/components/ai-quota";
// import { UpgradeModal } from "@/components/ai-quota";
SubscriptionProvider Pattern
Wrap the app (or a subtree) with SubscriptionProvider to make subscription state available:
<SubscriptionProvider
plan={userPlan} // PlanKey: unauth | basic | pro | premium | admin
usage={usageInfo} // UsageInfo | null
plans={availablePlans} // PlanInfo[]
loading={isLoading}
canUseFeature={(key, tokens?) => boolean} // Feature gating function
onNavigateToPlans={handleNavigate}
onNavigateToAuth={handleAuth}
>
{children}
</SubscriptionProvider>
The provider manages upgrade modal state internally and renders <UpgradeModal> as a portal.
PremiumTag Component
Mark premium features in UI with a Crown icon:
<PremiumTag size="md" label={true} featureName="Plan from Summary" />
- Premium/Admin users: Shows subtle amber Crown icon only
- Non-premium users: Clickable Crown that opens upgrade dialog with CTA
- Admin-configurable promo text via
setPremiumTagConfig(config)
Gated Feature Example: plan-from-summary
-- Feature key: 'plan-from-summary'
-- Premium only: 3 plans per 6-hour window
INSERT INTO bible_schema.ai_plan_quotas (plan_key, feature_key, tokens_per_window, enabled) VALUES
('unauth', 'plan-from-summary', 0, false),
('basic', 'plan-from-summary', 0, false),
('pro', 'plan-from-summary', 0, false),
('premium', 'plan-from-summary', 3, true),
('admin', 'plan-from-summary', 999999, true);
Architecture Overview
Plan Tiers
| Plan | Target | AI Tokens | Feature Access |
|------|--------|-----------|----------------|
| unauth | Guests | 2,000 | Very limited |
| basic | Free users | 10,000 | Moderate |
| pro | Paid | 50,000 | Full |
| premium | Top tier | 200,000 | Full + extras |
| admin | Admins | Unlimited | All |
Limit Types
| Type | Window | Example |
|------|--------|---------|
| tokens | rolling | AI features (6-hour window) |
| count | lifetime | Max notes per user |
| count | monthly | PDF exports per month |
| size | none | Max content length |
| boolean | none | Feature on/off |
Usage Examples
Three complete, copy-pasteable end-to-end examples (migration + RPC + hook +
component) live in references/examples.md — read that file when
implementing a new gated feature:
- Add a feature with a count limit (user notes, 50/plan) — full table,
RLS,
can_create_noteRPC,useUserNoteshook, component wiring. - Add a notification channel (WhatsApp for pro) — preferences/queue
tables,
can_use_notification_channelRPC, Twilio edge function. - Extend the quota table for new limit types —
limit_type/window_type/size_limitcolumns + unifiedcan_use_featureRPC.
All three follow the same shape: quota rows for all 5 plans → feature RPC
returning {allowed, reason, upgrade_suggestion} → hook exposing
checkOrPrompt → component calls checkOrPrompt() then acts.
Unified Pattern: checkOrPrompt
The recommended pattern for all gated features:
// Hook provides checkOrPrompt function
const { checkOrPrompt } = useFeatureHook();
// In component - single call handles everything:
// 1. Checks if feature allowed
// 2. Shows appropriate modal if denied
// 3. Returns boolean for flow control
const handleAction = async () => {
if (!await checkOrPrompt()) return; // Modal shown automatically
// Proceed with action
await performAction();
};
This pattern:
- Reduces boilerplate in components
- Ensures consistent upgrade flow
- Handles all deny reasons (auth, locked, quota, limit)
- Works for all feature types
Updating the Plans/Pricing Page (Tilaussivu)
When adding or changing features per plan, update three places:
1. PlansComparison component
File: apps/raamattu-nyt/src/components/ai-quota/PlansComparison.tsx
The PLAN_DETAILS array defines features shown per plan via i18n keys:
// Add new feature key to the relevant plan's featureKeys array
{
key: "pro",
featureKeys: [
"plans.pro.features.existingFeature",
"plans.pro.features.newFeature", // Add here
],
}
2. Translation files (fi + en)
- FI:
apps/raamattu-nyt/public/locales/fi/common.json→plans.<tier>.features.<key> - EN:
apps/raamattu-nyt/public/locales/en/common.json→ same path
Add the translation for the new feature key under the correct plan tier:
"features": {
"existingFeature": "Olemassa oleva ominaisuus",
"newFeature": "Uusi ominaisuus"
}
3. PlansPage
File: apps/raamattu-nyt/src/pages/PlansPage.tsx
Usually no changes needed — it renders PlansComparison. Only update if adding new sections (FAQ items, hero text, etc.).
Pages using PlansComparison: PlansPage, AccountPlanPage, UpgradeModal, CheckoutPage
Key Files
| File | Purpose |
|------|---------|
| Docs/13-SUBSCRIPTION-SYSTEM.md | Full system documentation |
| supabase/migrations/20260107180305_*.sql | AI quota core schema |
| supabase/migrations/20260107180306_*.sql | Quota RPC functions |
| packages/shared-subscription/src/ | Shared subscription package (SubscriptionProvider, PremiumTag, UpgradeModal) |
| apps/raamattu-nyt/src/hooks/useAIQuota.ts | AI quota hook (reference) |
| apps/raamattu-nyt/src/components/ai-quota/ | UI components (legacy — prefer @shared-subscription imports) |
| packages/shared-subscription/src/components/PlansComparison.tsx | Plan feature lists + pricing cards |
| apps/raamattu-nyt/public/locales/fi/common.json | FI translations (plans.*) |
| apps/raamattu-nyt/public/locales/en/common.json | EN translations (plans.*) |
| packages/shared-auth/ | Shared auth hooks |
Checklist for New Features
- [ ] Add quota entries to
ai_plan_quotasfor all 5 plans - [ ] Create RPC function for feature-specific checks
- [ ] Build React hook with
checkOrPromptpattern - [ ] Integrate UpgradeModal in component
- [ ] Add feature to
PlansComparisonPLAN_DETAILSfeatureKeys - [ ] Add feature translation to
fi/common.jsonanden/common.jsonunderplans.<tier>.features - [ ] Update admin panel if needed
- [ ] Add feature to
13-SUBSCRIPTION-SYSTEM.md - [ ] Test all plan tiers (unauth, basic, pro, premium, admin)
Related Skills
supabase-migration-writer- Database migrationsedge-function-generator- Edge Functionsadmin-panel-builder- Admin UIrls-policy-validator- RLS security