This administrator manual was first written against v1.1.0. The v1.2.0 release (2026-04-23) adds five operator-facing capabilities that are not individually documented in every section below. Where this manual and the CHANGELOG disagree, the CHANGELOG wins. Always treat CHANGELOG.md §[1.2.0] as the authoritative delta.
data_sources.connection_config is now stored as a Fernet envelope (AES-128-CBC + HMAC-SHA256). A new required ENCRYPTION_KEY env var drives the encryption; the installer auto-generates a Fernet key on fresh install and prints a loud "BACK IT UP SEPARATELY FROM YOUR DATABASE" banner. Operator section: USER-MANUAL.md §B.3.2. Spec: UNIFIED-SPEC §8.10. Reversible Alembic migration 019_encrypt_connection_config. Losing the key means losing decryption access to every connector's credentials — no recovery path in v1.2.0.PORTAL_MODE env var (private default, public opt-in). Private mode is the existing staff-only posture; public mode exposes exactly three surfaces — landing page, resident-registration path, authenticated records-request submission for UserRole.PUBLIC. Nothing else. Operator section: USER-MANUAL.md §B.3.1. Spec: UNIFIED-SPEC §8.9..exe shipped as a GitHub Release asset (CivicRecordsAI-1.2.0-Setup.exe + .sha256). Unsigned by design per locked signing posture α — SmartScreen will show "Windows protected your PC — Unknown publisher." Remediation: More info → Run anyway, then confirm UAC. Two-shortcut flow: Start CivicRecords AI (daily docker compose up -d) and Install or Repair CivicRecords AI (full bootstrap + model pick + pull). macOS/Linux remain on the guided-script path (install.sh).gemma4:e4b is the default selected model. The fake tags gemma4:12b and gemma4:27b that appeared in earlier hardware-detect scripts have been purged. gemma4:26b / gemma4:31b are still selectable but gated behind an explicit "requires stronger hardware" acknowledgement against the locked 32 GB target profile. The embedding model nomic-embed-text is auto-pulled alongside the selected LLM.onboarding_status through the not_started → in_progress → complete lifecycle. T5B auto-seeds 175 state-scoped exemption rules, 5 compliance templates, and 12 notification templates on first boot via app/main.lifespan; idempotent via skip-if-exists so admin customizations survive re-seeds.
Test coverage at v1.2.0: 617 backend pytest + 36 frontend vitest, CI-verified (GitHub Actions run 24859606170 on commit cc06abc). See the v1.2.0 release page for the curated release notes.
CivicRecords AI is a locally-hosted, Docker Compose-based application that assists municipal clerks in processing open records (public records / FOIA) requests. The system runs entirely on-premises with no cloud dependencies, no telemetry, and no outbound data transmission. All AI inference is performed locally via Ollama using open-weight models.
The architecture follows a standard three-tier pattern: a React single-page application serves as the administrative front end, communicating with a FastAPI REST backend, which in turn interfaces with PostgreSQL (with pgvector for semantic search), Redis (for task queuing), and Ollama (for local LLM inference). Asynchronous document ingestion is handled by Celery workers with Redis as the message broker.
For the full architectural diagram and data flow, refer to docs/architecture/system-architecture.html.
| Service | Image | Port | Purpose | Health Check |
|---|---|---|---|---|
postgres |
pgvector/pgvector:pg17 |
5432 (internal) | Primary database with vector search extensions | pg_isready -U civicrecords |
redis |
redis:7.2-alpine |
6379 (internal) | Task queue broker and result backend | redis-cli ping |
ollama |
ollama/ollama:latest |
11434 (internal) | Local LLM inference engine | ollama list |
api |
Custom (Dockerfile.backend) | 8000 | FastAPI REST API server | curl -sf http://localhost:8000/health |
worker |
Custom (Dockerfile.backend) | — | Celery async task worker (ingestion, embeddings) | celery inspect ping |
beat |
Custom (Dockerfile.backend) | — | Celery periodic task scheduler | — |
frontend |
Custom (Dockerfile.frontend) | 8080 | React admin panel (served via nginx) | curl -sf http://localhost:80/ |
| Layer | Technology | Version |
|---|---|---|
| Runtime | Python | 3.12 |
| API Framework | FastAPI | 0.115+ |
| ORM | SQLAlchemy | 2.0 |
| Migrations | Alembic | 1.13+ |
| Auth | FastAPI-Users + JWT | — |
| Task Queue | Celery | 5.x |
| Database | PostgreSQL + pgvector | 17 |
| Cache / Broker | Redis | 7.2 (BSD licensed) |
| AI Inference | Ollama | latest |
| Frontend | React + shadcn/ui + Tailwind CSS | 18.x |
| Containerization | Docker Compose | v2 |
CivicRecords AI is released under the Apache License 2.0. All runtime dependencies use permissive or weak-copyleft licenses (MIT, Apache 2.0, BSD, LGPL, MPL). Redis is pinned to version 7.2 (BSD licensed); version 8.x and later changed to a non-permissive license and must not be used.
redis:7.2-alpine. Redis 8.x uses the Server Side Public License (SSPL), which is incompatible with the project's permissive licensing requirements.
| Component | Minimum | Notes |
|---|---|---|
| CPU | 8 cores (x86_64 or ARM64) | Modern Intel Core i7 / AMD Ryzen 7 or better |
| RAM | 32 GB | Required for 12B parameter model inference |
| Storage | 50 GB SSD | OS + Docker images + models + small document corpus |
| Network | 1 Gbps Ethernet | Internal network only; no internet required at runtime |
| Component | Recommended | Notes |
|---|---|---|
| CPU | 12–16 cores | Intel Xeon / AMD EPYC for production workloads |
| RAM | 64 GB | Recommended tier; headroom for workstation Gemma 4 tags (26b/31b) plus a large document corpus |
| Storage | 2 TB NVMe SSD | Room for extensive document archives and database growth |
| GPU (optional) | Dedicated GPU with 8+ GB VRAM | Dramatically accelerates inference; see GPU support below |
| Platform | GPU Framework | Deployment Method |
|---|---|---|
| Linux (AMD) | ROCm | docker-compose.gpu.yml overlay with GPU device passthrough |
| Windows (AMD/other) | DirectML | Host-native Ollama via docker-compose.host-ollama.yml |
| Linux/Windows (NVIDIA) | CUDA | NVIDIA Container Toolkit + Ollama GPU support |
The installer automatically detects GPU hardware and selects the appropriate Docker Compose overlay. GPU acceleration is optional; the system functions fully on CPU-only hardware.
gemma4:e4b), the system targets completion of a single records query — including document retrieval, exemption scanning, and LLM analysis — in under 30 seconds.
Open PowerShell as Administrator in the project directory and run:
.\install.ps1
The script performs the following steps:
.env from .env.example and generates a cryptographically random JWT secret.envscripts/detect_hardware.ps1) and writes .env.hardwarenomic-embed-text)Run from the project directory:
chmod +x install.sh ./install.sh
The script will automatically install Docker and Docker Compose if they are not present on the system. On Linux with AMD GPUs, the script detects ROCm support and applies the docker-compose.gpu.yml overlay for GPU device passthrough.
Docker Desktop for Mac is required. Install it from docker.com, then run:
chmod +x install.sh ./install.sh
macOS does not support GPU passthrough to Docker containers. All inference runs on CPU.
After installation completes, verify that all services are healthy:
# Check all container health statuses docker compose ps NAME STATUS PORTS civicrecords-api Up (healthy) 0.0.0.0:8000->8000/tcp civicrecords-beat Up civicrecords-frontend Up (healthy) 0.0.0.0:8080->80/tcp civicrecords-ollama Up (healthy) civicrecords-postgres Up (healthy) 5432/tcp civicrecords-redis Up (healthy) 6379/tcp civicrecords-worker Up (healthy) # Test API health endpoint curl http://localhost:8000/health {"status":"ok","version":"1.4.1"} # Verify OpenAPI documentation is accessible curl -s http://localhost:8000/docs | head -1 # Verify frontend is serving curl -s -o /dev/null -w "%{http_code}" http://localhost:8080 200 # Run data sovereignty verification bash scripts/verify-sovereignty.sh # Linux/macOS .\scripts\verify-sovereignty.ps1 # Windows
The system automatically creates the first administrator account on startup using the credentials specified in the .env file:
FIRST_ADMIN_EMAIL — the login email for the initial admin (default: admin@example.gov)FIRST_ADMIN_PASSWORD — the initial password (must be changed from the default)JWT_SECRET is left at its default placeholder value. The installer generates a secure secret automatically, but if you are configuring .env manually, generate one with: openssl rand -hex 32
After installation, navigate to http://<server-ip>:8080 to access the admin panel and log in with the configured credentials. Change the admin password immediately after first login.
| Variable | Description | Default | Required |
|---|---|---|---|
DATABASE_URL |
PostgreSQL connection string (asyncpg driver) | postgresql+asyncpg://civicrecords:civicrecords@postgres:5432/civicrecords |
Yes |
JWT_SECRET |
Secret key for signing JWT tokens. Minimum 32 characters. Application refuses to start with insecure defaults. | None (must be set) | Yes |
JWT_LIFETIME_SECONDS |
JWT token expiration time in seconds | 3600 (1 hour) |
No |
FIRST_ADMIN_EMAIL |
Email address for the auto-created initial admin account | admin@example.gov |
Yes |
FIRST_ADMIN_PASSWORD |
Password for the initial admin account | None (must be set) | Yes |
OLLAMA_BASE_URL |
URL for the Ollama inference server | http://ollama:11434 |
Yes |
REDIS_URL |
Redis connection string for Celery task queue | redis://redis:6379/0 |
Yes |
AUDIT_RETENTION_DAYS |
Number of days to retain audit log entries | 1095 (3 years) |
No |
CORS_ORIGINS |
Allowed CORS origins (JSON list format) | ["http://localhost:8080"] |
No |
EMBEDDING_MODEL |
Ollama model name for document embeddings | nomic-embed-text |
No |
CHAT_MODEL |
Ollama model name for query analysis and summarization | gemma4:e4b |
No |
VISION_MODEL |
Ollama model name for document image/PDF analysis | gemma4:e4b |
No |
The installer generates a .env.hardware file with auto-detected values. These variables are consumed by the Docker Compose overlays and are not typically edited manually.
| Variable | Description | Example |
|---|---|---|
CIVICRECORDS_GPU_ENABLED | Whether a compatible GPU was detected | true |
CIVICRECORDS_PLATFORM | Detected GPU platform identifier | AMD |
CIVICRECORDS_GFX_VERSION | GPU microarchitecture version | gfx1100 |
CIVICRECORDS_TOTAL_RAM_GB | Total system RAM in gigabytes | 64 |
CIVICRECORDS_RECOMMENDED_MODEL | Default pre-filled into the installer picker (operator can change) | gemma4:e4b |
CIVICRECORDS_USE_HOST_OLLAMA | Use native host Ollama instead of container (Windows GPU) | true |
GPU support is configured through Docker Compose overlay files:
docker-compose.gpu.yml — Linux AMD ROCm: passes GPU devices into the Ollama containerdocker-compose.host-ollama.yml — Windows DirectML: replaces containerized Ollama with host-native Ollama for GPU accessdeploy.resources.reservations.devices in a custom overlay
The system supports standard Docker Compose override patterns. To customize resource limits, port mappings, or volume mounts without modifying the base file, create a docker-compose.override.yml:
# docker-compose.override.yml services: api: ports: - "443:8000" deploy: resources: limits: memory: 4G
User accounts are created exclusively by administrators through the admin API or admin panel. There is no self-registration. This is a deliberate security design: only authorized municipal staff should have access to the system.
# Create a new user via the API curl -X POST http://localhost:8000/admin/users \ -H "Authorization: Bearer <admin-jwt-token>" \ -H "Content-Type: application/json" \ -d '{ "email": "clerk@city.gov", "password": "SecurePassword123!", "full_name": "Jane Doe", "role": "staff" }'
| Role | Code | Create Requests | Search Documents | Review / Approve | Manage Users | System Config | Audit Logs |
|---|---|---|---|---|---|---|---|
| Admin | admin |
Yes | Yes (all depts) | Yes | Yes | Yes | Yes |
| Reviewer | reviewer |
Yes | Yes (all depts) | Yes | No | No | View only |
| Staff | staff |
Yes | Own dept only | No | No | No | No |
| Read Only | read_only |
No | Own dept only | No | No | No | No |
curl -X POST http://localhost:8000/departments/ \ -H "Authorization: Bearer <admin-jwt-token>" \ -H "Content-Type: application/json" \ -d '{"name": "Public Works", "description": "Roads, water, utilities"}'
Use the PATCH /departments/{dept_id} endpoint to update department membership, or assign a department when creating users via the admin panel. A user can belong to one department at a time.
Service accounts enable machine-to-machine API access for integrations with other municipal systems (e.g., a records management system pushing documents). Service accounts authenticate with API keys rather than JWT tokens.
# Create a service account curl -X POST http://localhost:8000/service-accounts/ \ -H "Authorization: Bearer <admin-jwt-token>" \ -H "Content-Type: application/json" \ -d '{"name": "RMS Integration", "description": "Automated document feed from Records Management System"}'
A directory data source points to a file system path accessible to the API container. This path must be mounted as a Docker volume.
curl -X POST http://localhost:8000/datasources/ \ -H "Authorization: Bearer <admin-jwt-token>" \ -H "Content-Type: application/json" \ -d '{ "name": "City Council Minutes", "source_type": "directory", "config": {"path": "/data/council-minutes"} }'
Files can be uploaded directly through the API or the admin panel. Uploaded files are stored in the uploads Docker volume at /tmp/civicrecords-uploads inside the container.
curl -X POST http://localhost:8000/datasources/upload \ -H "Authorization: Bearer <admin-jwt-token>" \ -F "file=@meeting-minutes-2025.pdf"
nomic-embed-text by default) and stored as a vector in pgvector.Ingestion is performed asynchronously by the Celery worker service.
# Trigger ingestion for a specific data source curl -X POST http://localhost:8000/datasources/{source_id}/ingest \ -H "Authorization: Bearer <admin-jwt-token>"
# View data source statistics including ingestion progress curl http://localhost:8000/datasources/stats \ -H "Authorization: Bearer <admin-jwt-token>"
Each document tracks its ingestion status: pending, processing, completed, or failed.
| Format | Extensions | Parser | Track |
|---|---|---|---|
.pdf | PyMuPDF / Vision model fallback | Text + OCR | |
| Microsoft Word | .docx | python-docx | Text |
| Plain Text | .txt | Direct read | Text |
| CSV | .csv | CSV parser | Tabular |
| Markdown | .md | Direct read | Text |
| HTML | .html, .htm | BeautifulSoup | Text |
| JSON | .json | JSON parser | Structured |
.eml | Email parser | Text | |
| Images | .png, .jpg, .tiff | Vision model OCR | OCR |
docker compose ps worker. Restart if needed: docker compose restart worker.docker compose logs worker --tail 100. Common causes: unsupported file format, corrupt file, or Ollama not responding (for vision model parsing).docker compose exec ollama ollama list. If nomic-embed-text is missing, pull it.CivicRecords AI is optimized for the Gemma 4 family of open-weight models from Google. These models provide strong instruction-following, good factual accuracy, and are released under permissive licenses suitable for government use.
The installer picker presents all four supported Gemma 4 tags. The default is gemma4:e4b. Only e2b and e4b are supportable at the 32 GB baseline target profile; 26b and 31b require stronger hardware and are gated behind an explicit confirmation in the installer.
| Model | Class | Parameters | Disk | RAM (advisory) | Supportable at baseline |
|---|---|---|---|---|---|
gemma4:e2b | Edge | 2.3B effective (5.1B w/ embeddings) | 7.2 GB | ~16 GB | Yes |
gemma4:e4b (default) | Edge | 4.5B effective (8B w/ embeddings) | 9.6 GB | ~20 GB | Yes |
gemma4:26b | Workstation MoE | 25.2B total / 3.8B active | 18 GB | 48+ GB recommended | No (32 GB baseline) |
gemma4:31b | Workstation dense | 30.7B | 20 GB | 64+ GB; GPU recommended | No (32 GB baseline) |
# Pull the embedding model (required) docker compose exec ollama ollama pull nomic-embed-text # Pull the default chat/vision model docker compose exec ollama ollama pull gemma4:e4b # OR a smaller edge model for tighter installs: docker compose exec ollama ollama pull gemma4:e2b # OR workstation models (require 48+ GB RAM for 26b, 64+ GB RAM and GPU for 31b): docker compose exec ollama ollama pull gemma4:26b docker compose exec ollama ollama pull gemma4:31b # If using host-native Ollama (Windows GPU): ollama pull nomic-embed-text ollama pull gemma4:e4b # Verify installed models docker compose exec ollama ollama list
The model registry tracks compliance metadata for every model used in the system. This supports audit and governance requirements by recording which model version produced each analysis.
# View registered models curl http://localhost:8000/admin/models/registry \ -H "Authorization: Bearer <admin-jwt-token>" # Register a new model with compliance metadata curl -X POST http://localhost:8000/admin/models/registry \ -H "Authorization: Bearer <admin-jwt-token>" \ -H "Content-Type: application/json" \ -d '{ "model_name": "gemma4:e4b", "version": "latest", "license": "Apache 2.0", "provider": "Google", "purpose": "Records query analysis and exemption reasoning" }'
To change the active model, update the CHAT_MODEL and/or VISION_MODEL variables in .env and restart the API service:
# Edit .env to set the new model (any Ollama tag; picker defaults are four Gemma 4 tags) # CHAT_MODEL=gemma4:e2b # VISION_MODEL=gemma4:e2b # Restart API to pick up the change docker compose restart api
The embedding model (nomic-embed-text by default) converts document chunks into vector representations for semantic search. Changing the embedding model requires re-ingesting all documents to rebuild the vector index.
EMBEDDING_MODEL invalidates all existing document embeddings. You must re-ingest every data source after changing this value. This can be a time-consuming operation for large document corpora.
| Model | Disk Space | RAM (inference) | GPU VRAM | Avg. Query Time (CPU) |
|---|---|---|---|---|
nomic-embed-text | ~275 MB | ~1 GB | ~512 MB | <1 sec / chunk |
gemma4:e2b | 7.2 GB | ~16 GB | ~8 GB | 8–15 sec |
gemma4:e4b (default) | 9.6 GB | ~20 GB | ~10 GB | 10–20 sec |
gemma4:26b | 18 GB | 48+ GB recommended | ~20 GB | 20–40 sec |
gemma4:31b | 20 GB | 64+ GB recommended | ~24 GB | 30–60 sec |
CivicRecords AI uses a rules-primary, LLM-secondary architecture for exemption detection. When a document is scanned for potential exemptions (e.g., personal privacy, law enforcement confidentiality, trade secrets):
The system ships with approximately 180 exemption rules covering all 50 U.S. states plus federal FOIA categories. Rules are seeded from the backend/scripts/seed_rules.py script and stored in the exemption_rules database table.
# List all exemption rules (filterable by state) curl "http://localhost:8000/exemptions/rules/?state=CO" \ -H "Authorization: Bearer <admin-jwt-token>"
curl -X POST http://localhost:8000/exemptions/rules/ \ -H "Authorization: Bearer <admin-jwt-token>" \ -H "Content-Type: application/json" \ -d '{ "state": "CO", "statute": "CRS 24-72-204(3)(a)(IV)", "category": "Trade Secrets", "description": "Trade secrets, privileged information, confidential commercial data", "keywords": ["trade secret", "proprietary", "confidential commercial"], "rule_type": "pattern", "enabled": true }'
# Disable a specific rule curl -X PATCH http://localhost:8000/exemptions/rules/{rule_id} \ -H "Authorization: Bearer <admin-jwt-token>" \ -H "Content-Type: application/json" \ -d '{"enabled": false}'
To populate or reset the exemption rules database with the built-in rule set:
docker compose exec api python -m scripts.seed_rules
Every significant action in the system (login, document access, search queries, exemption decisions, request status changes) generates an immutable audit log entry. Each entry includes a SHA-256 hash of its contents chained to the previous entry's hash, creating a tamper-evident log structure similar to a blockchain.
If any entry is modified or deleted, the chain verification will fail at that point, providing cryptographic proof of tampering. This design satisfies records retention and legal discovery requirements.
The default retention period is 1,095 days (3 years), configurable via the AUDIT_RETENTION_DAYS environment variable. Entries older than this threshold are eligible for archival.
# Verify the audit log hash chain is intact curl http://localhost:8000/audit/verify \ -H "Authorization: Bearer <admin-jwt-token>" {"valid": true, "entries_checked": 14523, "chain_intact": true}
# Export audit logs (supports CSV and JSON formats) curl "http://localhost:8000/audit/export?format=csv&start_date=2026-01-01&end_date=2026-03-31" \ -H "Authorization: Bearer <admin-jwt-token>" \ -o audit-q1-2026.csv # JSON export curl "http://localhost:8000/audit/export?format=json" \ -H "Authorization: Bearer <admin-jwt-token>" \ -o audit-full.json
The system includes five compliance template documents stored at backend/compliance_templates/:
| Template | File | Purpose |
|---|---|---|
| AI Governance Policy | ai-governance-policy.md | Organizational AI use policy framework |
| AI Use Disclosure | ai-use-disclosure.md | Public notice that AI is used in records processing |
| CAIA Impact Assessment | caia-impact-assessment.md | Colorado AI Act risk classification assessment |
| Data Residency Attestation | data-residency-attestation.md | Certification that all data remains on-premises |
| Response Letter Disclosure | response-letter-disclosure.md | AI disclosure language for inclusion in response letters |
Templates contain placeholder variables (city name, contact information, etc.) that are populated from the city profile:
# Set up the city profile first curl -X POST http://localhost:8000/city-profile \ -H "Authorization: Bearer <admin-jwt-token>" \ -H "Content-Type: application/json" \ -d '{ "city_name": "City of Boulder", "state": "CO", "contact_email": "records@bouldercolorado.gov" }' # Render a template with city profile data curl http://localhost:8000/exemptions/templates/{template_id}/render \ -H "Authorization: Bearer <admin-jwt-token>"
Run the sovereignty verification script to confirm that no data leaves the local system:
# Linux / macOS bash scripts/verify-sovereignty.sh # Windows .\scripts\verify-sovereignty.ps1
This script inspects Docker network configuration, DNS resolution settings, and container firewall rules to verify that no container has outbound internet access.
Under the Colorado Artificial Intelligence Act (SB 24-205), CivicRecords AI is classified as not high-risk. The system does not autonomously make or substantially contribute to consequential decisions. All exemption flags and document classifications require human review and approval before any action is taken. The system is an assistive tool, not a decision-maker. The full impact assessment is documented in backend/compliance_templates/caia-impact-assessment.md.
By default, only the API (port 8000) and frontend (port 8080) services expose ports to the host. All other services (PostgreSQL, Redis, Ollama) communicate over Docker's internal network and are not accessible from outside the Docker Compose stack.
CivicRecords AI does not include a built-in TLS certificate. To enable HTTPS, place a reverse proxy in front of the application:
# Example: nginx reverse proxy configuration server { listen 443 ssl; server_name records.city.gov; ssl_certificate /etc/ssl/certs/records.city.gov.crt; ssl_certificate_key /etc/ssl/private/records.city.gov.key; location /api/ { proxy_pass http://127.0.0.1:8000/; } location / { proxy_pass http://127.0.0.1:8080/; } }
CHANGE-ME, empty string, or the placeholder value).openssl rand -hex 32.env file with restricted permissions: chmod 600 .env
The login endpoint (POST /auth/jwt/login) is rate-limited to 5 requests per minute per IP address. Exceeding this limit returns HTTP 429 (Too Many Requests). The rate limiter uses in-memory tracking with a maximum of 10,000 tracked IPs and automatic expiry of stale entries.
The installer generates a random JWT secret and forces the administrator to set the initial admin password before the first start. The application will not start with placeholder credentials.
CivicRecords AI makes zero outbound network connections during normal operation. There is no telemetry, no analytics, no update checking, and no "phone home" behavior. All AI inference happens locally via Ollama. The system can operate on a completely air-gapped network after initial Docker image pulls.
All API endpoints enforce role-based access control (RBAC) using the require_role() dependency. See the User & Department Administration section for the full role permissions matrix. Key restrictions:
/admin/*) require the admin role.admin role.staff and read_only roles.reviewer or admin role.Service account API keys are hashed with bcrypt before storage. The plaintext key is returned exactly once at creation time and is never stored or logged. Authentication of service account requests validates the provided key against the stored hash.
| Component | Location | Contents | Priority |
|---|---|---|---|
| PostgreSQL database | pgdata Docker volume | All application data: users, documents, audit logs, search indices, rules | Critical |
| Environment files | .env, .env.hardware | Configuration, JWT secret, database credentials | Critical |
| Uploaded documents | uploads Docker volume | Original files uploaded through the admin panel | High |
| Ollama models | ollamadata Docker volume | Downloaded model weights (~8–17 GB) | Low (re-downloadable) |
# Create a full database dump docker compose exec -T postgres pg_dump -U civicrecords civicrecords > backup-$(date +%Y%m%d).sql # Compressed backup (recommended for large databases) docker compose exec -T postgres pg_dump -U civicrecords -Fc civicrecords > backup-$(date +%Y%m%d).dump # Back up environment files cp .env .env.backup-$(date +%Y%m%d) # Back up uploaded documents volume docker run --rm -v civicrecords-ai_uploads:/data -v $(pwd)/backups:/backup alpine \ tar czf /backup/uploads-$(date +%Y%m%d).tar.gz -C /data .
# 1. Stop the application docker compose down # 2. Start only the database docker compose up -d postgres # 3. Wait for database readiness docker compose exec -T postgres pg_isready -U civicrecords # 4. Drop and recreate the database docker compose exec -T postgres dropdb -U civicrecords civicrecords docker compose exec -T postgres createdb -U civicrecords civicrecords # 5a. Restore from SQL dump docker compose exec -T postgres psql -U civicrecords civicrecords < backup-20260401.sql # 5b. OR restore from compressed dump docker compose exec -T postgres pg_restore -U civicrecords -d civicrecords < backup-20260401.dump # 6. Start all services docker compose up -d # 7. Verify health curl http://localhost:8000/health
| Backup Type | Frequency | Retention |
|---|---|---|
| Full database dump | Daily | 30 days |
| Environment file backup | On every change | Indefinite |
| Uploaded documents | Weekly | 90 days |
| Off-site copy | Weekly | 1 year |
curl http://localhost:8000/health {"status":"ok","version":"1.4.1"}
The GET /health endpoint returns HTTP 200 with the application version when the API is operational. Use this for load balancer health checks and monitoring systems.
# View health status of all services docker compose ps # View health check logs for a specific service docker inspect --format='{{json .State.Health}}' civicrecords-api-1
| Symptom | Likely Cause | Resolution |
|---|---|---|
| API returns 500 on startup | Database migration failure | Check logs: docker compose logs api. Run migrations manually: docker compose exec api alembic upgrade head |
| Search returns no results | Embedding model not pulled | Pull the model: docker compose exec ollama ollama pull nomic-embed-text |
| Ingestion stuck at "processing" | Worker service crashed or not running | Check and restart: docker compose restart worker |
| Login returns 429 | Rate limit exceeded (5/min per IP) | Wait 60 seconds. If persistent, check for brute-force attempts in audit logs. |
| Ollama timeout errors | Insufficient RAM for model | Switch to a smaller model or add RAM. Check: docker stats |
| "JWT_SECRET is set to an insecure default" | .env not configured |
Generate a secret: openssl rand -hex 32 and set in .env |
| Frontend shows blank page | API not reachable from browser | Verify CORS_ORIGINS in .env includes the frontend URL |
| PostgreSQL won't start | Corrupted data volume or port conflict | Check logs: docker compose logs postgres. Verify port 5432 is not in use. |
# API server logs docker compose logs api --tail 100 -f # Worker (ingestion) logs docker compose logs worker --tail 100 -f # Database logs docker compose logs postgres --tail 50 # Ollama inference logs docker compose logs ollama --tail 50 # All services combined docker compose logs --tail 200 -f
docker compose exec ollama ollama list NAME ID SIZE MODIFIED nomic-embed-text 274b5616... 274 MB 3 days ago gemma4:e4b c6eb396d... 9.6 GB 3 days ago
# Test database connectivity from the API container docker compose exec api python -c " from app.database import engine import asyncio async def check(): async with engine.connect() as conn: result = await conn.execute(text('SELECT 1')) print('Database OK:', result.scalar()) from sqlalchemy import text asyncio.run(check()) " # Direct PostgreSQL check docker compose exec postgres pg_isready -U civicrecords /var/run/postgresql:5432 - accepting connections
docker compose exec -T postgres pg_dump -U civicrecords -Fc civicrecords > pre-upgrade-backup.dump
git pull origin main
docker compose build
docker compose run --rm api alembic upgrade head
docker compose up -d
curl http://localhost:8000/health
CivicRecords AI uses Alembic for database schema migrations. Migration files are stored in backend/alembic/versions/. Key commands:
# Apply all pending migrations docker compose exec api alembic upgrade head # Check current migration version docker compose exec api alembic current # View migration history docker compose exec api alembic history
Always upgrade sequentially through minor versions. Skipping major versions may result in migration conflicts. Check the CHANGELOG.md for breaking changes between versions.
docker compose down
docker compose up -d postgres docker compose exec -T postgres dropdb -U civicrecords civicrecords docker compose exec -T postgres createdb -U civicrecords civicrecords docker compose exec -T postgres pg_restore -U civicrecords -d civicrecords < pre-upgrade-backup.dump
git checkout v1.0.0 # Replace with the target version tag
docker compose build docker compose up -d
alembic downgrade is possible but risky in production. Prefer a full database restore from backup for rollback scenarios.
| Method | Path | Auth | Description |
|---|---|---|---|
| Health | |||
GET | /health | None | Application health check and version |
| Authentication | |||
POST | /auth/jwt/login | None | Authenticate and receive JWT token |
POST | /auth/jwt/logout | JWT | Invalidate current session |
| Users | |||
GET | /users/me | JWT | Get current authenticated user profile |
PATCH | /users/me | JWT | Update current user profile |
| Administration | |||
GET | /admin/status | Admin | System status overview |
GET | /admin/users | Admin | List all users |
POST | /admin/users | Admin | Create a new user account |
GET | /admin/models | Admin | List available Ollama models |
GET | /admin/models/registry | Admin | List registered models with compliance metadata |
POST | /admin/models/registry | Admin | Register a model with compliance metadata |
PATCH | /admin/models/registry/{model_id} | Admin | Update model registry entry |
DELETE | /admin/models/registry/{model_id} | Admin | Remove model from registry |
| Audit | |||
GET | /audit/logs | Admin | Query audit log entries with filters |
GET | /audit/verify | Admin | Verify audit log hash chain integrity |
GET | /audit/export | Admin | Export audit logs (CSV/JSON) |
| Data Sources | |||
POST | /datasources/ | Admin | Create a new data source |
GET | /datasources/ | JWT | List data sources |
PATCH | /datasources/{source_id} | Admin | Update data source configuration |
POST | /datasources/{source_id}/ingest | Admin | Trigger ingestion for a data source |
POST | /datasources/upload | JWT | Upload a file for ingestion |
GET | /datasources/stats | JWT | Data source ingestion statistics |
| Documents | |||
GET | /documents/ | JWT | List ingested documents |
GET | /documents/{document_id} | JWT | Get document details and metadata |
GET | /documents/{document_id}/chunks | JWT | Get document chunks with embeddings info |
| Search | |||
POST | /search/query | JWT | Execute a semantic + keyword search query |
GET | /search/sessions | JWT | List search sessions |
GET | /search/sessions/{session_id} | JWT | Get search session details and results |
GET | /search/filters | JWT | Get available search filter options |
| Records Requests | |||
POST | /requests/ | JWT | Create a new records request |
GET | /requests/ | JWT | List records requests |
GET | /requests/stats | JWT | Request processing statistics |
GET | /requests/{request_id} | JWT | Get request details |
PATCH | /requests/{request_id} | JWT | Update request details |
POST | /requests/{request_id}/documents | JWT | Attach documents to a request |
GET | /requests/{request_id}/documents | JWT | List documents attached to a request |
DELETE | /requests/{request_id}/documents/{doc_id} | JWT | Remove document from a request |
POST | /requests/{request_id}/submit-review | JWT | Submit request for review |
POST | /requests/{request_id}/ready-for-release | Reviewer | Mark request ready for release |
POST | /requests/{request_id}/approve | Reviewer | Approve and finalize request |
POST | /requests/{request_id}/reject | Reviewer | Reject request with reason |
GET | /requests/{request_id}/timeline | JWT | Get request timeline / activity log |
POST | /requests/{request_id}/timeline | JWT | Add timeline entry |
GET | /requests/{request_id}/messages | JWT | Get request messages / correspondence |
POST | /requests/{request_id}/messages | JWT | Send a message on a request |
GET | /requests/{request_id}/fees | JWT | Get fee schedule for a request |
POST | /requests/{request_id}/fees | JWT | Add fee line items to a request |
POST | /requests/{request_id}/response-letter | JWT | Generate response letter |
GET | /requests/{request_id}/response-letter | JWT | Get response letter |
PATCH | /requests/{request_id}/response-letter/{letter_id} | JWT | Update response letter |
| Exemptions | |||
POST | /exemptions/rules/ | Admin | Create an exemption rule |
GET | /exemptions/rules/ | JWT | List exemption rules (filterable by state) |
PATCH | /exemptions/rules/{rule_id} | Admin | Update or disable an exemption rule |
POST | /exemptions/scan/{request_id} | JWT | Run exemption scan on request documents |
GET | /exemptions/flags/{request_id} | JWT | Get exemption flags for a request |
PATCH | /exemptions/flags/{flag_id} | Reviewer | Accept, modify, or dismiss an exemption flag |
GET | /exemptions/dashboard | JWT | Exemption analytics dashboard data |
GET | /exemptions/dashboard/accuracy | JWT | Exemption detection accuracy metrics |
GET | /exemptions/dashboard/export | Admin | Export exemption dashboard data |
GET | /exemptions/templates/ | JWT | List disclosure templates |
POST | /exemptions/templates/ | Admin | Create a disclosure template |
GET | /exemptions/templates/{template_id}/render | JWT | Render template with city profile data |
PUT | /exemptions/templates/{template_id} | Admin | Update a disclosure template |
| Departments | |||
POST | /departments/ | Admin | Create a department |
GET | /departments/ | JWT | List departments |
GET | /departments/{dept_id} | JWT | Get department details |
PATCH | /departments/{dept_id} | Admin | Update department details or membership |
DELETE | /departments/{dept_id} | Admin | Delete a department |
| Service Accounts | |||
POST | /service-accounts/ | Admin | Create a service account (returns API key once) |
GET | /service-accounts/ | Admin | List service accounts |
PATCH | /service-accounts/{account_id} | Admin | Update or deactivate a service account |
| Analytics | |||
GET | /analytics/operational | JWT | Operational analytics and dashboard metrics |
| City Profile | |||
GET | /city-profile | JWT | Get the city profile |
POST | /city-profile | Admin | Create the city profile |
PATCH | /city-profile | Admin | Update the city profile |
| Systems Catalog | |||
GET | /catalog/domains | JWT | List catalog domains (functional areas) |
GET | /catalog/systems | JWT | List cataloged municipal systems |
POST | /catalog/load | Admin | Load/reload systems catalog from seed data |
| Notifications | |||
GET | /notifications/templates | JWT | List notification templates |
GET | /notifications/log | JWT | View notification delivery log |
The system uses 29 PostgreSQL tables. Key tables and their relationships:
| Table | Purpose | Key Relationships |
|---|---|---|
users | User accounts and authentication | Has many requests, belongs to department |
departments | Organizational departments | Has many users, has many data sources |
data_sources | Registered document data sources | Has many documents, belongs to department |
documents | Ingested document records | Belongs to data source, has many chunks |
document_chunks | Text chunks with vector embeddings | Belongs to document (pgvector column) |
model_registry | AI model compliance tracking | Referenced by audit entries |
search_sessions | Search session containers | Has many queries, belongs to user |
search_queries | Individual search queries | Belongs to session, has many results |
search_results | Search result rankings | Belongs to query, references document chunks |
records_requests | Open records request lifecycle | Has many documents, flags, timeline entries |
request_documents | Documents attached to requests | Join table: requests ↔ documents |
document_cache | Cached document analysis results | Belongs to request |
exemption_rules | Statutory exemption rule definitions | Referenced by exemption flags |
exemption_flags | Flagged exemptions on request documents | Belongs to request, references rule |
disclosure_templates | Response letter disclosure templates | Referenced by response letters |
request_timelines | Request activity timeline entries | Belongs to request |
request_messages | Correspondence on requests | Belongs to request, belongs to user |
response_letters | Generated response letters | Belongs to request |
fee_schedules | Fee schedule definitions | Has many line items |
fee_line_items | Individual fee entries | Belongs to fee schedule |
notification_templates | Email/notification templates | Referenced by notification log |
notification_log | Notification delivery records | References template and user |
prompt_templates | LLM prompt templates | Used by search and exemption modules |
city_profiles | Municipality configuration data | Referenced by template rendering |
system_catalog | Municipal IT systems catalog | Has many connector templates |
connector_templates | Integration connector definitions | Belongs to catalog system |
service_accounts | Machine-to-machine API accounts | Hashed API key, audit-logged |
audit_log | Tamper-evident hash-chained audit log | References user, hash chain to previous entry |
alembic_version | Database migration version tracking | Managed by Alembic |
The following environment variables are set within docker-compose.yml for infrastructure services:
| Service | Variable | Value |
|---|---|---|
postgres | POSTGRES_USER | civicrecords |
postgres | POSTGRES_PASSWORD | civicrecords |
postgres | POSTGRES_DB | civicrecords |
civicrecords/civicrecords) are acceptable because the database is only accessible within the Docker internal network. However, for defense-in-depth, consider changing them in production by overriding the postgres service environment and updating DATABASE_URL accordingly.
Application services (api, worker, beat) load their configuration from the .env file via the env_file directive in docker-compose.yml.
| Platform | Architecture | Docker | GPU Support | Installer | Status |
|---|---|---|---|---|---|
| Ubuntu 22.04 / 24.04 | x86_64 | Docker Engine | ROCm (AMD), CUDA (NVIDIA) | install.sh | Fully supported |
| Debian 12 | x86_64 | Docker Engine | ROCm (AMD), CUDA (NVIDIA) | install.sh | Fully supported |
| RHEL 9 / Rocky 9 | x86_64 | Docker Engine | ROCm (AMD), CUDA (NVIDIA) | install.sh | Community tested |
| Windows 10/11 Pro | x86_64 | Docker Desktop | DirectML (native Ollama) | install.ps1 | Fully supported |
| macOS 13+ (Intel) | x86_64 | Docker Desktop | None (CPU only) | install.sh | Supported |
| macOS 13+ (Apple Silicon) | ARM64 | Docker Desktop | None (CPU only) | install.sh | Supported |