Documentation Architecture Server & API

Server & API

Overview

The Primer server is a FastAPI application built on SQLAlchemy 2.0 and Pydantic v2. It receives session data from the hook system, stores and indexes it in a relational database, serves analytics to the dashboard, and backs the MCP sidecar.

The server follows a service-layer architecture: routers define HTTP endpoints, services contain business logic and database queries, and shared models and schemas live in a common package.

src/primer/server/
├── app.py          # FastAPI application, router registration, middleware
├── middleware.py   # Rate limiting (slowapi)
├── routers/        # Endpoint definitions
│   ├── health.py
│   ├── teams.py
│   ├── engineers.py
│   ├── sessions.py
│   ├── ingest.py
│   ├── analytics.py
│   ├── alert_configs.py
│   └── admin.py
└── services/       # Business logic
    ├── ingest_service.py
    ├── analytics_service.py
    ├── synthesis_service.py
    ├── alert_config_service.py
    └── audit_service.py

Authentication

Primer uses two authentication mechanisms depending on the caller.

Admin Auth

Pass the x-admin-key header with your admin API key. This is required for team management, engineer management, session queries, analytics, and alert configuration.

curl http://localhost:8000/api/v1/teams \
  -H "x-admin-key: your-admin-key"

The admin key is set via the PRIMER_ADMIN_API_KEY environment variable on the server. See Configuration for details.

Engineer Auth

Engineer API keys have the form primer_{urlsafe_base64} and are stored as bcrypt hashes. They authenticate two ways:

  • In the request body — for session ingest endpoints (/api/v1/ingest/session, /api/v1/ingest/bulk), the key is passed in the JSON payload.
  • As a header — for facet upserts (/api/v1/ingest/facets/{session_id}), use the x-api-key header.
# Facet upsert with header auth
curl -X POST http://localhost:8000/api/v1/ingest/facets/abc-123 \
  -H "x-api-key: primer_..." \
  -H "Content-Type: application/json" \
  -d '{"outcome": "success", "session_type": "feature"}'

GitHub OAuth + JWT

The dashboard uses GitHub OAuth for engineer login. After OAuth, the server issues short-lived JWTs for API access and long-lived refresh tokens for session continuity. This flow is handled automatically by the frontend.

Key security

Engineer API keys are shown only once at creation time. Store them securely. If a key is lost, an admin must create a new engineer account.

REST API Reference

Base URL: http://localhost:8000. All resource routes are prefixed with /api/v1/.

Health

GET /health

No authentication required. Returns server status.

{"status": "ok"}

Teams

POST   /api/v1/teams    # Create team (Admin)
GET    /api/v1/teams    # List all teams (Admin)

Example — create a team:

curl -X POST http://localhost:8000/api/v1/teams \
  -H "x-admin-key: your-admin-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "Backend"}'

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Backend",
  "created_at": "2025-01-15T10:30:00"
}

Engineers

POST   /api/v1/engineers                        # Create engineer, returns API key (Admin)
GET    /api/v1/engineers                        # List engineers (Admin)
GET    /api/v1/engineers/{engineer_id}/sessions # List engineer sessions (Admin)

When creating an engineer, the response includes the plaintext api_key — this is the only time it is returned.

curl -X POST http://localhost:8000/api/v1/engineers \
  -H "x-admin-key: your-admin-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com", "team_id": "..."}'

Ingest

POST   /api/v1/ingest/session             # Ingest or update a session (API key in body)
POST   /api/v1/ingest/bulk                # Bulk ingest sessions (API key in body)
POST   /api/v1/ingest/facets/{session_id} # Upsert session facets (x-api-key header)

The session ingest endpoint is idempotent — sending the same session ID updates the existing record rather than creating a duplicate.

Bulk ingest

Use /api/v1/ingest/bulk when syncing multiple sessions at once (e.g., after offline work). It accepts an array of session payloads and processes them in a single transaction.

Sessions

GET    /api/v1/sessions             # List sessions with filters (Admin)
GET    /api/v1/sessions/{session_id} # Full session details (Admin)

Query parameters for the list endpoint:

ParameterTypeDescription
engineer_idstringFilter by engineer
team_idstringFilter by team
start_dateISO dateSessions after this date
end_dateISO dateSessions before this date
limitintegerMax results (default 50)
offsetintegerPagination offset

Analytics

All analytics endpoints accept optional team_id, start_date, and end_date query parameters for scoping.

GET    /api/v1/analytics/overview         # Aggregate stats (Admin)
GET    /api/v1/analytics/friction         # Friction points by frequency (Admin)
GET    /api/v1/analytics/tools            # Tool usage rankings (Admin)
GET    /api/v1/analytics/models           # Model usage by tokens (Admin)
GET    /api/v1/analytics/recommendations  # Actionable recommendations (Admin)

Example — get overview for a specific team in the last 30 days:

curl "http://localhost:8000/api/v1/analytics/overview?team_id=abc&start_date=2025-01-01&end_date=2025-01-31" \
  -H "x-admin-key: your-admin-key"

Alert Configs

GET    /api/v1/alert-configs         # List alert configs (Admin)
POST   /api/v1/alert-configs         # Create alert config (Admin)
PUT    /api/v1/alert-configs/{id}    # Update alert config (Admin)
DELETE /api/v1/alert-configs/{id}    # Delete alert config (Admin)

Alert configs follow a priority chain: team-specific > global > config defaults. See Alert Thresholds for details.

Data Model

Primer uses SQLAlchemy 2.0 declarative models with UUID string primary keys. The database schema includes the following core tables:

TablePurpose
teamsEngineering teams
engineersIndividual engineers with bcrypt-hashed API keys, GitHub identity
sessionsOne record per AI coding session with token counts, timing, metadata
session_facetsQualitative analysis: goal, outcome, session type, friction
tool_usagesPer-session tool call counts by tool name
model_usagesPer-session model token usage by model name
daily_statsPre-aggregated per-engineer daily counts
ingest_eventsAudit log of all ingest operations
session_messagesIndividual messages within sessions (role, content, tool calls)
session_commitsGit commits made during sessions with diff stats

Supporting tables:

TablePurpose
git_repositoriesTracked repositories with AI readiness scores
pull_requestsGitHub PRs linked to sessions via commits
alert_configsThreshold configuration for usage alerts
alertsTriggered alert instances
audit_logsAdmin mutation audit trail (actor, action, resource, IP)
refresh_tokensJWT refresh tokens for dashboard auth
narrative_cacheCached AI-generated narrative summaries

All tables use func.now() for server-side timestamps and UUID strings for primary keys (except auto-increment integer IDs on junction tables).

Session Ingest Flow

The complete data flow from a coding session to stored analytics:

Claude Code session ends
  └── SessionEnd hook fires
        └── primer.hook.extractor reads JSONL transcript
              └── Extracts metadata (tokens, tools, git, etc.)
                    └── POST /api/v1/ingest/session
                          └── ingest_service stores to DB
                                ├── sessions table
                                ├── tool_usages table
                                ├── model_usages table
                                ├── session_commits table
                                ├── daily_stats (upsert)
                                └── ingest_events (audit)

The ingest service:

  1. Validates the engineer API key via bcrypt comparison.
  2. Creates or updates the session record (idempotent on session ID).
  3. Replaces tool and model usage records for the session.
  4. Upserts daily stats for the engineer.
  5. Logs the ingest event for audit purposes.

Rate Limiting

The server uses slowapi for per-route rate limiting. The rate limit key is derived from the API key prefix (first 8 characters) or the client IP address for unauthenticated requests.

Rate limits are configured per-route. The ingest endpoints have higher limits to accommodate bulk syncing, while analytics endpoints have lower limits to prevent abuse.

Rate limit headers

When rate-limited, the server returns HTTP 429 with a Retry-After header indicating how many seconds to wait before retrying.

Error Codes

StatusMeaning
401Missing or invalid engineer API key
403Missing or invalid admin key
404Resource not found
422Validation error (Pydantic schema mismatch)
429Rate limit exceeded

All error responses follow a consistent JSON format:

{"detail": "Description of what went wrong"}

Validation errors (422) include field-level detail:

{
  "detail": [
    {
      "loc": ["body", "name"],
      "msg": "Field required",
      "type": "missing"
    }
  ]
}

Production Deployment

For single-user or small team deployments, the default SQLite database works well. For larger teams with concurrent users, switch to PostgreSQL by setting PRIMER_DATABASE_URL.

Running with Multiple Workers

# Multiple uvicorn workers
uvicorn primer.server.app:app --host 0.0.0.0 --port 8000 --workers 4

# Or via Gunicorn
gunicorn primer.server.app:app -w 4 -k uvicorn.workers.UvicornWorker

SQLite and workers

SQLite does not support concurrent writes. If running multiple workers, use PostgreSQL. Set PRIMER_DATABASE_URL=postgresql+asyncpg://user:pass@host/primer in your environment.

Database Migrations

Primer uses Alembic for schema migrations. After upgrading Primer:

alembic upgrade head

To create a new migration after modifying models:

alembic revision --autogenerate -m "add new_column to sessions"

Reverse Proxy

In production, run behind a reverse proxy (nginx, Caddy, etc.) that handles TLS termination, request buffering, and static file serving for the dashboard.

See Configuration for all environment variables.