# Database POSTGRES_USER=postgres POSTGRES_PASSWORD=changeme_use_a_strong_password POSTGRES_DB=soroban_registry REPMGR_PASSWORD=changeme_use_a_strong_password PGPOOL_ADMIN_PASSWORD=changeme_use_a_strong_password # Replication monitor configuration (optional outside Docker Compose) DATABASE_PRIMARY_URL=postgresql://postgres:changeme@localhost:5433/soroban_registry DATABASE_REPLICA_URL=postgresql://postgres:changeme@localhost:5434/soroban_registry DATABASE_REPLICATION_LAG_THRESHOLD_MS=100 # API # # DATABASE_URL is required by the backend at BOTH compile time and runtime. # SQLx's `query!` / `query_as!` / `query_scalar!` macros connect to this # database during `cargo build` to verify SQL against the live schema, so # the database must exist AND have all migrations applied before building. # # If you cannot reach a database at build time (e.g. some CI runners), # set SQLX_OFFLINE=true and commit `backend/.sqlx/` generated by # `cargo sqlx prepare`. See README "Running From Source" for details. DATABASE_URL=postgresql://postgres:changeme@localhost:5432/soroban_registry # SQLX_OFFLINE=true RUST_LOG=info JWT_SECRET=changeme_use_at_least_32_chars_of_random_data PORT=3001 # Data encryption (#895) # ----------------------------------------------------------------------------- # At-rest field encryption uses AES-256-GCM. Keys are loaded from the # environment ONLY (never committed to config files) so they can be injected by # a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager, etc.). # # ENCRYPTION_KEYS is a comma-separated list of `id:base64key` entries. Each key # must decode to exactly 32 bytes. Generate one with: # openssl rand -base64 32 # # Keep retired keys in the list after a rotation so previously-encrypted rows # stay readable; ENCRYPTION_ACTIVE_KEY_ID selects which key signs new writes. # If ENCRYPTION_KEYS is unset, field encryption runs in pass-through mode # (development only) and logs a warning. # ENCRYPTION_KEYS=key-2026-06:BASE64_32_BYTE_KEY,key-2026-01:OLD_BASE64_KEY # ENCRYPTION_ACTIVE_KEY_ID=key-2026-06 # HTTPS / TLS in transit (#895) # HSTS is enabled by default. Set FORCE_HTTPS=true behind a TLS-terminating # proxy/load balancer to redirect plaintext (X-Forwarded-Proto: http) requests. # HSTS_ENABLED=true # HSTS_MAX_AGE_SECS=63072000 # HSTS_INCLUDE_SUBDOMAINS=true # FORCE_HTTPS=false # # Encrypt traffic to Postgres by appending sslmode to DATABASE_URL in non-local # deployments, e.g. ...?sslmode=verify-full # Frontend NEXT_PUBLIC_API_URL=http://localhost:3001 # CORS — comma-separated list of allowed browser origins (scheme + host + port, # no trailing slash, no path). Each entry must match a Request Origin exactly. # Example: ALLOWED_ORIGINS=http://localhost:3000,https://soroban-registry.vercel.app # If unset, defaults to: http://localhost:3000,https://soroban-registry.vercel.app ALLOWED_ORIGINS=http://localhost:3000 # AI-Powered Contract Assistant (Issue #643) # Provider: openai or anthropic AI_PROVIDER=openai OPENAI_API_KEY=your-openai-api-key # Or for Anthropic # ANTHROPIC_API_KEY=your-anthropic-api-key OPENAI_MODEL=gpt-4-turbo-preview # Anthropic model: claude-3-opus-20240229, claude-3-sonnet-20240229 ANTHROPIC_MODEL=claude-3-opus-20240229 AI_TIMEOUT=60 AI_MAX_RETRIES=3 AI_BASE_URL= # Real-Time Contract State Monitor (Issue #647) - Polling interval (seconds) STATE_POLL_INTERVAL=5 # Marketplace — license signing key (Phase 1) # # Ed25519 32-byte seed, base64-encoded. Generate with: # openssl rand -base64 32 # All marketplace license issuance/validation endpoints return 503 until # this is set. Rotating the value invalidates every issued license token, # so treat it like a long-lived secret. MARKETPLACE_LICENSE_SIGNING_KEY= # Marketplace — Stripe (Phase 2) # # Use sk_test_… for development; sk_live_… for production. The same # code path handles both — there is no live-mode guard, so be careful # which key you ship to which environment. # * SECRET_KEY — server-to-Stripe API auth (Bearer / Basic auth) # * WEBHOOK_SECRET — whsec_… string from the Stripe dashboard, # used to HMAC-verify incoming webhook events. # Checkout + webhook endpoints return 503 until both are set. MARKETPLACE_STRIPE_SECRET_KEY= MARKETPLACE_STRIPE_WEBHOOK_SECRET= # Marketplace — USDC on Stellar (Phase 3) # # Single-platform-wallet model: all USDC payments go to one receiving # address. Per-publisher wallets are out of scope for v1. # * RECEIVING_ADDRESS — Stellar account that holds platform USDC. # Must be a 56-char StrKey starting with 'G'. # * ASSET_ISSUER — USDC issuer for the target network. On Stellar # public network this is Circle's issuer # (GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN). # Test networks use a different issuer; check Circle docs. # * NETWORK — 'testnet' or 'public'. Defaults to 'testnet'. # Intent + confirm endpoints return 503 until RECEIVING_ADDRESS and # ASSET_ISSUER are set. MARKETPLACE_USDC_RECEIVING_ADDRESS= MARKETPLACE_USDC_ASSET_ISSUER= MARKETPLACE_USDC_NETWORK=testnet # Marketplace — USDC indexer scanner (Phase 3, indexer crate) # # The indexer's USDC scanner polls Horizon for payments to # MARKETPLACE_USDC_RECEIVING_ADDRESS and POSTs each match to # /api/marketplace/usdc/confirm. The scanner is opt-in — it only # starts when the receiving address above is set. # * HORIZON_URL — Stellar Horizon endpoint (auto-defaulted from # NETWORK if unset). # * REGISTRY_BASE_URL — base URL the indexer uses to reach the API # (e.g. http://localhost:3001 in dev, internal service URL in # prod). Must NOT include a trailing slash. # * INDEXER_API_TOKEN — bearer token the scanner sends to the # confirm endpoint. Must be a valid JWT issued by the same # auth path as regular publishers; the publisher identified by # the token will be recorded in license metadata as # "confirmed_by". Issuing/rotating this token is an operator # task — there's no auto-mint. MARKETPLACE_HORIZON_URL= MARKETPLACE_REGISTRY_BASE_URL=http://localhost:3001 MARKETPLACE_INDEXER_API_TOKEN= # Already existing variables... # Redis Cache (optional — enables L2 distributed caching) # REDIS_ENABLED=true # REDIS_URL=redis://localhost:6379 # CACHE_ENABLED=true # CACHE_MAX_CAPACITY=10000 # CONTRACTS_CACHE_TTL=3600 # contract metadata TTL in seconds (default: 1h) # ABI_CACHE_TTL=86400 # ABI TTL in seconds (default: 24h) # SEARCH_CACHE_TTL=300 # search results TTL in seconds (default: 5m) # STATS_CACHE_TTL=300 # stats/analytics TTL in seconds (default: 5m) # VERIFICATION_CACHE_MAX_CAPACITY=10000 # verification cache max capacity (default: CACHE_MAX_CAPACITY) # VERIFICATION_CACHE_TTL=604800 # verification result TTL in seconds (default: 7d) # Alertmanager (optional) # SLACK_WEBHOOK_URL= # PAGERDUTY_SERVICE_KEY= # DB pool tuning — Issue #876: Connection pooling and optimization # All values are optional; defaults shown below. # # Pool sizing (Rust API: min 10, max 50; Indexer: min 2, max 10) # DB_MIN_POOL_SIZE=10 # Minimum connections kept open at all times # DB_MAX_POOL_SIZE=20 # Maximum connections (hard-capped at 50; auto: cpu*2, min 10) # # Connection lifecycle # DB_ACQUIRE_TIMEOUT_SECS=30 # Seconds to wait for a free connection before erroring # DB_IDLE_TIMEOUT_SECS=600 # Idle connections are closed after this many seconds (10 min) # DB_MAX_LIFETIME_SECS=1800 # Connections are recycled after this many seconds (30 min) # # Query protection # DB_QUERY_TIMEOUT_MS=30000 # PostgreSQL statement_timeout per connection (30 s) # # Prepared statement cache (Rust API only) # DB_STATEMENT_CACHE_CAPACITY=250 # LRU cache size per connection # # Slow query logging (all services) # DB_SLOW_QUERY_THRESHOLD_MS=100 # Queries slower than this emit a WARN log # ----------------------------------------------------------------------------- # Observability # ----------------------------------------------------------------------------- # Grafana admin password. Required by the grafana service in docker-compose.yml; # `docker-compose up` fails hard if this is unset. Log in at http://localhost:3003 # with user `admin` and this password. GRAFANA_ADMIN_PASSWORD=changeme_use_a_strong_password # Rate Limiting — Issue #891 / #1045 # --------------------------------------------------------------------------- # The sliding-window rate limiter is per-instance (not distributed). # For true distributed limiting, point the limiter at a shared Redis store. # # General per-IP / per-API-key limits # RATE_LIMIT_ANON_PER_MINUTE=1000 # Anonymous (IP-keyed) requests per window # RATE_LIMIT_AUTH_PER_MINUTE=1000 # Authenticated (token-keyed) requests per window # RATE_LIMIT_WINDOW_SECONDS=60 # Window duration in seconds (default: 60) # # General write-endpoint limits (POST/PUT/PATCH/DELETE) # RATE_LIMIT_WRITE_ANON_PER_HOUR=100 # Anonymous write requests per window # RATE_LIMIT_WRITE_AUTH_PER_HOUR=300 # Authenticated write requests per window # # Issue #1045: per-endpoint tighter limits for publish and search. # These override the general read/write limits for the specific endpoints. # # Publish (POST /api/contracts): # Protects the registry from junk-contract spam. Anonymous callers are # capped very tightly; authenticated publishers get a higher allowance. # RATE_LIMIT_PUBLISH_ANON_PER_WINDOW=5 # Default: 5 publishes per window (anon) # RATE_LIMIT_PUBLISH_AUTH_PER_WINDOW=30 # Default: 30 publishes per window (authenticated) # # Search / list (GET /api/contracts, /api/v1/contracts/search, etc.): # Prevents aggressive scraping from degrading search for everyone. # RATE_LIMIT_SEARCH_ANON_PER_WINDOW=100 # Default: 100 search queries per window (anon) # RATE_LIMIT_SEARCH_AUTH_PER_WINDOW=500 # Default: 500 search queries per window (authenticated) # # Enterprise custom limit (overrides tier defaults for the Enterprise plan) # ENTERPRISE_RATE_LIMIT_PER_WINDOW=100000 # # Trusted IPs bypass rate limiting entirely (comma-separated) # RATE_LIMIT_TRUSTED_IPS=10.0.0.1,10.0.0.2 # # Trusted API keys bypass rate limiting entirely (comma-separated) # RATE_LIMIT_TRUSTED_API_KEYS=my-internal-key # # Per-API-key overrides: KEY=LIMIT pairs (comma-separated) # RATE_LIMIT_API_KEY_LIMITS=key1=500,key2=2000