Environment Configuration Strategy¶
This document explains the environment file strategy used in the Juddges App.
File Structure¶
The project uses a simple two-file strategy for environment configuration:
1. .env.example (COMMITTED)¶
- Purpose: Template showing all available environment variables
- Tracked: YES - committed to git
- Contains: Variable names with NO actual values (or example values)
- Usage: Reference for developers to create their own
.envfile
2. .env (GITIGNORED)¶
- Purpose: Contains all actual configuration values
- Tracked: NO - gitignored (never commit this file)
- Contains: All actual values including secrets, API keys, and configuration
- Usage: Your working configuration file used by Docker Compose and the application
Getting Started¶
For New Developers¶
-
Copy the template:
-
Edit
.envwith your actual values: - Add your Supabase URL and keys
- Add your OpenAI API key
- Add database URLs
- Set port numbers (or keep defaults)
-
Add any other required configuration
-
Start services:
For Production Deployment¶
- Copy
.env.exampleto.envon your production server - Fill in production credentials in
.env - Ensure
.envhas appropriate file permissions (e.g.,chmod 600 .env) - Start services with
docker compose up -d
What Goes Where?¶
.env.example (Committed - Template)¶
Contains variable names and example/documentation values:
- SUPABASE_URL=your_supabase_url_here
- OPENAI_API_KEY=sk-...
- PORT=3007
- ENABLE_CHAT=true
Important: Never put real secrets in .env.example!
.env (Gitignored - Actual Values)¶
Contains your real configuration: - Supabase URL and keys (actual values) - OpenAI API key (actual value) - Database passwords (actual values) - Port numbers (your preferences) - Feature flags (your settings) - All other configuration
Docker Compose Integration¶
Both docker-compose.yml and docker-compose.dev.yml load .env:
If .env doesn't exist, Docker Compose will fail with an error. You must create it from .env.example.
Best Practices¶
DO¶
- ✓ Always copy from
.env.exampleto.envwhen starting - ✓ Keep
.env.exampleup to date with new variables (but no real values) - ✓ Document what each variable does in
.env.exampleusing comments - ✓ Set appropriate file permissions on
.env(e.g.,chmod 600 .env) - ✓ Use strong, unique values for secrets in
.env
DON'T¶
- ✗ NEVER commit
.envto git (it's in .gitignore for a reason) - ✗ Don't put real secrets or passwords in
.env.example - ✗ Don't share your
.envfile with others (they should create their own) - ✗ Don't use
.env.exampledirectly (copy it to.envfirst) - ✗ Don't commit any file with "env" in the name except
.env.example
Updating Configuration¶
Adding a New Variable¶
When you add a new environment variable:
- Add it to your
.envwith the actual value - Add it to
.env.examplewith a placeholder/example value - Commit the
.env.examplechanges (but never.env) - Document the variable in this file if it's complex
Changing Existing Variables¶
Just edit your .env file directly. Changes take effect after restarting the services:
# Restart all services
docker compose down
docker compose up -d
# Or restart specific service
docker compose restart backend
Troubleshooting¶
"Environment variable not set"¶
- Check if the variable exists in your
.envfile - Verify the variable name matches what the application expects
- Ensure there are no typos or extra spaces
"Service can't connect" or "Authentication failed"¶
- Verify secrets in
.envare correct (API keys, passwords, URLs) - Check that URLs don't have trailing slashes unless required
- Ensure Supabase keys match your project
"File not found: .env"¶
- You need to create
.envfrom.env.example: - Then edit
.envwith your actual values
Checking Final Configuration¶
View the final merged configuration (useful for debugging):
Migration from Three-File Strategy¶
If you previously used .env.defaults, .env.local, and .env.secrets:
-
Merge all values into a single
.envfile: -
Remove duplicate lines from
.env(keep the last occurrence of each variable) -
Clean up old files:
-
Update
.env.examplewith all variable names but no real values -
Test your configuration:
Security Best Practices¶
File Permissions¶
# Lock down .env to only your user
chmod 600 .env
# Ensure .env is in .gitignore
grep -q "^\.env$" .gitignore || echo ".env" >> .gitignore
Secrets Management¶
- Never commit
.envto version control - Use different
.envfiles for different environments (dev, staging, prod) - Rotate secrets regularly
- Use strong, random values for passwords and tokens
- Consider using a secrets manager (AWS Secrets Manager, HashiCorp Vault) for production
Required vs Optional Variables (dev vs prod)¶
backend/app/server.py:validate_environment_variables enforces this matrix at
startup. PYTHON_ENV=production switches the stricter prod rules on.
| Variable | Dev | Prod | Notes |
|---|---|---|---|
BACKEND_API_KEY |
Required | Required | Missing → startup aborts (ValueError) in any env. |
OPENAI_API_KEY |
Required | Required | |
SUPABASE_URL |
Required | Required | |
SUPABASE_SERVICE_ROLE_KEY |
Required | Required | |
REDIS_AUTH |
Optional (silent) | Required | Prod startup aborts (SystemExit) if unset — guest sessions would run on an unauthenticated Redis. |
LANGCHAIN_CACHE_DATABASE_URL |
Optional (silent) | Required* | *Satisfied by DATABASE_URL (the cache falls back to it). Prod aborts only if both are unset — a disabled LLM cache doubles OpenAI cost and adds latency. |
MEILISEARCH_SEARCH_KEY / MEILISEARCH_ADMIN_KEY |
Optional (silent) | Required if wired | Prod aborts only when MEILISEARCH_URL is set but no key is configured. |
DATABASE_URL |
Optional | Optional | Also the default source for the LLM cache (above). Without it the checkpointer falls back to in-memory. |
REDIS_HOST / REDIS_PORT |
Optional | Optional | |
MEILISEARCH_URL / MEILISEARCH_INDEX_NAME |
Optional | Optional | |
LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_HOST |
Optional | Optional | Observability only. |
PYTHON_ENV |
Optional | Optional | Selects dev vs prod rules above (production enables strict mode). |
Genuinely-optional misses log at INFO in dev (quiet local startup) and WARNING in prod (so real gaps are visible). Production-required misses are fatal — the process exits rather than silently degrading.
Database connection pooling (DATABASE_URL)¶
DATABASE_URL is consumed by short-lived, per-request paths (LangGraph
checkpointer pool in backend/app/server.py, the LLM cache, health checks,
maintenance VACUUM ANALYZE). For that traffic pattern point it at the Supabase
transaction pooler (host ...pooler.supabase.com, port 6543), not the
direct connection (port 5432):
DATABASE_URL="postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres"
- Transaction mode (6543) returns a connection to the pool after every transaction, so bursty request traffic doesn't exhaust Postgres backends and p99 latency stays flat. This is the default choice for the backend.
- Prepared statements are unsupported in transaction mode. The checkpointer
pool already sets
prepare_threshold=None(seeserver.py, kwargs onAsyncConnectionPool), so it is transaction-pooler-safe out of the box. Any new psycopg pool added againstDATABASE_URLmust do the same. - Use the session pooler (5432) only for a client that genuinely needs
server-side prepared statements or session-scoped state — e.g. if
langgraph-checkpoint-postgresis ever run withoutprepare_threshold=None. Keep that on a separate URL rather than downgrading the sharedDATABASE_URL.
Verify in your deployment: the running backend's
DATABASE_URLhost containspooler.supabase.comand the port is6543. If it points atdb.<ref>...:5432(the direct connection), bursty traffic can exhaust connections under load.
See Also¶
.env.example- Complete reference of all configuration optionsdocker-compose.yml- Production service configurationdocker-compose.dev.yml- Development service configurationCLAUDE.md- Project overview and development commandsseo-metadata.md- SEO/metadata; setNEXT_PUBLIC_SITE_URLin production for canonical/OG/sitemap URLs