# Coves Local Development Environment Configuration # Copy this to .env.dev and fill in your values # # Quick Start: # 1. cp .env.dev.example .env.dev # 2. Generate OAuth key: go run cmd/genjwks/main.go (copy output to OAUTH_PRIVATE_JWK) # 3. Generate cookie secret: openssl rand -hex 32 # 4. make dev-up # Start Docker services # 5. make run # Start the server (uses -tags dev) # ============================================================================= # Dev Mode Quick Reference # ============================================================================= # REQUIRED for local OAuth to work with local PDS: # IS_DEV_ENV=true # Master switch for dev mode # PDS_URL=http://localhost:3001 # Local PDS for handle resolution # PLC_DIRECTORY_URL=http://localhost:3002 # Local PLC directory # APPVIEW_PUBLIC_URL=http://127.0.0.1:8081 # Use IP not localhost (RFC 8252) # # BUILD TAGS: # make run - Runs with -tags dev (includes localhost OAuth resolvers) # make build - Production binary (no dev code) # make build-dev - Dev binary (includes dev code) # ============================================================================= # PostgreSQL Configuration # ============================================================================= POSTGRES_HOST=localhost POSTGRES_PORT=5435 POSTGRES_DB=coves_dev POSTGRES_USER=dev_user POSTGRES_PASSWORD=dev_password # Test database POSTGRES_TEST_DB=coves_test POSTGRES_TEST_USER=test_user POSTGRES_TEST_PASSWORD=test_password POSTGRES_TEST_PORT=5434 # ============================================================================= # PDS Configuration # ============================================================================= PDS_HOSTNAME=localhost PDS_PORT=3001 PDS_SERVICE_ENDPOINT=http://localhost:3000 PDS_DID_PLC_URL=http://plc-directory:3000 PDS_JWT_SECRET=local-dev-jwt-secret-change-in-production PDS_ADMIN_PASSWORD=admin PDS_SERVICE_HANDLE_DOMAINS=.local.coves.dev,.coves.social PDS_PLC_ROTATION_KEY= # ============================================================================= # AppView Configuration # ============================================================================= APPVIEW_PORT=8081 FIREHOSE_URL=ws://localhost:3001/xrpc/com.atproto.sync.subscribeRepos PDS_URL=http://localhost:3001 APPVIEW_PUBLIC_URL=http://127.0.0.1:8081 # ============================================================================= # Jetstream Configuration (multi-feed) # ============================================================================= # Semicolon-separated = entries. Every consumer runs once per # feed; per-consumer collection filters are derived in code, so base URLs carry # no query string (path optional). Local dev uses the dev-stack Jetstream only. JETSTREAM_FEEDS=self=ws://localhost:6008 # ============================================================================= # Identity Resolution # ============================================================================= IDENTITY_CACHE_TTL=24h # Process-local negative cache for failed/unverified DID resolutions. Default # 90s; must stay BELOW REDRIVE_INTERVAL (set it to 1s if you shorten that to 5s). # IDENTITY_NEGATIVE_CACHE_TTL=90s PLC_DIRECTORY_URL=http://localhost:3002 # Dev/CI only (refused unless IS_DEV_ENV=true): sends the handle-verification # GET for these suffixes to the local PDS with the handle in the Host header, # so handles verify with no DNS and no TLS. Suffixes must start with a dot. HANDLE_WELL_KNOWN_HOSTS=.local.coves.dev=localhost:3001,.coves.social=localhost:3001 # ============================================================================= # OAuth Configuration (MUST GENERATE YOUR OWN) # ============================================================================= # Generate with: go run cmd/genjwks/main.go OAUTH_PRIVATE_JWK= # Generate with: openssl rand -hex 32 OAUTH_COOKIE_SECRET= # OAuth Confidential Client Configuration (optional, for testing) # If both are set, Coves becomes a confidential OAuth client with 90-day session lifetime # (Public clients are limited to 14 days by the auth server) # Generate keys with: go run ./cmd/tools/generate-oauth-key # P-256 private key in multibase format (z-prefixed base58btc) # OAUTH_CLIENT_PRIVATE_KEY=z... # Key identifier (arbitrary string, used in JWT header) # OAUTH_CLIENT_KEY_ID=coves-dev-key-1 # ============================================================================= # Development Settings # ============================================================================= ENV=development NODE_ENV=development IS_DEV_ENV=true LOG_LEVEL=debug LOG_ENABLED=true # Security settings (ONLY for local dev - set to false in production!) # IS_DEV_ENV=true is what makes SKIP_DID_WEB_VERIFICATION acceptable: with it # false, the server refuses to start while did:web verification is disabled, and # it additionally requires OAUTH_SEAL_SECRET, ENCRYPTION_KEY, CURSOR_SECRET, JETSTREAM_FEEDS, # APPVIEW_PUBLIC_URL, and a non-localhost PDS_URL. # AUTH_SKIP_VERIFY and HS256_ISSUERS are read by no Go code in this repository; # they are inert and nothing gates them. SKIP_DID_WEB_VERIFICATION=true AUTH_SKIP_VERIFY=true HS256_ISSUERS=http://localhost:3001 # HTTP timeouts and database pool sizing use safe defaults in dev and rarely # need overriding here. See .env.prod.example for the full list (HTTP_*, DB_*). # ============================================================================= # Image Proxy Configuration # ============================================================================= # On-the-fly image resizing with disk caching. Every image URL the AppView # BUILDS - post/comment embeds, link-card thumbnails, avatars, banners - comes # from IMAGE_PROXY_BASE_URL (or IMAGE_PROXY_CDN_URL, which wins when set). The # exception is images inside resolved Bluesky quote embeds, which are passed # through from bsky's CDN; see docs/PRD_CSAM_SCANNING.md. IMAGE_PROXY_ENABLED=true # Point at the Caddy origin (:8080), not the backend port (:8081), so emitted # URLs are reachable from mobile devices and emulators: mobile's dev apiUrl is # also http://localhost:8080 and `make mobile-setup` reverses tcp:8080. The dev # Caddyfile routes /img/* to the backend, and web is served same-origin at :8080. # # This REQUIRES the dev Caddy to be running on :8080 (`make web-proxy`, or # `make run-web` which expects it). A bare `make run` starts only the backend on # :8081 and no proxy, so these URLs point at a closed port and every image 404s # - set IMAGE_PROXY_BASE_URL=http://127.0.0.1:8081 for that workflow. IMAGE_PROXY_BASE_URL=http://localhost:8080 IMAGE_PROXY_CACHE_PATH=./cache/images IMAGE_PROXY_CACHE_MAX_GB=5 # Optional: CDN URL for production (leave empty for local dev) # IMAGE_PROXY_CDN_URL= IMAGE_PROXY_FETCH_TIMEOUT_SECONDS=30 IMAGE_PROXY_MAX_SOURCE_SIZE_MB=10 # ============================================================================= # OpenTelemetry Observability (Optional) # ============================================================================= # Disabled by default. Enable for local Jaeger: docker compose --profile observability up OTEL_ENABLED=false # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # OTEL_SERVICE_NAME=coves-appview-dev # ============================================================================= # Telegram Operator Alerts (Optional) # ============================================================================= # Off in dev by default. Turn it on locally to check the alert text and your # bot wiring end to end: submit a report from the app and the message should # arrive within a second or two. # # Get a token from @BotFather, then read your chat ID from # https://api.telegram.org/bot/getUpdates after messaging the bot. # Enabling this without both values fails startup on purpose. # # Use a SEPARATE bot from production — a dev instance posting test reports into # the channel you rely on for real ones is how a real alert gets ignored. # # Uncomment all three lines together — filling in the credentials while leaving # TELEGRAM_ALERTS_ENABLED unset gives you a clean boot and no alerts. Group chat # IDs are negative; a channel can be given as @channelname. # TELEGRAM_ALERTS_ENABLED=true # TELEGRAM_BOT_TOKEN= # TELEGRAM_CHAT_ID= # Comma-separated subset of csam,doxing,harassment,spam,illegal,other. # Unset = alert on all of them. # TELEGRAM_ALERT_REASONS= # Values above 10 are capped by the AppView's own 10s backstop. # TELEGRAM_TIMEOUT_SECONDS=5 # ============================================================================= # Optional: Post submission limits # ============================================================================= # Anyone can write unlimited records naming any community, so the limits that # matter are enforced when a submission is ADMITTED rather than when a record is # written (docs/PRD_AUTHOR_OWNED_POSTS.md section 8). Both quotas are scoped to # one (author, community) pair: being at the limit in a busy community must not # silence an author everywhere on the instance. # # All three have working defaults, and the server REFUSES TO START if any of # them is set to zero or a negative value — "unset" must never be readable as # "unlimited", and a zero limit does not relax the rule, it inverts it. # # How many posts one author may have admitted to one community inside # POST_SUBMISSIONS_WINDOW. Counted over a ROLLING window, so slots come back # individually rather than all at once on the hour (default: 10). # POST_SUBMISSIONS_MAX_PER_COMMUNITY=10 # # The width of that rolling window (default: 1h). # POST_SUBMISSIONS_WINDOW=1h # # How long an identical resubmission is refused as a repeat. Separate from the # window above because the two answer different questions: one bounds volume, # the other recognises the retry a client sends after a lost response. Raising # it makes reposting the same content take longer to become possible again # (default: 1h). # # Two deliberate edges of this window. First, dedupe is bucketed against the # epoch rather than against each submission, so the effective protection # ranges from just above zero up to the full window depending on where in the # bucket a submission lands — content posted just before a bucket edge can be # reposted right after it. Accepted so the dedupe key expires on its own with # no cleanup process. Second, a crash between reserving a submission slot and # the PDS write leaves an orphaned reservation that burns one quota slot and # refuses identical content as a duplicate until the bucket rolls, then # clears itself; there is no sweeper, and the damage is bounded by this # window. # POST_SUBMISSIONS_DEDUPE_WINDOW=1h # ----------------------------------------------------------------------------- # Acceptance queue (the engine's pull side) # ----------------------------------------------------------------------------- # A post claiming a community is not visible in it until the community writes an # acceptance record (docs/PRD_AUTHOR_OWNED_POSTS.md section 5.6). The write path # and the firehose consumer both push work at the engine that writes those, and # neither can see a subject left undecided because a credential expired or a # lookup blipped. This periodic pass is the only thing that reaches those, so its # cadence is the worst case delay before a stranded post becomes visible. # # It is a no-op on an instance that hosts no communities: both the backlog query # and the repo factory key on STORED PDS CREDENTIALS, which exist only for # communities this AppView provisioned itself. # # How often the pass runs (default: 1m). Set to 0 to DISABLE the driver # entirely, which is supported rather than a misconfiguration — an AppView that # hosts no communities can accept nothing. Disabling it also removes the # acceptanceQueue block from /health/consumers, so an absent driver and an idle # one stay distinguishable. # ACCEPTANCE_QUEUE_INTERVAL=1m # # How many subjects one pass takes (default: 50). Unlike the quotas above this # one cannot fail open — the backlog query substitutes its own page size for a # non-positive value and clamps an over-large one — so it is not validated at # startup. # ACCEPTANCE_QUEUE_BATCH_SIZE=50 # ENCRYPTION_KEY seals stored credentials (community PDS passwords/tokens, # aggregator OAuth sessions) with AES-256-GCM in the AppView process. Dev/CI # only: unset in dev generates a random key per boot, which strands every # stored credential at the next restart, so pin one here. Never reuse in prod. # Generate a production key with: openssl rand -base64 32 ENCRYPTION_KEY=s+mOXSpNxojVEVj1IgVYrWBYxqbifw8WTBMn7BSsSdc=