# =========================================== # CaseCite - Environment Configuration # =========================================== # Copy this file to .env and fill in your values. # # Lines that are commented out are OPTIONAL: uncomment one only when you have a # real value for it. Never leave an example value in an active line — the app # would treat it as configuration. # # This one file is read by docker compose (DOMAIN, POSTGRES_PASSWORD, ...), by # the app's settings, and by a few modules directly from the environment # (CORS_ORIGINS, ALLOWED_HOSTS, TRUSTED_PROXIES, DB_SSL). # ----- AI/LLM APIs ----- # A server-level key is shared by every user of the instance. It is optional: # leave all of these empty and each user saves their own key under Settings # (bring your own key). To get going quickly, set ONE of them. # # What these keys imply: questions and document excerpts are sent to the # provider you configure, and uploaded document text is sent to the embeddings # provider. See docs/subprocessors.md. # OpenAI (chat and, by default, embeddings) OPENAI_API_KEY= # OpenAI-compatible endpoint for CHAT (Ollama, vLLM, LM Studio, a gateway): # OPENAI_BASE_URL=http://ollama:11434/v1 # OpenAI-compatible endpoint for EMBEDDINGS. Set this as well as OPENAI_BASE_URL # for a deployment where no document text leaves your network; without it, # embeddings go to api.openai.com even when chat is local. # EMBEDDING_BASE_URL=http://ollama:11434/v1 # EMBEDDING_MODEL=nomic-embed-text # EMBEDDING_DIMENSIONS=768 (only needed with VECTOR_DB=pinecone) # Model overrides (defaults are in backend/app/config.py): # OPENAI_CHAT_MODEL= # OPENAI_UTILITY_MODEL= # OPENAI_EMBEDDING_MODEL= # Context window (tokens) of the chat model — only needed for custom/local # endpoints (OPENAI_BASE_URL) whose model names don't reveal it. Long-document # drafting budgets how much reference text each section call carries against # this. Unset: inferred from the model name (gpt-5 → 400k, gpt-4o → 128k, ...). # LLM_CONTEXT_WINDOW=128000 # Anthropic Claude (alternative LLM) # ANTHROPIC_API_KEY= # ANTHROPIC_MODEL= # ----- Embeddings (optional alternatives to OpenAI) ----- # Changing the embedding model after documents are indexed requires a reindex. # Voyage AI — voyage-law-2 is trained on legal text. Set BOTH lines. # VOYAGE_API_KEY= # EMBEDDING_MODEL=voyage-law-2 # Cohere (alternative embeddings provider) # COHERE_API_KEY= # ----- AI provider allowlist (optional) ----- # When enabled, every embedding call and every LLM call (research chat, # contract analysis, drafting, strategy, clause intelligence, # case-law research) is refused unless its provider is in the list — see # docs/subprocessors.md. # Provider names: openai, anthropic, google, voyage, cohere, # self_hosted (an endpoint on a private address); any other OpenAI-compatible # endpoint is listed by its hostname (e.g. api.groq.com). The variable name # is historical — this is an allowlist, not a HIPAA compliance feature. # HIPAA_ENFORCEMENT_ENABLED=true # APPROVED_AI_PROVIDERS=["anthropic","self_hosted"] # ----- Legal Research Data ----- # CourtListener (free case-law database). The app boots without a token, but # citation check requires one and the other case-law tools are heavily # rate-limited without it. Free token: https://www.courtlistener.com/help/api/ # COURTLISTENER_API_TOKEN= # ----- Vector Database ----- # Default is ChromaDB (local, no config needed) # Set to "pinecone" to use Pinecone cloud (document text is then stored with # Pinecone as vector metadata — see docs/subprocessors.md) VECTOR_DB=chroma # Pinecone (if using) # PINECONE_API_KEY= # PINECONE_ENVIRONMENT=us-east-1 # PINECONE_INDEX_NAME=casecite-legal # ----- Data Source Connectors ----- # Uncomment and fill in only the integrations you use. Each also needs its # redirect URI (see "OAuth Redirect URIs" below). # Google Drive # GOOGLE_CLIENT_ID= # GOOGLE_CLIENT_SECRET= # Microsoft OneDrive/SharePoint # MICROSOFT_CLIENT_ID= # MICROSOFT_CLIENT_SECRET= # Your Azure AD tenant ID (a GUID). Required for Azure AD SSO. # MICROSOFT_TENANT_ID= # Azure AD SSO sign-in (backend API only; uses the same app registration). # Roles ALWAYS come from the local user record, never from the Azure token. # First-time SSO users are created with AZURE_SSO_DEFAULT_ROLE (viewer, # paralegal, or attorney — admin is refused). Set AZURE_SSO_AUTO_PROVISION=false # to allow sign-in only for accounts an admin has already invited. # AZURE_SSO_AUTO_PROVISION=true # AZURE_SSO_DEFAULT_ROLE=viewer # Box # BOX_CLIENT_ID= # BOX_CLIENT_SECRET= # Experimental connectors — iManage and NetDocuments. Both are implemented # against the vendors' published APIs but have NOT been tested against a live # tenant. They are not registered (absent from the connector list, every route # refused) until this is true. If you try them, the maintainer welcomes a # report of what worked and what did not. EXPERIMENTAL_CONNECTORS=false # NetDocuments (experimental; non-US data centres: also set the two hosts) # NETDOCUMENTS_CLIENT_ID= # NETDOCUMENTS_CLIENT_SECRET= # NETDOCUMENTS_VAULT_HOST=https://vault.netvoyage.com # NETDOCUMENTS_API_HOST=https://api.vault.netvoyage.com # iManage (experimental, see EXPERIMENTAL_CONNECTORS above) # IMANAGE_CLIENT_ID= # IMANAGE_CLIENT_SECRET= # IMANAGE_BASE_URL= # Filevine (API key + secret; signed session handshake, not OAuth). # The key is firm-wide: a sync sees every project it can reach, regardless of # the syncing user's own Filevine permissions. It therefore stays off until # FILEVINE_ENABLED=true, and is then usable only by users with the # "Instance settings" (admin.settings) permission. FILEVINE_ENABLED=false # FILEVINE_API_KEY= # FILEVINE_API_SECRET= # FILEVINE_BASE_URL=https://api.filevine.io # Clio (EU tenants: set CLIO_BASE_URL=https://eu.app.clio.com) # CLIO_CLIENT_ID= # CLIO_CLIENT_SECRET= # CLIO_BASE_URL=https://app.clio.com # Dropbox # DROPBOX_APP_KEY= # DROPBOX_APP_SECRET= # ===================================================== # FILE PICKER APIs (NO OAUTH VERIFICATION REQUIRED!) # ===================================================== # These use limited scopes that don't need Google/Microsoft app verification. # Users can select specific files without you needing 100+ user verification. # Google Picker API # Get these from Google Cloud Console > APIs & Services > Credentials: # 1. Create an API Key (restrict to Picker API). It is handed to the browser as # the Picker developerKey, so also restrict it by HTTP referrer to your domain. # 2. Get your Project Number (from project settings) # GOOGLE_API_KEY= # GOOGLE_APP_ID= # Microsoft OneDrive Picker - uses same MICROSOFT_CLIENT_ID above # Box Content Picker - uses same BOX_CLIENT_ID above # Dropbox Chooser - uses same DROPBOX_APP_KEY above # ----- Application Settings ----- DEBUG=false # "development" here so a fresh copy of this file runs locally (the app refuses # to start with ENVIRONMENT=production and DEBUG=true, which the test suite and # `DEBUG=true uvicorn` both set). On a server set ENVIRONMENT=production. ENVIRONMENT=development # Name shown in the API title and /health; the frontend has its own branding. # APP_NAME=CaseCite # Human-facing product name used in emails and server-rendered text. # APP_DISPLAY_NAME=CaseCite # Path prefix of every API route. The proxies (Caddyfile, nginx.unified.conf) # and the frontend assume /api/v1 — change them together or not at all. # API_PREFIX=/api/v1 # Uvicorn worker processes. Must stay 1 (or unset): the session store and # connector caches are process-local, and the app refuses any other value. # Scale with a single replica per instance instead. # UVICORN_WORKERS=1 # ----- Security (REQUIRED for production) ----- # These THREE keys are mandatory in production and the app refuses to start # without real values. The placeholders below intentionally fail validation # (they're rejected as placeholders) — you MUST replace all three before the # first `docker compose up`. Generate each with the command shown, then paste: # # SECRET_KEY / AUDIT_HMAC_KEY : openssl rand -hex 32 # ENCRYPTION_SALT : openssl rand -hex 16 # # WARNING: never change ENCRYPTION_SALT or SECRET_KEY after first run — data # encrypted at rest (API keys, connector tokens) becomes unrecoverable. SECRET_KEY=REPLACE-ME-openssl-rand-hex-32 ENCRYPTION_SALT=REPLACE-ME-openssl-rand-hex-16 AUDIT_HMAC_KEY=REPLACE-ME-openssl-rand-hex-32 # Rotating AUDIT_HMAC_KEY? Put the OLD value here so existing audit entries # still verify at startup (otherwise the app treats them as tampered and # refuses to boot). Remove it once those logs age out of AUDIT_RETENTION_DAYS. # AUDIT_HMAC_KEY_PREVIOUS= # ----- Bundled production stack (docker-compose.prod.yml) — REQUIRED there ----- # The bundled stack (Caddy + Postgres + Redis) needs these three. Uncomment and # set them when using `docker compose -f docker-compose.prod.yml up -d`. # # DOMAIN: the public hostname Caddy serves. Use DOMAIN=localhost to try the # stack on your own machine — CORS_ORIGINS and ALLOWED_HOSTS are derived from # DOMAIN automatically by the compose file (set ALLOWED_HOSTS yourself only to # serve additional hostnames). # DOMAIN=localhost # # Generate each password with: openssl rand -hex 32 (never use a default) # POSTGRES_PASSWORD= # REDIS_PASSWORD= # Database role name. Leave unset for the default; the backup and restore # scripts read the same variable, so change it in one place only. # POSTGRES_USER=casecite # # Which image the stack runs. Defaults to the image CI publishes; pin # CASECITE_TAG to a commit SHA for reproducible deploys and one-line rollbacks # (`CASECITE_TAG= docker compose -f docker-compose.prod.yml up -d`). # Forks: point CASECITE_IMAGE at your own registry, or build from source with # `up -d --build`. # CASECITE_IMAGE=ghcr.io/thomasunise/casecite # CASECITE_TAG=latest # # Backups (scripts/backup_database.sh, run on the host from this directory). # Its settings — BACKUP_ENCRYPTION_KEY, BACKUP_DIR, BACKUP_RETENTION_DAYS, # S3_BUCKET — belong in the backup job's own environment (cron, or a root-only # file it sources), NOT in this file: everything here is injected into the # backend container, and the backup key must not live beside the data it # protects. See the header of scripts/backup_database.sh. # Audit-log retention in days (older files are gzip-compressed; 2x older deleted) # AUDIT_RETENTION_DAYS=90 # What to do when an audit entry cannot be written to disk (full volume, bad # permissions). false (default): log at CRITICAL, keep serving. true: fail the # request that produced the event, so nothing happens without an audit record. # AUDIT_FAIL_CLOSED=false # Per-request timeout (seconds) for every LLM/embedding call, plus two SDK # retries. Raise it for slow self-hosted models; lower it only if you never run # long contract reviews or multi-page drafts. # API_TIMEOUT=180 # Trusted proxy IPs/CIDRs for accurate client IP resolution (comma-separated). # Set to the reverse-proxy address(es). MUST be IPs/CIDRs, not hostnames. # The root docker-compose.yml (Coolify/Traefik) REQUIRES this to be set — it # refuses to start without it, because no default is correct behind a proxy. # # IMPORTANT for Coolify/Traefik (or any external reverse proxy): the proxy # connects from its Docker-network IP, NOT 127.0.0.1. If that IP is not trusted, # every user resolves to the proxy's IP, all rate-limit/lockout buckets collapse # into one, and 3 failed logins by ANYONE lock out the whole firm. Set this to # the proxy's Docker network CIDR (find it with: # docker network inspect -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}') # docker-compose.prod.yml pins its network to 172.28.0.0/24 and sets this for you. # TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12 # ----- Registration, MFA and sessions ----- # Off by default in production: an internet-reachable instance with open # registration lets anyone create a full-access account. Enable for trusted nets. # ALLOW_REGISTRATION=false # REQUIRED in production to create the FIRST (admin) account, so an attacker who # reaches a fresh instance first can't seize the admin slot. Generate with # `openssl rand -hex 16` and pass it as `bootstrap_token` in the first # /auth/register call (see "Create the admin account" in docs/deployment.md). REGISTRATION_BOOTSTRAP_TOKEN= # Require every password account to enrol TOTP MFA before using the API. # REQUIRE_MFA=false # A sign-in ends this long after it started, however often it is refreshed. # SESSION_ABSOLUTE_TIMEOUT_HOURS=8 # A session with no request for this long is ended (0 disables the idle check). # SESSION_IDLE_TIMEOUT_MINUTES=120 # Access-token lifetime; the browser refreshes it silently within a session. # ACCESS_TOKEN_EXPIRE_MINUTES=60 # End a session when the client IP changes. Off by default: it signs out users # on mobile networks. # ENFORCE_SESSION_IP_BINDING=false # Max concurrently-executing background jobs (bounds memory/connection use) # JOB_MAX_CONCURRENCY=5 # Documents a tabular review (/review-tables) extracts from concurrently per run # REVIEW_TABLE_CONCURRENCY=4 # Deep research (Research view): sub-questions researched concurrently per run, # and the caps on plan size, tool calls, passages and follow-up depth # DEEP_RESEARCH_CONCURRENCY=3 # DEEP_RESEARCH_MAX_SUBQUESTIONS=8 # DEEP_RESEARCH_MAX_TOOL_CALLS=24 # DEEP_RESEARCH_MAX_PASSAGES=60 # DEEP_RESEARCH_FOLLOWUP_DEPTH=1 # Workflows (/workflows): steps per workflow, documents one step may analyse or # extract from, and citations one citation-check step may scan # WORKFLOW_MAX_STEPS=12 # WORKFLOW_MAX_DOCUMENTS_PER_STEP=25 # WORKFLOW_MAX_CITATIONS_PER_STEP=10 # A step's stored output is cut past this many bytes (the fields cut are # listed on the output); email the runner and approvers when a run completes, # fails, waits for approval or is rejected (needs SENDGRID_API_KEY; each user # can opt out in Settings) # WORKFLOW_MAX_OUTPUT_BYTES=524288 # WORKFLOW_NOTIFICATIONS=false # ----- Redis and database (REQUIRED for production) ----- # docker-compose.prod.yml builds both URLs from POSTGRES_PASSWORD and # REDIS_PASSWORD above — leave these commented with the bundled stack. Set them # only for external services (the root docker-compose.yml, or bare metal). # # Redis holds token revocations, lockout counters and rate limits. Run it with # `maxmemory-policy noeviction`: an LRU policy silently drops those keys. # REDIS_URL=redis://:password@host:6379/0 # # Format: postgresql+asyncpg://user:password@host:5432/database # (unset = SQLite, development only) # DATABASE_URL=postgresql+asyncpg://casecite:password@host:5432/casecite # # PostgreSQL TLS. require = verified TLS (use for any database reached over a # network). internal = no TLS, only for a Postgres on the same private Docker # network (the bundled stack sets this itself). # DB_SSL=require # CA certificate for DB_SSL=require when the server uses a private CA # DB_SSL_CA=/path/to/ca-cert.pem # Enable SQL query logging (development only) # SQL_DEBUG=false # Skip Alembic migrations at container start (schema managed out-of-band) # SKIP_MIGRATIONS=false # Maximum upload size in bytes (the proxies cap request bodies at 100 MB) # MAX_UPLOAD_SIZE=104857600 # The four settings below are OPTIONAL with docker-compose.prod.yml: they # default sensibly from DOMAIN when unset (the compose file derives # CORS_ORIGINS and ALLOWED_HOSTS from DOMAIN). Leave them commented for a # localhost trial. Set them explicitly only for custom setups — e.g. the root # docker-compose.yml behind Coolify/Traefik, where CORS_ORIGINS and # ALLOWED_HOSTS are required directly. # Frontend URL (used in password reset emails and in the approval link the # Word add-in opens for its device-code sign-in) # FRONTEND_URL=https://your-domain.com # Word add-in (word-addin/). The origin the add-in's task pane is served from; # it is added to the CORS allow list. Leave unset if you do not deploy the # add-in. Device codes for its sign-in live DEVICE_CODE_TTL seconds (default # 600) and may be polled every DEVICE_CODE_INTERVAL seconds (default 5). # WORD_ADDIN_ORIGIN=https://addin.your-domain.com # DEVICE_CODE_TTL=600 # DEVICE_CODE_INTERVAL=5 # CORS allowed origins (comma-separated) # Example: https://app.yourdomain.com,https://admin.yourdomain.com # CORS_ORIGINS=https://your-domain.com # Allowed hosts (comma-separated) # ALLOWED_HOSTS=your-domain.com,www.your-domain.com # Force HTTPS redirect at application level (if not handled by reverse proxy) # FORCE_HTTPS=true # ----- Data-at-rest acknowledgement (REQUIRED for production) ----- # What you are acknowledging: the app encrypts BYOK API keys and uploaded # document originals at the APPLICATION layer (AES-256-GCM) on the data volume, # but DERIVED privileged text — vector chunks, extracted clause text, and chat # history — is stored in cleartext by the vector store and database. Host-level # full-disk encryption is the OPERATOR'S responsibility: encrypt the data volume # (LUKS / BitLocker / FileVault, or your cloud provider's disk encryption), then # set this to true to confirm. Production refuses to start until acknowledged. DISK_ENCRYPTION_ACKNOWLEDGED=false # ----- OAuth Redirect URIs (one per connector you enable) ----- # Replace your-domain.com with your hostname and register the same URI with # the provider. # GOOGLE_REDIRECT_URI=https://your-domain.com/api/v1/connectors/google/callback # MICROSOFT_REDIRECT_URI=https://your-domain.com/api/v1/connectors/microsoft/callback # BOX_REDIRECT_URI=https://your-domain.com/api/v1/connectors/box/callback # NETDOCUMENTS_REDIRECT_URI=https://your-domain.com/api/v1/connectors/netdocuments/callback # IMANAGE_REDIRECT_URI=https://your-domain.com/api/v1/connectors/imanage/callback # DROPBOX_REDIRECT_URI=https://your-domain.com/api/v1/connectors/dropbox/callback # CLIO_REDIRECT_URI=https://your-domain.com/api/v1/connectors/clio/callback # ----- Email (SendGrid) — optional ----- # Sends password-reset emails. Leave blank to disable (users can still change # their password while signed in; an admin can issue a new temporary one). SENDGRID_API_KEY= # ----- Advanced tuning (optional) ----- # Every value below is the built-in default; uncomment only to change it. # Retrieval. Per-user values set in Settings override these. # CHUNK_SIZE=512 # CHUNK_OVERLAP=128 # TOP_K=10 # SIMILARITY_THRESHOLD=0.25 # Keyword (BM25) rescoring of the vector candidates, and its weight in the blend # HYBRID_SEARCH_ENABLED=true # KEYWORD_WEIGHT=0.3 # Candidate pool size for rescoring. RERANK_ENABLED only has an effect when the # optional sentence-transformers package is installed (it is not, by default). # RERANK_TOP_K=20 # RERANK_ENABLED=true # GEMINI_MODEL=gemini-3.1-pro-preview # Provider calls: retries and the per-credential circuit breaker # API_RETRY_ATTEMPTS=3 # API_RETRY_DELAY=1.0 # CIRCUIT_BREAKER_THRESHOLD=5 # CIRCUIT_BREAKER_TIMEOUT=60.0 # REDIS_SOCKET_TIMEOUT=5.0 # Upload safety limits: reject decompression bombs and bound parsing/OCR work # EXTRACTION_MAX_UNCOMPRESSED_BYTES=524288000 # EXTRACTION_MAX_ZIP_MEMBERS=10000 # EXTRACTION_MAX_PDF_PAGES=5000 # EXTRACTION_MAX_OCR_PAGES=300 # How much of a long filing/opinion is read (windows of 60k-80k characters); # anything beyond is reported to the user as not covered. # RESEARCH_MAX_WINDOWS_PER_OPINION=6 # Background jobs: result lifetimes (seconds), size cap (bytes), in-memory cap # JOB_RESULT_TTL=1800 # JOB_FAILED_TTL=86400 # JOB_DEAD_LETTER_TTL=604800 # JOB_MAX_RESULT_SIZE=1048576 # JOB_SHUTDOWN_TIMEOUT=30 # JOB_MEMORY_MAX_JOBS=200 # Audit log. Production refuses to start when the chain fails verification; # set false only to boot while investigating (tamper detection is then off). # AUDIT_HALT_ON_TAMPERING=true # Data locations inside the container (the compose files mount /app/data) # UPLOAD_DIR=./data/uploads # CHROMA_PERSIST_DIR=./data/chroma # From-address for password-reset emails # NOREPLY_EMAIL=noreply@your-domain.com # Key rotation: the previous ENCRYPTION_SALT, set by scripts/rotate_encryption_key.py # while stored secrets are re-encrypted (see docs/secret-rotation.md) # ENCRYPTION_SALT_PREVIOUS= # Development only: skips CSRF validation, and only when DEBUG=true as well. # CSRF_DISABLED=false