Phase B Implementation Complete
Date: 2026-02-17 Branch: feature/sonnet-impl-20260217-155229 Status: ✅ Implemented (Testing Deferred)
Summary
Phase B implements security hardening and request correlation:
- JWT Custom Claims: Roles injected into JWT via Postgres hook
- Request-ID Propagation: UUID correlation across all subsystems
- Rate Limiting: Per-user sliding-window rate limits
- Structured Logging: JSON logs with request_id
- Auth Modernization: Migrate from x-admin-key to JWT-only
What Was Implemented
S6: JWT Custom Claims Hook
Migration 018: custom_access_token_hook Postgres function
- Injects app_metadata.role into JWT from profiles table
- Requires manual registration in Supabase Dashboard
- Fallback to user_metadata.role during migration
Migration 019: Updated RLS policies
- Changed from auth.jwt() ->> 'role' to auth.jwt() -> 'app_metadata' ->> 'role'
- Applied to references and scrape_runs tables
Updated: app/core/auth.py
- get_current_user() now reads from app_metadata.role
- Fallback to user_metadata.role for backward compatibility
S7: Deprecate x-admin-key
Updated: app/core/auth.py
- Added deprecation warnings to require_admin()
- x-admin-key still works but logs warning
- Will be fully removed in follow-up PR
Updated: .env.example
- Marked ADMIN_API_KEY as deprecated
- Added migration instructions
S9b: Request-ID Propagation + Rate Limiting
New: app/core/middleware.py
- RequestIDMiddleware: Generates or adopts UUID request_id
- RateLimitMiddleware: Per-user and per-IP sliding-window limits
- get_request_id(): Context variable for propagation
Rate Limits: - Chat endpoints: 10 req/min per user - Wallet/upload: 30 req/min per user - Auth endpoints: 5 req/min per IP - Admin endpoints: 60 req/min per user
429 Response Format:
S17: Structured Logging
New: app/core/logging.py
- JSONFormatter: Outputs structured JSON logs
- Includes request_id in every log line
- setup_logging(): Configure JSON logging
- log_with_context(): Helper for logging with custom fields
Log Format:
{
"timestamp": "2026-02-17T15:30:00Z",
"level": "INFO",
"logger": "app.services.ingestion",
"message": "Job 123 transitioned to ready",
"request_id": "uuid-...",
"job_id": "123",
"status": "ready"
}
Database Migrations
| Migration | File | Purpose |
|---|---|---|
| 018 | jwt_custom_claims_hook.sql |
Postgres function for JWT claims injection |
| 019 | update_rls_for_jwt_claims.sql |
Update RLS policies to use app_metadata.role |
Manual Steps Required (On Other Laptop)
1. Register JWT Custom Claims Hook
After running migration 018:
- Open Supabase Dashboard → Authentication → Hooks
- Under "Customize Access Token", select:
custom_access_token_hook - Enable the hook
- Test: Log in with a test user and decode the JWT
- Verify: JWT contains
app_metadata.role
2. Test JWT Claims
# Decode a test JWT to verify app_metadata.role is present
python -c "
import jwt
token = 'your-test-jwt-here'
decoded = jwt.decode(token, options={'verify_signature': False})
print('Role:', decoded.get('app_metadata', {}).get('role'))
"
Expected output: Role: student (or admin)
3. Test Rate Limiting
# Send 11 requests in quick succession (should get 429 on 11th)
for i in {1..11}; do
curl -X POST http://localhost:8000/ask \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"question": "test"}' \
-w "\nStatus: %{http_code}\n"
done
Expected: First 10 succeed (200), 11th returns 429 with retry_after.
4. Test Request-ID Propagation
# Check that request_id is in response headers and logs
curl -X GET http://localhost:8000/wallet/balance \
-H "Authorization: Bearer $TOKEN" \
-H "X-Request-ID: test-request-123" \
-v 2>&1 | grep -i "x-request-id"
Expected: Response header X-Request-ID: test-request-123
Configuration Updates
New Environment Variables
Add to .env:
# Supabase JWT Secret (get from Dashboard → Settings → API)
SUPABASE_JWT_SECRET=your_jwt_secret_here
# Database URL for migrations
DATABASE_URL=postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres
# Rate limiting (already have defaults)
RATE_LIMIT_CHAT_PER_MINUTE=10
RATE_LIMIT_ADMIN_PER_MINUTE=60
RATE_LIMIT_AUTH_PER_MINUTE=5
Deprecated Variables
Mark these as deprecated in your .env:
Testing Checklist
JWT Custom Claims (S6)
- [ ] Migration 018 runs successfully
- [ ] Hook registered in Supabase Dashboard
- [ ] Test user JWT contains
app_metadata.role - [ ] Admin endpoints accept JWT with
app_metadata.role = 'admin' - [ ] Fallback to
user_metadata.roleworks during migration
Rate Limiting (S9b)
- [ ] 11th chat request in 1 minute returns 429
- [ ] 429 response includes
request_idandretry_after - [ ] Different users have independent rate limits
- [ ] Rate limits reset after window expires
Request-ID Propagation (S9b)
- [ ] Client-provided
X-Request-IDis adopted - [ ] Auto-generated UUID if header not provided
- [ ]
request_idin response headers - [ ]
request_idin all log lines - [ ]
request_idin error responses (400, 401, 403, 429, 503)
Structured Logging (S17)
- [ ] Logs output as JSON
- [ ] Every log line includes
timestamp,level,logger,message,request_id - [ ] Exception logs include stack traces
Known Limitations
-
x-admin-key Not Fully Removed: Still works with deprecation warning. Full removal scheduled for follow-up PR.
-
Rate Limiting In-Memory Only: Uses in-memory storage (not Redis). Works for single-instance deployments. For multi-instance, migrate to Redis-backed counters.
-
JWT Secret Required:
SUPABASE_JWT_SECRETmust be set for JWT verification. Get from Supabase Dashboard → Settings → API. -
Manual Hook Registration: JWT custom claims hook must be registered manually in Supabase Dashboard (cannot be automated via migration).
Next Phase: Phase C - Caching & Cost Control
Once Phase B tests pass:
- S10: Rerank result caching (Redis/LRU)
- S11: Chunk text cache
- S12: Tier-based retrieval limits
Files Changed
New Files
db/migrations/018_jwt_custom_claims_hook.sqldb/migrations/019_update_rls_for_jwt_claims.sqlapp/core/middleware.pyapp/core/logging.py
Modified Files
app/core/auth.py(updated for JWT custom claims).env.example(added new variables, deprecated old ones)
Status: ✅ Phase B Complete - Ready for Testing
See SONNET_RUN.md for full implementation log