From fb84bd814489149b95510881b0e8759ecce85f18 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Fri, 6 Feb 2026 21:36:01 -0500 Subject: [PATCH] docs: add subscription tier implementation plan Comprehensive implementation plan for transitioning from team-based to SaaS subscription model with Free/Pro/Team tiers: - Phase 1 (Days 1-3): Database migration in 6 separate migrations - Migration 016: Create accounts, subscriptions, plan_limits, account_invites tables - Migration 017: Add account_id and account_role to users - Migration 018 (critical): Migrate users/teams to accounts - Migration 019: Migrate team_id FKs to account_id on content tables - Migration 020: Add constraints and finalize migration - Migration 021: Drop old team columns and teams table - Phase 2 (Days 4-7): Backend updates - New models: Account, Subscription, PlanLimits, AccountInvites - Refactor permissions system (account_role replaces role/is_team_admin) - Add subscription helpers for feature gating - Update all 25+ endpoints to use account_id - Update test fixtures and fix 61+ tests - Phase 3 (Days 8-10): Frontend updates - Update types (account_id, account_role) - New hooks: useSubscription, updated usePermissions - Account settings page with subscription info - Usage indicators and upgrade prompts - Stripe Checkout button (disabled until ready) - Phase 4 (Days 11-12): Stripe preparation - Install Stripe SDK - Webhook skeleton with event handlers - Code ready to enable when Stripe account created Key features: - Build Stripe-ready but ship free-tier-only initially - Feature branch workflow (feat/subscription-tiers) - Comprehensive rollback plans for each phase - All limits configurable via plan_limits table - 10-12 day timeline with safety checks - Test on production copy before migration Co-Authored-By: Claude Opus 4.6 --- ...-02-06-subscription-tier-implementation.md | 1315 +++++++++++++++++ 1 file changed, 1315 insertions(+) create mode 100644 docs/plans/2026-02-06-subscription-tier-implementation.md diff --git a/docs/plans/2026-02-06-subscription-tier-implementation.md b/docs/plans/2026-02-06-subscription-tier-implementation.md new file mode 100644 index 00000000..7c34ba3d --- /dev/null +++ b/docs/plans/2026-02-06-subscription-tier-implementation.md @@ -0,0 +1,1315 @@ +# Subscription Tier Architecture - Implementation Plan + +**Date:** 2026-02-06 +**Status:** Ready for implementation +**Branch Strategy:** Feature branch (`feat/subscription-tiers`) +**Estimated Duration:** 10-12 days + +--- + +## Context + +ResolutionFlow is transitioning from a flat team-based system to a SaaS subscription model with three tiers (Free, Pro, Team). This is a **foundational architectural change** that affects the core data model, permission system, and user experience. + +### Why This Change Is Needed + +The current system uses `teams` as the multi-tenant boundary with basic RBAC (`role` + `is_team_admin` flags). This was designed for internal/single-team use and lacks: +- Billing/subscription tracking +- Feature gating by plan tier +- Account ownership model +- Scalable permission system for SaaS + +The new architecture enables monetization while maintaining clean separation between **subscription features** (what you can access) and **account roles** (what you can do). + +### Current State Summary + +**Existing:** +- User model: `role`, `is_team_admin`, `team_id` fields +- Team model with FKs from 5 content tables (Tree, StepLibrary, Category, Tag, StepCategory) +- Permission system in `backend/app/core/permissions.py` +- 61 passing integration tests +- Production deployment on Railway (test/dev data only) + +**Missing:** +- No Stripe integration (not even the library) +- No Account/Subscription models +- No plan limits or feature gating +- No multi-path registration flow + +### Key Decisions + +- **Timeline:** ASAP (1-2 weeks) +- **Data:** Only test/dev data exists - safe to drop/recreate DB if needed +- **Scope:** Full 3-tier system (Free + Pro + Team) with Stripe webhook skeleton +- **Stripe:** Build Stripe-ready infrastructure, enable when ready to monetize +- **Migration Strategy:** All existing users → Free tier accounts +- **Limits:** 3 trees, 20 sessions/month for Free (configurable via `plan_limits` table) +- **Development:** Feature branch with thorough testing before merge + +--- + +## Implementation Phases + +### Phase 1: Database Migration (Days 1-3) + +Create new tables and migrate existing data in 6 separate migrations for safety and rollback capability. + +#### Migration 016: Create New Tables + +**File:** `backend/alembic/versions/016_add_subscription_tables.py` + +Create 4 new tables: +1. **accounts** - Billing entity (replaces teams conceptually) + - Columns: id, name, display_code (8-char unique), owner_id, stripe_customer_id, timestamps + - Owner initially nullable (set after user creation) + +2. **subscriptions** - One per account + - Columns: id, account_id (unique FK), stripe_subscription_id, stripe_price_id, plan, billing_interval, status, seat_limit, period dates, cancel flag + - Indexed on account_id and plan + +3. **plan_limits** - Configuration table (not user data) + - Columns: plan (unique), max_trees, max_sessions_per_month, max_users, custom_branding, priority_support, export_formats (JSONB) + - Seed with 3 rows: free (3 trees, 20 sessions), pro (25/200), team (unlimited) + +4. **account_invites** - Team invitation codes + - Columns: id, account_id, invited_by_id, email, code, role, accepted_by_id, expires_at, timestamps + +**Why separate:** Independent tables, no FK dependencies yet, zero impact on existing code. + +**Rollback:** Drop tables in reverse order. + +**Validation:** +```sql +SELECT COUNT(*) FROM plan_limits; -- Should be 3 +SELECT plan, max_trees FROM plan_limits ORDER BY plan; +``` + +--- + +#### Migration 017: Add Columns to Users + +**File:** `backend/alembic/versions/017_add_account_id_to_users.py` + +Add to `users` table: +- `account_id` (UUID, nullable, indexed) +- `account_role` (String, nullable - will become 'owner'/'engineer'/'viewer') + +**Why separate:** Keeps this atomic, allows testing before data migration. + +**Rollback:** Drop columns and index. + +**Validation:** All 61 existing tests should still pass (columns nullable and unused). + +--- + +#### Migration 018: Migrate Users to Accounts (CRITICAL) + +**File:** `backend/alembic/versions/018_migrate_users_to_accounts.py` + +**This is the most critical migration.** Must be idempotent with comprehensive validation. + +**Logic:** + +1. **For each existing team:** + - Create Account (name from team.name, generate 8-char display_code) + - Set owner_id to first team admin (or first user who joined) + - Create free Subscription for that account + - Update all users in that team: set account_id, map role → account_role: + - `is_team_admin=True` → `account_role='owner'` + - `role='engineer'` → `account_role='engineer'` + - `role='viewer'` → `account_role='viewer'` + +2. **For each user without a team:** + - Create personal Account (name: "User's Account") + - Set owner_id to that user + - Create free Subscription + - Update user: set account_id, account_role='owner' (personal accounts default to owner) + +3. **Validate:** + - `SELECT COUNT(*) FROM users WHERE account_id IS NULL` → must be 0 + - `SELECT COUNT(*) FROM accounts` → should equal (teams + teamless users) + - `SELECT COUNT(*) FROM subscriptions` → should equal accounts + +**Display code generation:** Use `secrets.choice()` with charset excluding confusing chars (0, O, I, 1, L). Check uniqueness before inserting. + +**Rollback:** Downgrade clears account_id/account_role, deletes all accounts/subscriptions. + +**Test before production:** Run on a copy of production DB first! + +--- + +#### Migration 019: Migrate Foreign Keys + +**File:** `backend/alembic/versions/019_migrate_team_fks_to_account.py` + +For each table with `team_id`: trees, step_library, tree_categories, tree_tags, step_categories + +1. Add `account_id` column (UUID, nullable, indexed) +2. Migrate data: `UPDATE table SET account_id = (SELECT account_id FROM users WHERE users.team_id = table.team_id LIMIT 1)` +3. Validate: Ensure no rows have team_id but NULL account_id + +**Why separate:** Allows verifying user migration succeeded before touching content tables. + +**Rollback:** Drop account_id columns and indexes. + +--- + +#### Migration 020: Add Constraints + +**File:** `backend/alembic/versions/020_finalize_account_migration.py` + +1. Users table: + - Make account_id NOT NULL + - Make account_role NOT NULL + - Add FK: account_id → accounts.id (CASCADE) + - Add CHECK: account_role IN ('owner', 'engineer', 'viewer') + +2. Content tables: + - Add FK: account_id → accounts.id (CASCADE, nullable OK for global content) + +3. Accounts table: + - Make owner_id NOT NULL + - Add FK: owner_id → users.id (RESTRICT - can't delete owner without transfer) + +**Why last:** Only enforces constraints after data integrity verified. + +**Point of no return:** After this, old team-based code will break. + +--- + +#### Migration 021: Drop Old Columns + +**File:** `backend/alembic/versions/021_drop_old_team_columns.py` + +1. Drop team_id FKs and columns from: trees, step_library, tree_categories, tree_tags, step_categories +2. Drop from users: team_id, is_team_admin, role (replaced by account_role) +3. Drop CHECK constraint on old role field +4. Drop teams table entirely + +**Why last:** Point of no return. Run ONLY after Phase 2 backend code is deployed and tested. + +**Rollback:** Complex - requires recreating teams table and restoring relationships. Keep database backup! + +--- + +### Phase 2: Backend Updates (Days 4-7) + +Update all backend code to use account-based system and add subscription features. + +#### 2.1: Create New Models + +**Files to create:** + +- `backend/app/models/account.py` + - Relationships: owner (User), users (list), subscription (one), trees, categories, tags + - Generated display_code in migration, not model default + +- `backend/app/models/subscription.py` + - Relationship: account (back_populates) + - Properties: is_active, is_paid, days_until_renewal + +- `backend/app/models/plan_limits.py` + - No relationships, pure configuration + - JSONB export_formats field + +- `backend/app/models/account_invite.py` + - Relationships: account, invited_by, accepted_by + - Properties: is_valid, is_expired + +**Files to modify:** + +- `backend/app/models/user.py` + - **Remove:** team_id, is_team_admin, role + - **Add:** account_id (FK, NOT NULL), account_role (String, NOT NULL) + - **Update relationships:** account (via account_id), owned_account (via Account.owner_id) + - **Add properties:** is_account_owner, can_manage_account + - **Add CHECK:** account_role IN ('owner', 'engineer', 'viewer') + +- Update Tree, StepLibrary, Category, Tag, StepCategory: + - Replace team_id → account_id + - Update relationships: account (not team) + +**Critical:** Handle multiple FKs to users table with explicit `foreign_keys` parameters. + +--- + +#### 2.2: Refactor Permissions System + +**File:** `backend/app/core/permissions.py` (major refactor) + +**New role hierarchy:** +```python +ROLE_HIERARCHY = { + "super_admin": 4, + "owner": 3, # NEW - replaces is_team_admin + "engineer": 2, + "viewer": 1, +} +``` + +**Key function updates:** + +```python +def get_effective_role(user: User) -> str: + return "super_admin" if user.is_super_admin else user.account_role + +def can_manage_account(user: User) -> bool: + return user.is_super_admin or user.account_role == "owner" + +def can_edit_tree(user: User, tree: Tree) -> bool: + if user.is_super_admin: return True + if not can_create_content(user): return False + if tree.author_id == user.id: return True + # Account owners can edit any tree in their account + if user.account_role == "owner" and tree.account_id == user.account_id: + return True + return False +``` + +**Replace everywhere:** +- `team_id` → `account_id` +- `is_team_admin` → `account_role == "owner"` +- Add `can_manage_account()` for billing/member management + +--- + +#### 2.3: Add Subscription Helpers + +**File:** `backend/app/core/subscriptions.py` (new) + +Core functions: +```python +async def get_account_subscription(account_id, db) -> Subscription +async def get_plan_limits(plan: str, db) -> PlanLimits +async def get_user_plan_limits(user_id, db) -> PlanLimits + +async def check_tree_limit(account_id, db) -> tuple[bool, Optional[int], int] + # Returns: (can_create, limit, current_count) + +async def check_session_limit(account_id, db) -> tuple[bool, Optional[int], int] + # Counts sessions this month for entire account +``` + +These will be used by endpoints for feature gating. + +--- + +#### 2.4: Update API Dependencies + +**File:** `backend/app/api/deps.py` + +Update: +```python +async def require_engineer_or_admin(current_user): + if current_user.is_super_admin: return current_user + if current_user.account_role in ("owner", "engineer"): return current_user + raise HTTPException(403, "Engineer or admin access required") + +async def require_account_owner(current_user): + if current_user.is_super_admin or current_user.account_role == "owner": + return current_user + raise HTTPException(403, "Account owner access required") +``` + +Add new: +```python +async def get_plan_limits_for_user(current_user, db) -> PlanLimits: + from app.core.subscriptions import get_user_plan_limits + return await get_user_plan_limits(current_user.id, db) + +def require_feature(feature_name: str): + """Factory for feature-gated dependencies.""" + async def checker(limits = Depends(get_plan_limits_for_user)): + if not getattr(limits, feature_name, False): + raise HTTPException(402, "This feature requires a plan upgrade") + return checker +``` + +Use HTTP 402 for subscription blocks (vs 403 for permission blocks). + +--- + +#### 2.5: Update All Endpoints + +**Files to modify:** trees.py, sessions.py, steps.py, categories.py, tags.py, folders.py, admin.py + +**Pattern for creation endpoints:** + +```python +@router.post("/trees", response_model=TreeResponse) +async def create_tree( + tree_data: TreeCreate, + current_user: Annotated[User, Depends(require_engineer_or_admin)], + db: Annotated[AsyncSession, Depends(get_db)], +): + # Check plan limit + from app.core.subscriptions import check_tree_limit + can_create, limit, current = await check_tree_limit(current_user.account_id, db) + + if not can_create: + raise HTTPException(402, f"Tree limit reached ({current}/{limit}). Upgrade your plan.") + + new_tree = Tree( + author_id=current_user.id, + account_id=current_user.account_id, # Changed from team_id + # ... + ) +``` + +**Pattern for access filters:** + +```python +def build_tree_access_filter(current_user: User): + if current_user.is_super_admin: + return sa_true() + + conditions = [ + Tree.is_default == True, + Tree.is_public == True, + Tree.author_id == current_user.id, + ] + + if current_user.account_id: + conditions.append(Tree.account_id == current_user.account_id) + + return or_(*conditions) +``` + +--- + +#### 2.6: Add Account Management Endpoints + +**File:** `backend/app/api/endpoints/accounts.py` (new) + +```python +GET /api/v1/accounts/me → AccountResponse +GET /api/v1/accounts/me/subscription → SubscriptionResponse (includes limits) +GET /api/v1/accounts/me/members → List[User] +PATCH /api/v1/accounts/me/members/{id}/role (owner only) +DELETE /api/v1/accounts/me/members/{id} (owner only) +PATCH /api/v1/accounts/me → Update account name +``` + +Member management requires `require_account_owner` dependency. + +--- + +#### 2.7: Update Registration Endpoint + +**File:** `backend/app/api/endpoints/auth.py` + +```python +@router.post("/register") +async def register(user_data: UserCreate, db): + # Validate invite code (if required) + # Check email uniqueness + + # 1. Create account + display_code = generate_unique_display_code(db) + new_account = Account( + name=f"{user_data.name}'s Account", + display_code=display_code, + owner_id=None # Set after user creation + ) + db.add(new_account) + await db.flush() + + # 2. Create user + new_user = User( + email=user_data.email, + password_hash=get_password_hash(user_data.password), + name=user_data.name, + account_id=new_account.id, + account_role="owner", # Personal accounts default to owner + ) + db.add(new_user) + await db.flush() + + # 3. Set account owner + new_account.owner_id = new_user.id + + # 4. Create free subscription + subscription = Subscription( + account_id=new_account.id, + plan="free", + status="active", + ) + db.add(subscription) + + await db.commit() + return new_user +``` + +--- + +#### 2.8: Add Stripe Webhook Skeleton + +**File:** `backend/app/api/endpoints/webhooks.py` (new) + +```python +@router.post("/stripe") +async def stripe_webhook(request: Request, db: AsyncSession): + if not settings.STRIPE_WEBHOOK_SECRET: + return {"status": "ignored"} # Not configured yet + + # Verify signature + payload = await request.body() + sig = request.headers.get("stripe-signature") + event = stripe.Webhook.construct_event(payload, sig, settings.STRIPE_WEBHOOK_SECRET) + + # Dispatch to handler + handler = WEBHOOK_HANDLERS.get(event['type']) + if handler: + await handler(event, db) + + return {"status": "received"} +``` + +**File:** `backend/app/core/stripe_handlers.py` (new) + +Implement handlers for: +- `checkout.session.completed` - Create/update subscription with Stripe IDs +- `invoice.paid` - Update period dates, set status=active +- `invoice.payment_failed` - Set status=past_due, notify owner +- `customer.subscription.updated` - Sync plan changes +- `customer.subscription.deleted` - Downgrade to free + +Initially these can be stubs that log events. Full implementation when Stripe is enabled. + +**File:** `backend/app/core/config.py` + +Add optional Stripe settings: +```python +STRIPE_SECRET_KEY: Optional[str] = None +STRIPE_PUBLISHABLE_KEY: Optional[str] = None +STRIPE_WEBHOOK_SECRET: Optional[str] = None + +@property +def stripe_enabled(self) -> bool: + return bool(self.STRIPE_SECRET_KEY and self.STRIPE_WEBHOOK_SECRET) +``` + +**Add to requirements.txt:** `stripe` + +--- + +#### 2.9: Update Test Fixtures + +**File:** `backend/tests/conftest.py` + +**Critical updates:** + +```python +@pytest.fixture +async def test_account(test_db): + """Create account with free subscription.""" + account = Account( + name="Test Account", + display_code=generate_unique_code(), + owner_id=None + ) + test_db.add(account) + await test_db.flush() + + subscription = Subscription( + account_id=account.id, + plan="free", + status="active", + ) + test_db.add(subscription) + await test_db.commit() + return account + +@pytest.fixture +async def test_user(test_db, test_account): + """Create user linked to test account.""" + user = User( + email="test@example.com", + password_hash=get_password_hash("TestPassword123!"), + name="Test User", + account_id=test_account.id, + account_role="owner" + ) + test_db.add(user) + await test_db.flush() + + test_account.owner_id = user.id + await test_db.commit() + return user + +@pytest.fixture +async def test_admin(test_db): + """Create super admin.""" + # Create admin account + user with is_super_admin=True +``` + +**Strategy:** Update fixtures first, then fix failing tests incrementally (one module at a time). + +--- + +### Phase 3: Frontend Updates (Days 8-10) + +Update React app to use account-based API and add subscription UI. + +#### 3.1: Update Type Definitions + +**File:** `frontend/src/types/user.ts` + +```typescript +export interface User { + id: string; + email: string; + name: string; + account_id: string; // Changed from team_id + account_role: 'owner' | 'engineer' | 'viewer'; // Changed from role + is_super_admin: boolean; + is_active: boolean; + created_at: string; + last_login: string | null; +} +``` + +**File:** `frontend/src/types/account.ts` (new) + +```typescript +export interface Account { + id: string; + name: string; + display_code: string; + owner_id: string; + stripe_customer_id: string | null; + created_at: string; + updated_at: string; +} + +export interface Subscription { + plan: 'free' | 'pro' | 'team'; + status: 'active' | 'past_due' | 'canceled' | 'trialing'; + // ... other fields +} + +export interface PlanLimits { + plan: string; + max_trees: number | null; + max_sessions_per_month: number | null; + custom_branding: boolean; + export_formats: string[]; +} + +export interface SubscriptionDetails { + subscription: Subscription; + limits: PlanLimits; +} +``` + +--- + +#### 3.2: Update Auth Store + +**File:** `frontend/src/store/authStore.ts` + +Add to state: +```typescript +account: Account | null; +subscription: SubscriptionDetails | null; + +setUser: (user: User, account: Account, subscription: SubscriptionDetails) => void; +``` + +Persist account and subscription in localStorage. + +--- + +#### 3.3: Create Subscription Hook + +**File:** `frontend/src/hooks/useSubscription.ts` (new) + +```typescript +export function useSubscription() { + const { subscription } = useAuthStore(); + + return { + plan: subscription.subscription.plan, + limits: subscription.limits, + + canUseFeature: (feature: keyof PlanLimits) => Boolean(limits[feature]), + isPaidPlan: plan !== 'free', + isTeamPlan: plan === 'team', + + formatLimit: (resource: 'trees' | 'sessions') => { + const key = resource === 'trees' ? 'max_trees' : 'max_sessions_per_month'; + return limits[key] === null ? 'Unlimited' : String(limits[key]); + } + }; +} +``` + +--- + +#### 3.4: Update usePermissions Hook + +**File:** `frontend/src/hooks/usePermissions.ts` + +```typescript +const effectiveRole = user.is_super_admin ? 'super_admin' : user.account_role; + +return { + canCreateContent: effectiveRole !== 'viewer', + canManageAccount: user.is_super_admin || user.account_role === 'owner', + isAccountOwner: user.account_role === 'owner', + + canEditTree: (tree) => { + if (user.is_super_admin) return true; + if (effectiveRole === 'viewer') return false; + if (tree.author_id === user.id) return true; + if (user.account_role === 'owner' && tree.account_id === user.account_id) return true; + return false; + }, + + canDeleteTree: (tree) => { + if (user.is_super_admin) return true; + return user.account_role === 'owner' && tree.account_id === user.account_id; + }, +}; +``` + +--- + +#### 3.5: Create Account API Client + +**File:** `frontend/src/api/accounts.ts` (new) + +```typescript +export const accountsApi = { + getMyAccount: () => api.get('/api/v1/accounts/me'), + getMySubscription: () => api.get('/api/v1/accounts/me/subscription'), + getMembers: () => api.get('/api/v1/accounts/me/members'), + updateMemberRole: (userId: string, role: 'engineer' | 'viewer') => + api.patch(`/api/v1/accounts/me/members/${userId}/role`, { role }), + removeMember: (userId: string) => + api.delete(`/api/v1/accounts/me/members/${userId}`), +}; +``` + +--- + +#### 3.6: Update Login/Register to Fetch Account Data + +**File:** `frontend/src/api/auth.ts` + +```typescript +export const authApi = { + login: async (credentials) => { + const { data } = await api.post('/api/v1/auth/login/json', credentials); + const { access_token, refresh_token, user } = data; + + // Fetch account and subscription + const [accountRes, subRes] = await Promise.all([ + accountsApi.getMyAccount(), + accountsApi.getMySubscription() + ]); + + return { + access_token, + refresh_token, + user, + account: accountRes.data, + subscription: subRes.data + }; + } +}; +``` + +Update auth store login action to call `setUser(user, account, subscription)`. + +--- + +#### 3.7: Add Account Settings Page + +**File:** `frontend/src/pages/AccountSettingsPage.tsx` (new) + +Sections: +1. **Subscription Info** + - Current plan badge (Free/Pro/Team) + - Usage stats (trees, sessions this month) + - Upgrade button (if free plan) + +2. **Team Members** (only for account owners) + - List members with role dropdowns + - Remove member button + - Generate invite link button + +3. **Billing** (only for paid plans) + - Link to Stripe Customer Portal (when Stripe enabled) + +Use `useSubscription()` and `usePermissions()` hooks. + +**Route:** Add `/settings/account` to router. + +--- + +#### 3.8: Add Usage Indicators + +**File:** `frontend/src/pages/TreeLibraryPage.tsx` + +Near "Create Tree" button, show: +```tsx +const { formatLimit } = useSubscription(); +const treeCount = trees?.length || 0; + + + {limits.max_trees === null + ? `${treeCount} trees` + : `${treeCount} / ${limits.max_trees} trees` + } + +``` + +Disable "Create Tree" button if at limit, show upgrade prompt. + +--- + +#### 3.9: Add Upgrade Prompt Component + +**File:** `frontend/src/components/common/UpgradePrompt.tsx` (new) + +```tsx +export function UpgradePrompt({ feature, description }: Props) { + const { plan } = useSubscription(); + + return ( +
+ +
+

{feature} requires an upgrade

+

{description}

+ +
+
+ ); +} +``` + +Use when API returns 402 Payment Required. + +--- + +#### 3.10: Add Stripe Checkout Button (Disabled) + +**File:** `frontend/src/components/subscription/CheckoutButton.tsx` (new) + +```tsx +const stripePromise = import.meta.env.VITE_STRIPE_KEY + ? loadStripe(import.meta.env.VITE_STRIPE_KEY) + : null; + +export function CheckoutButton({ plan, interval }) { + const handleCheckout = async () => { + if (!stripePromise) { + alert('Payment processing coming soon!'); + return; + } + + // TODO: Create Checkout Session and redirect + }; + + return ( + + ); +} +``` + +Shows "Coming Soon" until `VITE_STRIPE_KEY` is set. + +**Add to `.env.local.example`:** +``` +VITE_STRIPE_KEY=pk_test_... # Optional - only needed for paid plans +``` + +--- + +### Phase 4: Stripe Preparation (Days 11-12) + +Set up Stripe infrastructure without requiring active billing. + +#### 4.1: Install Stripe Libraries + +**Backend:** +```bash +pip install stripe +pip freeze > requirements.txt +``` + +**Frontend:** +```bash +npm install @stripe/stripe-js @stripe/react-stripe-js +``` + +--- + +#### 4.2: Stripe Dashboard Setup (Manual) + +When ready to enable billing: + +1. **Create Products:** + - ResolutionFlow Pro + - Monthly price (note price_id: `price_pro_monthly`) + - Annual price (note price_id: `price_pro_annual`) + - ResolutionFlow Team + - Monthly tiers (5 users, 15 users, 50 users) + - Annual tiers + +2. **Configure Webhook:** + - Endpoint URL: `https://api.resolutionflow.com/api/v1/webhooks/stripe` + - Select events: checkout.session.completed, invoice.paid, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted + - Note webhook signing secret → `STRIPE_WEBHOOK_SECRET` + +3. **Customer Portal:** + - Enable self-service billing management + - Configure allowed actions (cancel, change plan, update payment method) + +--- + +#### 4.3: Environment Variables + +**Backend `.env` (production):** +```bash +STRIPE_SECRET_KEY=sk_live_... +STRIPE_WEBHOOK_SECRET=whsec_... +``` + +**Frontend `.env.production`:** +```bash +VITE_STRIPE_KEY=pk_live_... +``` + +Leave empty until ready to enable billing. Code handles missing keys gracefully. + +--- + +## Testing Strategy + +### Phase 1: Database Migration Testing + +After each migration: +```bash +# Run migration +alembic upgrade head + +# Verify schema +docker exec -it patherly_postgres psql -U postgres -d patherly -c "\d users" +docker exec -it patherly_postgres psql -U postgres -d patherly -c "\d accounts" + +# Verify data integrity +docker exec -it patherly_postgres psql -U postgres -d patherly -c " + SELECT COUNT(*) FROM users WHERE account_id IS NULL; + SELECT COUNT(*) FROM accounts; + SELECT COUNT(*) FROM subscriptions; + SELECT plan, COUNT(*) FROM subscriptions GROUP BY plan; +" + +# Run backend tests (should pass even before code changes for migrations 16-17) +cd backend +pytest --override-ini="addopts=" +``` + +**Test migration 018 on production copy:** +```bash +# Dump production data +docker exec patherly_postgres pg_dump -U postgres patherly > prod_backup.sql + +# Load into test DB +docker exec -i patherly_postgres psql -U postgres -c "CREATE DATABASE patherly_migration_test" +docker exec -i patherly_postgres psql -U postgres patherly_migration_test < prod_backup.sql + +# Run migration 018 on test DB +# Edit DATABASE_URL to point to patherly_migration_test +alembic upgrade 018 + +# Verify results, then drop test DB when satisfied +``` + +--- + +### Phase 2: Backend Testing + +**Incremental test fixing:** +```bash +# Fix test fixtures first +pytest tests/conftest.py -v + +# Fix tests one module at a time +pytest tests/test_auth.py -v +pytest tests/test_trees.py -v +pytest tests/test_sessions.py -v +pytest tests/test_admin.py -v +pytest tests/test_permissions.py -v + +# Full suite +pytest --override-ini="addopts=" +``` + +**Add new tests:** +- `tests/test_subscription_limits.py` - Tree/session limit enforcement, 402 responses +- `tests/test_account_management.py` - Member role changes, account owner permissions +- `tests/test_permissions_account.py` - New account-role-based permission checks + +**Goal:** 61+ passing tests before merging to main. + +--- + +### Phase 3: Frontend Testing + +**Type checking:** +```bash +cd frontend +npm run type-check # or npx tsc --noEmit +``` + +**Manual test checklist:** +- [ ] Login with migrated user sees account info +- [ ] Settings page shows subscription (Free plan, 3/3 trees) +- [ ] Tree library shows usage count (X / Y trees) +- [ ] Creating tree at limit shows 402 error → upgrade prompt +- [ ] Account owner can see team members +- [ ] Account owner can change member role (engineer ↔ viewer) +- [ ] Viewer cannot create trees (403 error) +- [ ] Viewer can browse trees and start sessions +- [ ] Super admin bypasses all checks +- [ ] Logout and re-login preserves account/subscription in store + +--- + +### Phase 4: Stripe Testing + +**Test mode validation:** +```bash +# Use Stripe test keys +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_test_... +``` + +**Webhook testing with Stripe CLI:** +```bash +stripe listen --forward-to localhost:8000/api/v1/webhooks/stripe + +# Trigger test events +stripe trigger checkout.session.completed +stripe trigger invoice.paid +stripe trigger customer.subscription.deleted +``` + +Verify: +- [ ] Webhook endpoint receives events +- [ ] Signature verification works +- [ ] Events are logged (even if handlers are stubs) +- [ ] Invalid signature returns 400 + +--- + +## Deployment Strategy + +### Development (Feature Branch) + +```bash +# Create feature branch +git checkout -b feat/subscription-tiers + +# Commit each phase incrementally +git add backend/alembic/versions/016_*.py +git commit -m "feat: add subscription tables (migration 016)" + +git add backend/alembic/versions/017_*.py +git commit -m "feat: add account_id to users (migration 017)" + +# ... continue for each phase +``` + +**Keep commits atomic:** Each migration in its own commit for easy rollback. + +--- + +### Staging/Preview (Railway PR Environment) + +1. **Open PR:** `feat/subscription-tiers` → `main` +2. Railway auto-creates preview environment +3. **Generate domains** for PR services in Railway dashboard +4. **Run migrations** on PR database (happens automatically via `releaseCommand`) +5. **Test thoroughly** using preview URL +6. **QA checklist:** + - [ ] Migrations ran successfully (check Railway logs) + - [ ] Backend tests pass in CI + - [ ] Frontend builds without errors + - [ ] Manual smoke test: register new user → see Free account → browse trees + - [ ] Webhook endpoint returns 200 (even with no Stripe config) + +--- + +### Production Deployment + +**Prerequisites:** +- [ ] All tests passing in CI +- [ ] Manual QA complete on PR environment +- [ ] Database backup created +- [ ] Rollback plan reviewed + +**Steps:** + +1. **Merge PR:** `feat/subscription-tiers` → `main` +2. Railway auto-deploys to production +3. Migrations run automatically (via `releaseCommand` in railway.json) +4. **Monitor deployment:** + - Check Railway logs for migration success + - Watch for errors in backend logs + - Test login/registration immediately + +5. **Smoke test:** + - [ ] Login as existing user → verify account_id populated + - [ ] View trees → verify account_id filter works + - [ ] Create tree → verify limit enforcement (if at limit) + - [ ] Check settings page → see Free plan, usage stats + +6. **Rollback if needed:** + - Railway: Redeploy previous version + - Database: `alembic downgrade ` + - Restore from backup if migration cannot be reversed + +**Expected downtime:** <1 minute (time for migrations to run) + +--- + +## Rollback Plans + +### Rollback After Migration 016-018 (Safe) + +```bash +# Downgrade migrations one at a time +alembic downgrade -1 # Undo 018 +alembic downgrade -1 # Undo 017 +alembic downgrade -1 # Undo 016 + +# Verify rollback +docker exec -it patherly_postgres psql -U postgres -d patherly -c "\dt" +# Should NOT see: accounts, subscriptions, plan_limits, account_invites +# users.account_id should be NULL for all rows + +# Run tests to confirm +pytest --override-ini="addopts=" +``` + +--- + +### Rollback After Migration 019-020 (Careful) + +```bash +alembic downgrade 018 # Undo constraints and FK migration + +# Manually verify +docker exec -it patherly_postgres psql -U postgres -d patherly -c " + SELECT column_name, is_nullable + FROM information_schema.columns + WHERE table_name = 'users' AND column_name IN ('account_id', 'account_role'); +" +# account_id should be nullable again +``` + +--- + +### Rollback After Migration 021 (Point of No Return) + +**Cannot easily rollback** - teams table is dropped. + +**Options:** +1. Restore from database backup (recommended) +2. Manually recreate teams table and reverse-migrate data (complex, error-prone) + +**Prevention:** Only run migration 021 AFTER Phase 2 backend code is deployed, tested, and stable in production for at least 24 hours. + +--- + +## Risk Assessment + +### High-Risk Items + +| Risk | Impact | Mitigation | +|------|--------|------------| +| **Migration 018 data loss** | High - could orphan users/content | Test on production copy first, comprehensive validation in migration code | +| **Permission system breaks access** | High - users locked out of content | Thorough test coverage, manual QA with different roles | +| **Test suite breakage** | Medium - deployment blocked | Fix fixtures first, update tests incrementally | +| **Circular FK deadlock** | Medium - migration fails | Keep owner_id nullable until after user creation | + +### Medium-Risk Items + +| Risk | Impact | Mitigation | +|------|--------|------------| +| **Stripe webhook downtime** | Medium - subscriptions not updated | Webhook endpoint logs events even if handlers fail | +| **Frontend type errors** | Medium - build fails | TypeScript strict mode, incremental type updates | +| **Plan limit edge cases** | Low - users blocked unexpectedly | Conservative limits initially, easy to adjust via DB | + +### Low-Risk Items + +- Stripe integration (disabled by default, no impact until enabled) +- UI changes (progressive enhancement, no breaking changes) +- Account settings page (new page, doesn't affect existing flows) + +--- + +## Critical Files + +These files are the foundation of the implementation. Get these right and the rest follows: + +1. **`backend/alembic/versions/018_migrate_users_to_accounts.py`** + - Most critical migration + - Handles data transformation from teams → accounts + - Must be idempotent and thoroughly tested on production copy + +2. **`backend/app/models/user.py`** + - Core model change + - Affects all relationships and permission checks + - Breaking change point + +3. **`backend/app/core/permissions.py`** + - Permission system refactor + - Every endpoint depends on this + - Must handle account_role correctly + +4. **`backend/app/core/subscriptions.py`** + - Feature gating logic + - Used by all content creation endpoints + - Pattern for limit enforcement + +5. **`backend/tests/conftest.py`** + - Test fixture foundation + - Updating correctly ensures incremental test fixing + - Prevents catastrophic test breakage + +6. **`frontend/src/types/user.ts` + `account.ts`** + - Type foundation for frontend + - Breaking change for all components using User type + - Must match backend schemas exactly + +--- + +## Success Criteria + +### Minimum Viable Implementation + +- [ ] All 6 migrations run successfully +- [ ] All 61+ backend tests passing +- [ ] Frontend builds without type errors +- [ ] Can register new user → creates account + free subscription +- [ ] Can login → sees account info in settings +- [ ] Tree creation enforces limits (shows 402 at limit) +- [ ] Account owners can manage team members +- [ ] Viewers are blocked from content creation +- [ ] Super admins bypass all checks +- [ ] Webhook endpoint exists and responds 200 + +### Production Ready + +- [ ] Deployed to Railway production +- [ ] Existing users migrated to accounts successfully +- [ ] All features working as expected +- [ ] No permission leaks or access control issues +- [ ] Monitoring in place for errors +- [ ] Rollback plan tested and documented + +### Stripe Ready (Future) + +- [ ] Stripe products/prices created +- [ ] Webhook handlers fully implemented +- [ ] Checkout flow tested in test mode +- [ ] Customer portal configured +- [ ] Production keys added to environment + +--- + +## Post-Implementation + +After successful deployment: + +1. **Monitor for 48 hours:** + - Watch error logs for permission issues + - Track failed tree creation attempts (limits) + - Monitor webhook endpoint for spam/attacks + +2. **User communication:** + - Notify users of new account system + - Explain free tier limits + - Link to upgrade page (when ready) + +3. **Documentation updates:** + - Update README with new architecture + - Document account management for team admins + - Add subscription tier comparison table + +4. **Future enhancements:** + - Usage dashboard (current vs limits) + - Email notifications for limit warnings + - Team invite flow UI + - Multi-path registration (start free / start pro / join team) + - Stripe Checkout integration + +--- + +## Timeline Summary + +| Phase | Duration | Milestone | +|-------|----------|-----------| +| Phase 1: Database Migration | 2-3 days | All migrations pass, data migrated | +| Phase 2: Backend Updates | 3-4 days | All tests passing, endpoints updated | +| Phase 3: Frontend Updates | 2-3 days | TypeScript errors fixed, UI complete | +| Phase 4: Stripe Prep | 1-2 days | Webhook skeleton, test mode working | +| **Total** | **10-12 days** | **Production deployment** | + +Add 1-2 days buffer for unexpected issues, QA, and deployment. + +--- + +## Verification Checklist + +Before merging to main: + +**Database:** +- [ ] All 6 migrations run successfully in order +- [ ] Rollback tested for migrations 016-018 +- [ ] Data integrity verified (no NULL account_id) +- [ ] plan_limits table seeded with 3 rows + +**Backend:** +- [ ] 61+ tests passing +- [ ] New tests added for subscriptions and limits +- [ ] All endpoints use account_id (not team_id) +- [ ] Permissions use account_role (not role/is_team_admin) +- [ ] 402 returned when hitting plan limits +- [ ] Webhook endpoint responds to Stripe events + +**Frontend:** +- [ ] TypeScript compiles with no errors +- [ ] All components use account_id +- [ ] usePermissions hook updated +- [ ] useSubscription hook works +- [ ] Settings page shows subscription info +- [ ] Usage indicators display correctly +- [ ] Upgrade prompts show on 402 errors + +**Integration:** +- [ ] Can register new user +- [ ] Can login and see account info +- [ ] Can create trees (up to limit) +- [ ] Cannot create tree at limit (shows upgrade prompt) +- [ ] Account owner can manage members +- [ ] Viewer role blocked from creation +- [ ] Super admin bypasses everything + +--- + +## Notes + +- **Keep this plan updated** as implementation progresses +- **Document deviations** if approach changes during implementation +- **Link commits** back to plan phases for traceability +- **Celebrate milestones** - this is a major architectural upgrade! +