Skip to content

Commit a7c94ae

Browse files
committed
docs: Add tier system design document
Comprehensive design covering: - All API endpoints and which need limit checks - Error response format - Redis key patterns - Frontend integration points - Implementation order - Files to create/modify checklist
1 parent d20c7f5 commit a7c94ae

1 file changed

Lines changed: 378 additions & 0 deletions

File tree

docs/TIER_SYSTEM_DESIGN.md

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
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

Comments
 (0)