Something went wrong. Try again.
A community based topic aggregation platform built on atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256# 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=localhostPOSTGRES_PORT=5435POSTGRES_DB=coves_devPOSTGRES_USER=dev_userPOSTGRES_PASSWORD=dev_password# Test databasePOSTGRES_TEST_DB=coves_testPOSTGRES_TEST_USER=test_userPOSTGRES_TEST_PASSWORD=test_passwordPOSTGRES_TEST_PORT=5434# =============================================================================# PDS Configuration# =============================================================================PDS_HOSTNAME=localhostPDS_PORT=3001PDS_SERVICE_ENDPOINT=http://localhost:3000PDS_DID_PLC_URL=http://plc-directory:3000PDS_JWT_SECRET=local-dev-jwt-secret-change-in-productionPDS_ADMIN_PASSWORD=adminPDS_SERVICE_HANDLE_DOMAINS=.local.coves.dev,.coves.socialPDS_PLC_ROTATION_KEY=<generate-a-random-hex-key># =============================================================================# AppView Configuration# =============================================================================APPVIEW_PORT=8081FIREHOSE_URL=ws://localhost:3001/xrpc/com.atproto.sync.subscribeReposPDS_URL=http://localhost:3001APPVIEW_PUBLIC_URL=http://127.0.0.1:8081# =============================================================================# Jetstream Configuration (multi-feed)# =============================================================================# Semicolon-separated <feedKey>=<baseURL> 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=90sPLC_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.goOAUTH_PRIVATE_JWK=<generate-your-own-jwk># Generate with: openssl rand -hex 32OAUTH_COOKIE_SECRET=<generate-your-own-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=developmentNODE_ENV=developmentIS_DEV_ENV=trueLOG_LEVEL=debugLOG_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=trueAUTH_SKIP_VERIFY=trueHS256_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:8080IMAGE_PROXY_CACHE_PATH=./cache/imagesIMAGE_PROXY_CACHE_MAX_GB=5# Optional: CDN URL for production (leave empty for local dev)# IMAGE_PROXY_CDN_URL=IMAGE_PROXY_FETCH_TIMEOUT_SECONDS=30IMAGE_PROXY_MAX_SOURCE_SIZE_MB=10# =============================================================================# OpenTelemetry Observability (Optional)# =============================================================================# Disabled by default. Enable for local Jaeger: docker compose --profile observability upOTEL_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<TOKEN>/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 32ENCRYPTION_KEY=s+mOXSpNxojVEVj1IgVYrWBYxqbifw8WTBMn7BSsSdc=