|
| 1 | +# User Tier & Limits System - Design Document |
| 2 | + |
| 3 | +> **Issues**: #93, #94, #95, #96, #97 |
| 4 | +> **Author**: Devanshu |
| 5 | +> **Status**: Draft |
| 6 | +> **Last Updated**: 2024-12-13 |
| 7 | +
|
| 8 | +--- |
| 9 | + |
| 10 | +## 1. Problem Statement |
| 11 | + |
| 12 | +CodeIntel needs a tiered system to: |
| 13 | +1. **Protect costs** - Indexing is expensive ($0.02-$50/repo depending on size) |
| 14 | +2. **Enable growth** - Freemium model with upgrade path |
| 15 | +3. **Prevent abuse** - Rate limit anonymous playground users |
| 16 | + |
| 17 | +**Key Insight**: Searching is nearly free ($0.000001/query). Indexing is the real cost driver. |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## 2. Tier Definitions |
| 22 | + |
| 23 | +| Tier | Max Repos | Files/Repo | Functions/Repo | Playground/Day | |
| 24 | +|------|-----------|------------|----------------|----------------| |
| 25 | +| **Free** | 3 | 500 | 2,000 | 50 | |
| 26 | +| **Pro** | 20 | 5,000 | 20,000 | Unlimited | |
| 27 | +| **Enterprise** | Unlimited | 50,000 | 200,000 | Unlimited | |
| 28 | + |
| 29 | +**Rationale**: |
| 30 | +- Free tier: Enough for personal projects, not enterprise codebases |
| 31 | +- Playground limit: 50/day is generous (anti-abuse, not business gate) |
| 32 | +- File/function limits: Prevent expensive indexing jobs |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## 3. Current API Endpoints |
| 37 | + |
| 38 | +### 3.1 Authentication (`/api/v1/auth`) |
| 39 | +| Method | Endpoint | Auth | Description | |
| 40 | +|--------|----------|------|-------------| |
| 41 | +| POST | `/signup` | None | Create account | |
| 42 | +| POST | `/login` | None | Get JWT | |
| 43 | +| POST | `/refresh` | JWT | Refresh token | |
| 44 | +| POST | `/logout` | JWT | Invalidate session | |
| 45 | +| GET | `/me` | JWT | Get current user | |
| 46 | + |
| 47 | +### 3.2 Repositories (`/api/v1/repos`) |
| 48 | +| Method | Endpoint | Auth | Description | **Limits Check** | |
| 49 | +|--------|----------|------|-------------|------------------| |
| 50 | +| GET | `/` | JWT | List user repos | - | |
| 51 | +| POST | `/` | JWT | Add repo | **#95: Check repo count** | |
| 52 | +| POST | `/{id}/index` | JWT | Index repo | **#94: Check file/function count** | |
| 53 | + |
| 54 | +### 3.3 Search (`/api/v1/search`) |
| 55 | +| Method | Endpoint | Auth | Description | **Limits Check** | |
| 56 | +|--------|----------|------|-------------|------------------| |
| 57 | +| POST | `/search` | JWT | Search code | - | |
| 58 | +| POST | `/explain` | JWT | Explain code | - | |
| 59 | + |
| 60 | +### 3.4 Playground (`/api/v1/playground`) - **Anonymous** |
| 61 | +| Method | Endpoint | Auth | Description | **Limits Check** | |
| 62 | +|--------|----------|------|-------------|------------------| |
| 63 | +| GET | `/repos` | None | List demo repos | - | |
| 64 | +| POST | `/search` | None | Search demo repos | **#93: Rate limit 50/day** | |
| 65 | + |
| 66 | +### 3.5 Analysis (`/api/v1/analysis`) |
| 67 | +| Method | Endpoint | Auth | Description | |
| 68 | +|--------|----------|------|-------------| |
| 69 | +| GET | `/{id}/dependencies` | JWT | Dependency graph | |
| 70 | +| POST | `/{id}/impact` | JWT | Impact analysis | |
| 71 | +| GET | `/{id}/insights` | JWT | Repo insights | |
| 72 | +| GET | `/{id}/style-analysis` | JWT | Code style | |
| 73 | + |
| 74 | +### 3.6 Users (`/api/v1/users`) - **NEW** |
| 75 | +| Method | Endpoint | Auth | Description | |
| 76 | +|--------|----------|------|-------------| |
| 77 | +| GET | `/usage` | JWT | Get tier, limits, current usage | |
| 78 | +| GET | `/limits/check-repo-add` | JWT | Pre-check before adding repo | |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## 4. Implementation Plan by Issue |
| 83 | + |
| 84 | +### Issue #96: User Tier System (Foundation) ✅ DONE |
| 85 | +**Files Created**: |
| 86 | +- `backend/services/user_limits.py` - Core service |
| 87 | +- `backend/routes/users.py` - API endpoints |
| 88 | +- `supabase/migrations/001_user_profiles.sql` - DB schema |
| 89 | + |
| 90 | +**Service Methods**: |
| 91 | +```python |
| 92 | +class UserLimitsService: |
| 93 | + async def get_user_tier(user_id) -> UserTier |
| 94 | + async def get_user_limits(user_id) -> TierLimits |
| 95 | + async def get_user_repo_count(user_id) -> int |
| 96 | + async def check_repo_count(user_id) -> LimitCheckResult |
| 97 | + async def check_repo_size(user_id, file_count, func_count) -> LimitCheckResult |
| 98 | + async def get_usage_summary(user_id) -> dict |
| 99 | +``` |
| 100 | + |
| 101 | +### Issue #95: Repo Count Limits |
| 102 | +**Where**: `POST /api/v1/repos` |
| 103 | + |
| 104 | +**Changes to `routes/repos.py`**: |
| 105 | +```python |
| 106 | +@router.post("") |
| 107 | +async def add_repository(request, auth): |
| 108 | + # NEW: Check repo count limit |
| 109 | + result = await user_limits.check_repo_count(auth.user_id) |
| 110 | + if not result.allowed: |
| 111 | + raise HTTPException( |
| 112 | + status_code=403, |
| 113 | + detail={ |
| 114 | + "error": "REPO_LIMIT_REACHED", |
| 115 | + "message": result.message, |
| 116 | + "current": result.current, |
| 117 | + "limit": result.limit, |
| 118 | + "upgrade_url": "/pricing" # Frontend can use this |
| 119 | + } |
| 120 | + ) |
| 121 | + # ... existing code |
| 122 | +``` |
| 123 | + |
| 124 | +**Frontend Integration**: |
| 125 | +- Call `GET /users/limits/check-repo-add` before showing Add Repo button |
| 126 | +- Show "2/3 repos used" in sidebar |
| 127 | +- Show upgrade prompt when limit reached |
| 128 | + |
| 129 | +### Issue #94: Repo Size Limits |
| 130 | +**Where**: `POST /api/v1/repos/{id}/index` |
| 131 | + |
| 132 | +**Changes to `routes/repos.py`**: |
| 133 | +```python |
| 134 | +@router.post("/{repo_id}/index") |
| 135 | +async def index_repository(repo_id, auth): |
| 136 | + repo = get_repo_or_404(repo_id, auth.user_id) |
| 137 | + |
| 138 | + # Count files and estimate functions BEFORE indexing |
| 139 | + file_count = count_code_files(repo["local_path"]) |
| 140 | + estimated_functions = file_count * 25 # Conservative estimate |
| 141 | + |
| 142 | + # NEW: Check size limits |
| 143 | + result = await user_limits.check_repo_size( |
| 144 | + auth.user_id, file_count, estimated_functions |
| 145 | + ) |
| 146 | + if not result.allowed: |
| 147 | + raise HTTPException( |
| 148 | + status_code=400, |
| 149 | + detail={ |
| 150 | + "error": "REPO_TOO_LARGE", |
| 151 | + "message": result.message, |
| 152 | + "file_count": file_count, |
| 153 | + "limit": result.limit, |
| 154 | + "tier": (await user_limits.get_user_tier(auth.user_id)).value |
| 155 | + } |
| 156 | + ) |
| 157 | + # ... existing indexing code |
| 158 | +``` |
| 159 | + |
| 160 | +### Issue #93: Playground Rate Limiting |
| 161 | +**Where**: `POST /api/v1/playground/search` |
| 162 | + |
| 163 | +**New File**: `backend/services/playground_rate_limiter.py` |
| 164 | +```python |
| 165 | +class PlaygroundRateLimiter: |
| 166 | + def __init__(self, redis_client): |
| 167 | + self.redis = redis_client |
| 168 | + self.daily_limit = 50 |
| 169 | + |
| 170 | + async def check_and_increment(self, ip: str) -> tuple[bool, dict]: |
| 171 | + """Returns (allowed, headers_dict)""" |
| 172 | + key = f"playground:rate:{ip}" |
| 173 | + |
| 174 | + # Atomic increment |
| 175 | + count = self.redis.incr(key) |
| 176 | + if count == 1: |
| 177 | + self.redis.expire(key, 86400) # 24h TTL |
| 178 | + |
| 179 | + ttl = self.redis.ttl(key) |
| 180 | + reset_time = int(time.time()) + ttl |
| 181 | + |
| 182 | + headers = { |
| 183 | + "X-RateLimit-Limit": str(self.daily_limit), |
| 184 | + "X-RateLimit-Remaining": str(max(0, self.daily_limit - count)), |
| 185 | + "X-RateLimit-Reset": str(reset_time) |
| 186 | + } |
| 187 | + |
| 188 | + if count > self.daily_limit: |
| 189 | + headers["Retry-After"] = str(ttl) |
| 190 | + return False, headers |
| 191 | + |
| 192 | + return True, headers |
| 193 | +``` |
| 194 | + |
| 195 | +**Changes to `routes/playground.py`**: |
| 196 | +```python |
| 197 | +from fastapi import Request, Response |
| 198 | + |
| 199 | +@router.post("/search") |
| 200 | +async def playground_search(request: Request, response: Response, body: SearchRequest): |
| 201 | + # Get client IP |
| 202 | + ip = request.client.host |
| 203 | + forwarded = request.headers.get("X-Forwarded-For") |
| 204 | + if forwarded: |
| 205 | + ip = forwarded.split(",")[0].strip() |
| 206 | + |
| 207 | + # Check rate limit |
| 208 | + allowed, headers = await playground_rate_limiter.check_and_increment(ip) |
| 209 | + |
| 210 | + # Always add headers |
| 211 | + for key, value in headers.items(): |
| 212 | + response.headers[key] = value |
| 213 | + |
| 214 | + if not allowed: |
| 215 | + raise HTTPException( |
| 216 | + status_code=429, |
| 217 | + detail={ |
| 218 | + "error": "RATE_LIMIT_EXCEEDED", |
| 219 | + "message": "Daily search limit reached. Sign up for unlimited searches!", |
| 220 | + "limit": 50, |
| 221 | + "reset": headers["X-RateLimit-Reset"] |
| 222 | + } |
| 223 | + ) |
| 224 | + |
| 225 | + # ... existing search code |
| 226 | +``` |
| 227 | + |
| 228 | +### Issue #97: Progressive Signup CTAs |
| 229 | +**Where**: Frontend only |
| 230 | + |
| 231 | +**Implementation**: |
| 232 | +```typescript |
| 233 | +// hooks/usePlaygroundUsage.ts |
| 234 | +const usePlaygroundUsage = () => { |
| 235 | + const [searchCount, setSearchCount] = useState(0); |
| 236 | + |
| 237 | + // Read from response headers after each search |
| 238 | + const trackSearch = (response: Response) => { |
| 239 | + const remaining = response.headers.get('X-RateLimit-Remaining'); |
| 240 | + const limit = response.headers.get('X-RateLimit-Limit'); |
| 241 | + if (remaining && limit) { |
| 242 | + setSearchCount(parseInt(limit) - parseInt(remaining)); |
| 243 | + } |
| 244 | + }; |
| 245 | + |
| 246 | + return { searchCount, trackSearch }; |
| 247 | +}; |
| 248 | + |
| 249 | +// Show CTAs at thresholds |
| 250 | +// 10 searches: Subtle "Want to search YOUR codebase?" |
| 251 | +// 25 searches: More prominent with feature list |
| 252 | +// 40 searches: Final "You clearly love this" |
| 253 | +``` |
| 254 | + |
| 255 | +--- |
| 256 | + |
| 257 | +## 5. Error Response Format |
| 258 | + |
| 259 | +All limit-related errors follow this format: |
| 260 | + |
| 261 | +```json |
| 262 | +{ |
| 263 | + "detail": { |
| 264 | + "error": "ERROR_CODE", |
| 265 | + "message": "Human readable message", |
| 266 | + "current": 3, |
| 267 | + "limit": 3, |
| 268 | + "tier": "free", |
| 269 | + "upgrade_url": "/pricing" |
| 270 | + } |
| 271 | +} |
| 272 | +``` |
| 273 | + |
| 274 | +**Error Codes**: |
| 275 | +| Code | HTTP Status | Description | |
| 276 | +|------|-------------|-------------| |
| 277 | +| `REPO_LIMIT_REACHED` | 403 | Max repos for tier | |
| 278 | +| `REPO_TOO_LARGE` | 400 | File/function count exceeds tier | |
| 279 | +| `RATE_LIMIT_EXCEEDED` | 429 | Playground daily limit | |
| 280 | + |
| 281 | +--- |
| 282 | + |
| 283 | +## 6. Database Schema |
| 284 | + |
| 285 | +### user_profiles (NEW) |
| 286 | +```sql |
| 287 | +CREATE TABLE user_profiles ( |
| 288 | + id UUID PRIMARY KEY, |
| 289 | + user_id UUID REFERENCES auth.users(id), |
| 290 | + tier TEXT DEFAULT 'free', -- 'free', 'pro', 'enterprise' |
| 291 | + created_at TIMESTAMPTZ, |
| 292 | + updated_at TIMESTAMPTZ |
| 293 | +); |
| 294 | +``` |
| 295 | + |
| 296 | +### repositories (existing, no changes needed) |
| 297 | +Already has `user_id` column for ownership. |
| 298 | + |
| 299 | +--- |
| 300 | + |
| 301 | +## 7. Redis Keys |
| 302 | + |
| 303 | +| Key Pattern | TTL | Description | |
| 304 | +|-------------|-----|-------------| |
| 305 | +| `playground:rate:{ip}` | 24h | Playground search count | |
| 306 | +| `user:tier:{user_id}` | 5min | Cached user tier | |
| 307 | + |
| 308 | +--- |
| 309 | + |
| 310 | +## 8. Frontend Integration Points |
| 311 | + |
| 312 | +### Dashboard |
| 313 | +- Show usage bar: "2/3 repositories" |
| 314 | +- Show tier badge: "Free Tier" |
| 315 | +- Upgrade CTA when near limits |
| 316 | + |
| 317 | +### Add Repository Flow |
| 318 | +1. Call `GET /users/limits/check-repo-add` |
| 319 | +2. If `allowed: false`, show upgrade modal |
| 320 | +3. If `allowed: true`, proceed with add |
| 321 | + |
| 322 | +### Playground |
| 323 | +1. Read rate limit headers from search responses |
| 324 | +2. Show remaining searches: "47/50 searches today" |
| 325 | +3. Show progressive CTAs at thresholds |
| 326 | +4. On 429, show signup modal |
| 327 | + |
| 328 | +--- |
| 329 | + |
| 330 | +## 9. Migration Path |
| 331 | + |
| 332 | +### Existing Users |
| 333 | +All existing users default to `free` tier. Migration auto-creates profile on first API call. |
| 334 | + |
| 335 | +### Existing Repos |
| 336 | +No changes needed. Limit checks only apply to NEW repos. |
| 337 | + |
| 338 | +--- |
| 339 | + |
| 340 | +## 10. Implementation Order |
| 341 | + |
| 342 | +| Phase | Issue | Priority | Depends On | |
| 343 | +|-------|-------|----------|------------| |
| 344 | +| 1 | #96 User tier system | P0 | - | ✅ DONE | |
| 345 | +| 2 | #94 Repo size limits | P0 | #96 | |
| 346 | +| 2 | #95 Repo count limits | P0 | #96 | |
| 347 | +| 3 | #93 Playground rate limit | P1 | Redis | |
| 348 | +| 4 | #97 Progressive CTAs | P2 | #93 | |
| 349 | + |
| 350 | +--- |
| 351 | + |
| 352 | +## 11. Open Questions |
| 353 | + |
| 354 | +1. **Upgrade Flow**: Stripe integration? Manual for now? |
| 355 | +2. **Existing Large Repos**: Grandfather them or enforce limits? |
| 356 | +3. **Team/Org Support**: Future consideration for enterprise? |
| 357 | +4. **API Key Users**: Same limits as JWT users? |
| 358 | + |
| 359 | +--- |
| 360 | + |
| 361 | +## 12. Files to Create/Modify |
| 362 | + |
| 363 | +### Create |
| 364 | +- [x] `backend/services/user_limits.py` |
| 365 | +- [x] `backend/routes/users.py` |
| 366 | +- [x] `supabase/migrations/001_user_profiles.sql` |
| 367 | +- [ ] `backend/services/playground_rate_limiter.py` |
| 368 | +- [ ] `frontend/src/hooks/usePlaygroundUsage.ts` |
| 369 | +- [ ] `frontend/src/components/PlaygroundCTA.tsx` |
| 370 | +- [ ] `frontend/src/components/UsageBar.tsx` |
| 371 | + |
| 372 | +### Modify |
| 373 | +- [x] `backend/dependencies.py` |
| 374 | +- [x] `backend/main.py` |
| 375 | +- [ ] `backend/routes/repos.py` - Add limit checks |
| 376 | +- [ ] `backend/routes/playground.py` - Add rate limiting |
| 377 | +- [ ] `frontend/src/pages/Dashboard.tsx` - Show usage |
| 378 | +- [ ] `frontend/src/pages/LandingPage.tsx` - Show CTAs |
0 commit comments