Skip to content

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:

{
  "error": "rate_limited",
  "request_id": "uuid",
  "retry_after": 23,
  "limit": 10,
  "window": "1m"
}

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:

  1. Open Supabase Dashboard → AuthenticationHooks
  2. Under "Customize Access Token", select: custom_access_token_hook
  3. Enable the hook
  4. Test: Log in with a test user and decode the JWT
  5. 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:

# DEPRECATED: Will be removed
# ADMIN_API_KEY=...

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.role works during migration

Rate Limiting (S9b)

  • [ ] 11th chat request in 1 minute returns 429
  • [ ] 429 response includes request_id and retry_after
  • [ ] Different users have independent rate limits
  • [ ] Rate limits reset after window expires

Request-ID Propagation (S9b)

  • [ ] Client-provided X-Request-ID is adopted
  • [ ] Auto-generated UUID if header not provided
  • [ ] request_id in response headers
  • [ ] request_id in all log lines
  • [ ] request_id in 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

  1. x-admin-key Not Fully Removed: Still works with deprecation warning. Full removal scheduled for follow-up PR.

  2. Rate Limiting In-Memory Only: Uses in-memory storage (not Redis). Works for single-instance deployments. For multi-instance, migrate to Redis-backed counters.

  3. JWT Secret Required: SUPABASE_JWT_SECRET must be set for JWT verification. Get from Supabase Dashboard → Settings → API.

  4. 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.sql
  • db/migrations/019_update_rls_for_jwt_claims.sql
  • app/core/middleware.py
  • app/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