# 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;

<span className="text-sm text-muted-foreground">
  {limits.max_trees === null
    ? `${treeCount} trees`
    : `${treeCount} / ${limits.max_trees} trees`
  }
</span>
```

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 (
    <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
      <AlertCircle className="text-yellow-600" />
      <div>
        <h3>{feature} requires an upgrade</h3>
        <p>{description}</p>
        <Button>Upgrade from {plan}</Button>
      </div>
    </div>
  );
}
```

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 (
    <Button onClick={handleCheckout} disabled={!stripePromise}>
      {stripePromise ? 'Subscribe Now' : 'Coming Soon'}
    </Button>
  );
}
```

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 <migration_number>`
   - 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!

