mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
## Problem An agent `#p` + `#h` + `since` REQ (the `require_mention` subscription in `buzz-acp`) ran for over 45 minutes on a production writer and held `AccessShare` on every `events` partition. There were two causes: 1. **The mention side of the `#p` join was unbounded.** `build_query_events_sql` joined `event_mentions` on `(community_id, event_id)` only. The planner picked a nested loop whose outer side was every mention of the pubkey, ever, and whose inner side range-scanned the channel index from `since` on each partition. Cost ≈ total mentions × channel rows since `since`. 2. **Nothing cancelled it.** The writer `statement_timeout` defaulted to `0`. Only `#e` reads had a deadline (the 20s cap from #7854), and read-replica sessions had none at all. `pg_stat_statements` shows this shape has a multi-thousand-second max going back months, so it is chronic and was not introduced by a recent change. ## Change **`#p` mention bound** (`store/event.rs`, `push_mention_join`, shared by the page SELECT and COUNT) - Join on the full event key, `AND e.created_at = m.event_created_at`. Mention rows copy the event's `created_at` at insert (migration 0007 already relies on this equality), so the join can prune `events` partitions by time. - Repeat `since`/`until` on `m.event_created_at`, so the outer scan is a range on the existing `idx_event_mentions_pubkey_created`. `until` is a superset bound for the keyset cursor; the exact `(created_at, id)` cursor test stays on `e`. - Repeat `kinds` on `m.event_kind` when the filter has kinds. Every production mention writer binds the event kind (`insert_mentions` in `store/runtime/mod.rs`, and the replaceable-event path in `store/replaceable.rs`, back to the first Postgres schema), and no migration inserts mention rows, so this drops no rows written by the relay. Rows imported out of band with a NULL `event_kind` would no longer match a kinds-filtered `#p` read; a read-only production count on 2026-10-20 found 0 such rows on both production reader databases. The pushdown makes `idx_event_mentions_pubkey_kind_created` available to the planner for the `require_mention` kind-9 shape; its use has not been observed (see below). An E2E fixture with a hand-set NULL `event_kind` row confirmed the caveat: base returns 8 events, head 7. - The feed's mentions and needs-action queries (`store/feed.rs`, `build_mentions_query` / `build_needs_action_query`) join `event_mentions` the same way, so each joined row probes one `events` partition instead of all of them. Their `ORDER BY m.event_created_at ... LIMIT` already kept them from running away; this removes the per-row cross-partition probe. - Observed plan (E2E at `9cb6638b1`, before the `kinds` pushdown; 60,000 historical mentions + 2,500 recent decoys, stock planner, `EXPLAIN (ANALYZE, BUFFERS)` of the captured production SQL): the mention side is a bounded range on `idx_event_mentions_pubkey_created` (13 rows), then a hash join with a scan of the recent `events` partition. Base scanned 2,509 recent events and probed `event_mentions_pkey` 2,509 times. Head: **1.1 ms / 319 shared hits**; base: **8.5 ms / 10,049 shared hits**. Not measured against the production replica. A fresh `EXPLAIN (ANALYZE, BUFFERS)` replay at `a7065c367`, after the pushdown, ran on a smaller fixture where the planner chose a sequential mention scan, so it neither confirms nor replaces these numbers. **Reads: 20s deadline on every generic event read** - `query_events_on` and `count_events_on` always run under the transaction-local 20s deadline that `#e` reads used (`fetch_with_read_deadline`, formerly `fetch_with_e_tag_deadline`). It only tightens: shorter operator or caller caps are kept. - Read-replica sessions get a 20s session default through `after_connect`, using the same tighten-only SQL. - Cost: every generic REQ history read and COUNT now pays BEGIN + `set_config` + ROLLBACK (three extra round trips; before, only `#e` reads did). In the local E2E the end-to-end history read was still faster at head (median 4.1 ms vs 10.2 ms at base, 9 samples each, warm cache), so the round trips are dominated by the bounded plan; this does not isolate them or establish production p99. The round trips also lengthen how long each such read holds its writer connection; that hold time has not been measured against production pool sizing. - A replica read cancelled at its deadline (57014) is returned, not re-run on the writer. Usually the cancel means expensive work the writer would only repeat. It can also mean the replica waited on a lock only it holds (a replayed `ACCESS EXCLUSIVE` from primary DDL), when the writer would have answered quickly; that miss is the accepted price of keeping runaway reads off the primary. The rationale now lives once on `DbError::is_statement_cancelled` instead of in a dozen copied comments. The generic routed reads and `ReadSession` already did this; the eight routed reads with their own SQL (feed mentions, needs-action and activity; thread replies; channel window; relay membership; thread window and its auxiliary scan) now do too. A cancelled auxiliary scan also keeps its replica session instead of degrading it to the writer for the rest of the request. Other replica failures (e.g. hot-standby recovery conflicts) still fall back to the writer. - Paths with their own tighter `SET LOCAL` (artifact 2–5s, thread window 4s, personal reads, usage 5–15s, serving lease 1.5s) are unchanged. **Writes: every writer session is bounded** - The default `statement_timeout` changes from `0` to 120s (`DEFAULT_STATEMENT_TIMEOUT_MS`). - `BUZZ_DB_STATEMENT_TIMEOUT_MS` still overrides it. `0` or any value above 300s installs 300s (`MAX_STATEMENT_TIMEOUT_MS`), so a session can no longer disable it, and a clamped value logs a `warn!` when the pool is built. Only the operator-approved, fenced destruction paths below lift it, per transaction. **Long-running writer paths, audited** | Path | Behavior | |---|---| | Migrations / schema destruction (`with_exclusive_schema_destruction_lock`) | Unchanged: these lift lock and statement timeouts on their own detached connection | | Partition maintenance | Unchanged: own 5s `statement_timeout` | | Partition audit (`buzz-admin`) | Unchanged: 30s | | Community deletion inventory, purge and logical verification (`inventory_schema` whole-tenant counts, `purge_postgres` whole-community `DELETE` per table in one tx, `verify_postgres_logically_deleted` whole-tenant `EXISTS` per table right after the purge) | Lifted (`statement_timeout = 0`) transaction-locally by `lift_statement_timeout`, like migrations; lock and idle timeouts still bound them. Inventory counts each table in its own lifted transaction, so each table's lock and snapshot are released as soon as its count finishes, as the autocommit counts did. A cancelled purge would retry from scratch and eventually block, so a cap here would turn large-tenant deletion into an operator override | | Audit appends (`buzz-audit`, audit pool inherits the writer cap) | Waits on the audit-chain advisory lock. With `BUZZ_DB_LOCK_TIMEOUT_MS=0` (or at least the statement cap), `statement_timeout` (57014) cancels the wait instead of `lock_timeout` (55P03). The relay's audit worker now retries both with the same backoff instead of dropping the entry on 57014; admission refusal stays terminal | On HTTP routes a statement timeout already maps to the distinct retryable 503 (`api/mod.rs`, `statement_timeout_maps_to_distinct_503`). A cancelled REQ fails its history read instead of hanging. ## Tests - `p_tag_query_bounds_mention_side_by_time` (unit): page and COUNT SQL carry the `created_at` join and the `since`/`until`/`kinds` mention bounds, and the bounds are absent when the filter has none. - `p_tag_read_with_mention_bounds_keeps_results` (Postgres): through the production mention insert, unbounded, `since`, window and keyset-cursor reads return exactly the mentioned rows, and COUNT matches. Every bound sits exactly on a mentioned row's second and the cursor page includes a same-second sibling after the cursor id, so an off-by-one on either mention-side bound fails it. - `plain_read_is_cancelled_at_deadline_when_timeout_disabled` / `count_events_cancels_at_read_deadline_when_timeout_disabled` (Postgres): the production `#p` + `#h` + `since` shape and a plain COUNT are cancelled at 20s with the session timeout disabled. Previously these covered `#e` only. - `writer_statement_timeout_is_always_bounded` (unit) and `session_statement_timeouts_bound_writer_and_reader_sessions` (Postgres): through `Db::new`, writer sessions install `2min` / `5min` (for `0` and for 600s) / `45s`, and replica sessions install `20s`. - `deletion_whole_tenant_stages_outlive_the_writer_statement_timeout` (Postgres): on a scratch desired-state database with a 500ms writer cap, the real `inventory_schema`, `purge_postgres` and `verify_postgres_logically_deleted` each wait behind a held `ACCESS EXCLUSIVE` lock on `events` for over 1s and still complete, a plain statement is cancelled (57014), and pooled sessions still read `500ms` afterwards. A lifted transaction reads `statement_timeout = 0`, so a bounded lift (e.g. `'300000'`) fails it, as does removing any of the three `lift_statement_timeout` calls. - `mentions_query_is_tenant_scoped_and_joins_mentions_by_composite_key` / `needs_action_query_is_tenant_scoped_and_joins_mentions_by_composite_key` (unit): both feed queries carry the `e.created_at = m.event_created_at` join. - `replica_statement_cancel_propagates_without_writer_rerun` (Postgres): with the replica locked past its cap, each of the six routed reads above returns 57014 instead of the writer's copy. Removing the cancel arm in any of them fails it. - `thread_window_statement_cancel_propagates_without_writer_rerun` (Postgres): a thread-window read parked behind a replica lock and cancelled (57014; the window's own 1s `lock_timeout` would otherwise fire first) returns the cancel instead of the unlocked writer's window, and an auxiliary scan timed out on a replica session returns 57014 and leaves the session on the replica. Removing either guard fails it. - `audit_worker_retries_statement_timeout_until_original_entry_is_appended_once` (Postgres, `buzz-relay`): the production audit worker, with `lock_timeout` disabled and a 100ms statement cap, keeps the entry through repeated 57014 cancels while the chain lock is held and appends it exactly once after release. Treating only 55P03 as retryable fails it. - The existing writer and audit-pool GUC tests now expect `5min` for a disabled statement timeout. ## After deploy, watch - `buzz_db_route_decision{decision="replica",reason="statement_cancelled"}`: replica reads that now fail instead of falling back to the writer, including the replica-only lock case above. - `buzz_db_pool_acquire_wait_seconds` and `buzz_db_pool_waiters` on the writer pool: every generic REQ history read and COUNT now holds its connection for three extra round trips. ## Database checklist (`docs/database.md` § Review checklist) This PR changes schema-dependent `buzz-db` queries (the `#p` join and its `m.event_kind` pushdown). It does not change the schema. - **Phase:** none. No schema change, so this is not an expand, backfill or contract step. Nothing before or after it is required. - **Triggers, functions, procedures, rules, foreign keys:** none added. - **Row mutations / backfill:** no migration, and no row is mutated or backfilled. The one data-shape dependency is mention rows with a NULL `event_kind`: a kinds-filtered `#p` read no longer matches them (see the `kinds` bullet under Change). Relay writers always bind `event_kind`, so only rows imported out of band are affected. A read-only production count on 2026-10-20 found 0 such rows on both production reader databases. - **Contract migration:** N/A. - **Rule 4 locations:** none touched. No table, index or desired-state file changes, so no parity test applies. The new bounds use the existing `idx_event_mentions_pubkey_created` and `idx_event_mentions_pubkey_kind_created`. - **Deployed relay vs. new schema:** there is no new schema. The currently deployed relay and this relay run against the same tables. - **Lock and statement timeouts:** this PR is about them. Generic reads get a 20s deadline, replica sessions a 20s default, and writer sessions a 120s default capped at 300s. Community-deletion transactions lift `statement_timeout` per transaction; migrations and schema destruction are unchanged (see "Long-running writer paths, audited"). No index builds and no operator prebuild step. ## What got simpler - One read-deadline path instead of an `#e`-only branch in `query_events_on` and `count_events_on`. - The `#p` join SQL that was duplicated between SELECT and COUNT now lives in one `push_mention_join`. - The deletion tests share one scratch desired-state database helper instead of an inline copy. - "Is this query bounded?" now has one answer: yes, unless it runs in a migration, schema destruction, or community-deletion transaction. ## Identified debt (not in this PR) - `buzz-acp` re-sends the same filter after a timeout `CLOSED` (no backoff or narrowing). With the `#p` bound the `require_mention` read should finish well inside 20s, so this is a follow-up, not a blocker. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
395 lines
21 KiB
Plaintext
395 lines
21 KiB
Plaintext
# =============================================================================
|
||
# Buzz Backend — Local Development Environment
|
||
# =============================================================================
|
||
# Copy this file to .env and adjust as needed:
|
||
# cp .env.example .env
|
||
#
|
||
# All defaults here work with `docker compose up` out of the box.
|
||
#
|
||
# Service ports (defaults):
|
||
# Postgres → localhost:5432
|
||
# Redis → localhost:6379
|
||
# Typesense → localhost:8108
|
||
# Adminer → localhost:8082 (DB browser UI)
|
||
#
|
||
# Note: If port 8082 conflicts, change the adminer port in docker-compose.yml
|
||
# =============================================================================
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Database (Postgres 17)
|
||
# -----------------------------------------------------------------------------
|
||
DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz
|
||
# Optional read-replica URL; unset/blank keeps all reads on the writer.
|
||
# READ_DATABASE_URL=postgres://buzz:buzz_dev@localhost:5433/buzz
|
||
PGHOST=localhost
|
||
PGPORT=5432
|
||
PGUSER=buzz
|
||
PGPASSWORD=buzz_dev
|
||
PGDATABASE=buzz
|
||
|
||
# Fleet usage telemetry is replica-only and independent of serving-read
|
||
# routing. This freshness budget defaults to 30000ms; 0 disables the database
|
||
# telemetry families. When the reader is absent, stale, or unavailable, the
|
||
# relay skips the collection and reports its availability as 0 instead of
|
||
# falling back to the writer.
|
||
# BUZZ_USAGE_METRICS_REPLICA_MAX_AGE_MS=30000
|
||
# Per-community metric series are disabled by default to bound relay memory and
|
||
# monitoring cardinality. `all` is the temporary rollback mode.
|
||
# BUZZ_USAGE_METRICS_PER_COMMUNITY=off
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Redis 7
|
||
# -----------------------------------------------------------------------------
|
||
REDIS_URL=redis://localhost:6379
|
||
# Max connections in the relay's shared Redis pool (default 16).
|
||
# BUZZ_REDIS_POOL_SIZE=16
|
||
|
||
# Max connections in each of the relay's Postgres pools — writer and, when
|
||
# READ_DATABASE_URL is set, reader (default 50).
|
||
# BUZZ_DB_POOL_SIZE=50
|
||
# Read-only partition catalog audit interval, clamped to 60s..24h (default 900s).
|
||
# BUZZ_PARTITION_AUDIT_INTERVAL_SECS=900
|
||
# Emergency kill switch for all automatic partition DDL, including catch-all
|
||
# advancement; audits remain enabled.
|
||
# BUZZ_PARTITION_MANAGER_CREATE_ENABLED=true
|
||
# Replace an empty partition catch-all with six months of dedicated monthlies,
|
||
# at startup and on every audit interval (default true). Has no effect while the
|
||
# kill switch above is off.
|
||
# BUZZ_PARTITION_MANAGER_ADVANCE_ENABLED=true
|
||
|
||
# Writer-session Postgres timeouts for buzz-db-backed pools and the relay audit
|
||
# pool, all in milliseconds; 0 disables the lock and idle timeouts. The
|
||
# separately deployed push gateway owns its own database and session policy
|
||
# and does not consume these knobs.
|
||
# lock_timeout: fail a statement that waits this long on any lock instead of
|
||
# parking behind a wedged holder (default 5000).
|
||
# BUZZ_DB_LOCK_TIMEOUT_MS=5000
|
||
# idle_in_transaction_session_timeout: reap sessions idle inside an open
|
||
# transaction — bounds how long a wedged client can hold locks (default 60000).
|
||
# BUZZ_DB_IDLE_TXN_TIMEOUT_MS=60000
|
||
# statement_timeout: cap any single statement's runtime (default 120000). A
|
||
# session cannot disable it: 0 or anything above 300000 installs 300000 (5
|
||
# minutes) and logs a warning at startup. Migrations, schema destruction, and
|
||
# community-deletion inventory and purge transactions lift it for themselves.
|
||
# Generic event reads (REQ history, /query, /count) and read-replica sessions
|
||
# are further capped at 20s. Warning: a pathologically low value (e.g. 1) also
|
||
# times out connection setup and can prevent any DB connection from
|
||
# establishing.
|
||
# BUZZ_DB_STATEMENT_TIMEOUT_MS=120000
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Typesense (search)
|
||
# -----------------------------------------------------------------------------
|
||
TYPESENSE_API_KEY=buzz_dev_key
|
||
TYPESENSE_URL=http://localhost:8108
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Relay (WebSocket server)
|
||
# -----------------------------------------------------------------------------
|
||
# Bind address for the relay (host:port)
|
||
BUZZ_BIND_ADDR=0.0.0.0:3000
|
||
# Public WebSocket URL — used in NIP-42 auth challenges
|
||
RELAY_URL=ws://localhost:3000
|
||
# Stable relay signing key (required). `just bootstrap` generates a random key in
|
||
# the gitignored .env file. Preserve that value across restarts and backups.
|
||
# BUZZ_RELAY_PRIVATE_KEY=<32-byte hex private key>
|
||
# Optional: path to the web UI dist directory. When set, the relay serves
|
||
# the web frontend at / for browser requests. Leave unset for local dev
|
||
# (use `just web` for Vite HMR instead).
|
||
# BUZZ_WEB_DIR=./web/dist
|
||
|
||
# NIP-PL mobile push is an explicit deployment opt-in. A gateway URL alone
|
||
# never enables it. When enabled, the exact HTTP(S) delivery URL is required.
|
||
# Rollback requires replacing every enabled writer/worker process. Inspect and
|
||
# explicitly decide retained queue disposition before re-enabling (see docs/push-gateway-deployment.md).
|
||
BUZZ_PUSH_ENABLED=false
|
||
BUZZ_PUSH_GATEWAY_DELIVERY_URL=https://push.buzz.xyz/v1/deliveries/apns
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Admin Dashboard (private moderation surface)
|
||
# -----------------------------------------------------------------------------
|
||
# Host name that serves the moderation dashboard and its /api/admin/v1
|
||
# endpoints. Leave unset to keep the admin surface absent.
|
||
# BUZZ_ADMIN_HOST=admin.localhost:3000
|
||
#
|
||
# Authentication mode. Accepted values: nip98 (default), disabled.
|
||
# Any other value is a startup error. Token authentication was removed:
|
||
# BUZZ_ADMIN_TOKEN is ignored with a startup warning — remove it from the environment.
|
||
# BUZZ_ADMIN_AUTH=nip98
|
||
#
|
||
# Option A — BUZZ_ADMIN_AUTH=nip98 (Nostr pubkey-based auth, default):
|
||
# NIP-98 HTTP Auth. Each request must carry an Authorization: Nostr header
|
||
# with a signed kind-27235 event. Authorized principals are resolved from:
|
||
# 1. RELAY_OPERATOR_PUBKEYS — comma-separated 64-char hex pubkeys (config Operators).
|
||
# 2. RELAY_OWNER_PUBKEY — implicit Operator fallback when RELAY_OPERATOR_PUBKEYS is unset.
|
||
# 3. relay_operators table — DB-managed Operator/Moderator roster.
|
||
# The dashboard requires a NIP-07 browser extension.
|
||
# Setting RELAY_OPERATOR_PUBKEYS for the admin console does NOT require
|
||
# RELAY_OPERATOR_API_ORIGIN; that origin is only for community provisioning
|
||
# (see below). When BUZZ_ADMIN_HOST is set, the relay advertises the admin
|
||
# origin in its NIP-11 document (`admin_api` field) so clients can auto-discover
|
||
# the console without manual URL entry.
|
||
# RELAY_OPERATOR_PUBKEYS=<64-char hex pubkey>[,<64-char hex pubkey>...]
|
||
#
|
||
# Option B — BUZZ_ADMIN_AUTH=disabled (read-only, no authentication):
|
||
# Anyone who can reach the relay can read, in every community: reports (with
|
||
# reporter notes and reported message content, deleted included), feedback and
|
||
# attachments, bans and timeouts with reasons, the community directory, member
|
||
# search and profiles (not staff status), and any stored message by ID.
|
||
# Writes and staffing are refused.
|
||
# Only people allowed to see all of that should be able to reach it. The admin
|
||
# API is on the relay's main listener and its hostname check uses the caller's
|
||
# Host header, so the whole relay port must be private, not just the admin
|
||
# hostname. The relay logs a WARN on every startup.
|
||
# `just admin` defaults to this mode for local review.
|
||
#
|
||
# Directory holding the built dashboard assets (`pnpm -C admin-web build`).
|
||
# BUZZ_ADMIN_WEB_DIR=./admin-web/dist
|
||
#
|
||
# Canonical origin (http(s)://host[:port], no path) that community-provisioning
|
||
# NIP-98 requests are verified against. Required only to USE the provisioning
|
||
# endpoints (POST /operator/communities) — not for the admin console. When
|
||
# RELAY_OPERATOR_PUBKEYS is set but this is unset, the relay boots with a WARN
|
||
# and provisioning requests fail closed until it is set.
|
||
# RELAY_OPERATOR_API_ORIGIN=http://127.0.0.1:3000
|
||
|
||
# Deployment-global operator-listener routes. Each configured Nostr identity
|
||
# may register target pubkeys and receives mention notifications at its URL.
|
||
# URLs may use HTTP or HTTPS. Use HTTP only for operator-controlled internal
|
||
# connections: NIP-98 signs requests but does not encrypt them, and
|
||
# notifications carry full message content.
|
||
# Format: pubkey:deliveryUrl;pubkey:deliveryUrl
|
||
# BUZZ_OPERATOR_LISTENER_TIMEOUT_MS=5000
|
||
# BUZZ_OPERATOR_LISTENERS=<64-char hex pubkey>:https://listener.example/mentions
|
||
|
||
# Optional relay-owned KLIPY key. When set, NIP-11 advertises GIF search and
|
||
# authenticated desktop clients use this relay as the metadata/search proxy.
|
||
# Keep the real value in your deployment's secret manager; never commit it.
|
||
# BUZZ_KLIPY_API_KEY=
|
||
|
||
# Shared Redis-backed admission limits. Defaults shown below; each value must
|
||
# be a positive integer.
|
||
# BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN=60
|
||
# BUZZ_RATE_LIMIT_GIF_SEARCHES_PER_MIN=30
|
||
# BUZZ_RATE_LIMIT_HUMAN_API_CALLS_PER_MIN=300
|
||
# BUZZ_RATE_LIMIT_HUMAN_WS_EVENTS_PER_SEC=10
|
||
# BUZZ_RATE_LIMIT_AGENT_STANDARD_MESSAGES_PER_MIN=120
|
||
# BUZZ_RATE_LIMIT_AGENT_STANDARD_API_CALLS_PER_MIN=600
|
||
# BUZZ_RATE_LIMIT_AGENT_ELEVATED_MESSAGES_PER_MIN=300
|
||
# BUZZ_RATE_LIMIT_AGENT_PLATFORM_MESSAGES_PER_MIN=600
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Git (NIP-34 bare repositories)
|
||
# -----------------------------------------------------------------------------
|
||
# Root directory for ephemeral Git workspaces and the disposable pack cache.
|
||
# Default: ./repos (relative to CWD).
|
||
# BUZZ_GIT_REPO_PATH=./repos
|
||
# BUZZ_GIT_MAX_PACK_BYTES=524288000
|
||
# BUZZ_GIT_MAX_REPO_BYTES=1048576000
|
||
# Process-local immutable pack/index cache. Zero disables retention.
|
||
# BUZZ_GIT_PACK_CACHE_PATH=./repos/.pack-cache
|
||
# BUZZ_GIT_PACK_CACHE_MAX_BYTES=5368709120
|
||
# BUZZ_GIT_PACK_CACHE_MAX_CONCURRENT_POPULATIONS=2
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# S3-Compatible Object Storage (media + Git/CAS)
|
||
# -----------------------------------------------------------------------------
|
||
# The local MinIO container is reachable from host processes at localhost:9000.
|
||
# Path style keeps the bucket in the URL path and is required by this local DNS
|
||
# setup. Use `virtual` only when the provider requires bucket-as-subdomain URLs.
|
||
BUZZ_S3_ENDPOINT=http://localhost:9000
|
||
BUZZ_S3_ACCESS_KEY=buzz_dev
|
||
BUZZ_S3_SECRET_KEY=buzz_dev_secret
|
||
BUZZ_S3_BUCKET=buzz-media
|
||
BUZZ_S3_REGION=us-east-1
|
||
BUZZ_S3_ADDRESSING_STYLE=path
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Media Upload Admission
|
||
# -----------------------------------------------------------------------------
|
||
# Bound image/video parser and storage work admitted by one relay process.
|
||
# BUZZ_MEDIA_MAX_CONCURRENT_UPLOADS=8
|
||
# BUZZ_MEDIA_MAX_CONCURRENT_UPLOADS_PER_PUBKEY=2
|
||
# BUZZ_MEDIA_UPLOADS_PER_MINUTE=30
|
||
# GET/HEAD /media/* always require Blossom t=get auth and relay membership.
|
||
# BUZZ_REQUIRE_MEDIA_GET_AUTH and BUZZ_REQUIRE_MEDIA_READ_AUTH are no longer
|
||
# read; setting either (including to false) changes nothing and the relay warns
|
||
# about it at startup.
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Ephemeral Channels (TTL testing)
|
||
# -----------------------------------------------------------------------------
|
||
# Override the TTL for all ephemeral channels (in seconds). When set, any
|
||
# channel created with a TTL tag will use this value instead of the
|
||
# client-provided one. Unset to use the client-provided TTL.
|
||
# BUZZ_EPHEMERAL_TTL_OVERRIDE=60
|
||
|
||
# How often the reaper checks for expired ephemeral channels (default: 60s).
|
||
# BUZZ_REAPER_INTERVAL_SECS=5
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Logging / Tracing
|
||
# -----------------------------------------------------------------------------
|
||
RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz_pubsub=debug,tower_http=debug
|
||
# Optional OpenTelemetry-only target filter. This is deliberately independent
|
||
# from RUST_LOG so log verbosity changes cannot break trace parentage.
|
||
# BUZZ_OTEL_FILTER=buzz_relay=info,buzz_datastore=info
|
||
|
||
# OTLP tracing endpoint (optional — leave unset to disable)
|
||
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# ACP (Agent Communication Protocol — buzz-acp harness)
|
||
# -----------------------------------------------------------------------------
|
||
# The ACP harness bridges Buzz events to AI agents. Each env var below maps
|
||
# to a CLI flag of the same name (lowercase, hyphens → underscores). All values
|
||
# are optional unless noted; defaults are shown in comments.
|
||
#
|
||
# Quick start:
|
||
# BUZZ_PRIVATE_KEY=<hex> BUZZ_RELAY_URL=ws://localhost:3000 buzz-acp
|
||
|
||
# ── Identity & auth ──────────────────────────────────────────────────────────
|
||
# Nostr private key (hex or bech32). REQUIRED — identifies the agent on the relay.
|
||
# BUZZ_PRIVATE_KEY=<32-byte hex or nsec1… private key>
|
||
|
||
# Relay WebSocket URL the harness connects to.
|
||
# Note: the relay itself uses RELAY_URL (above); this is the ACP harness's
|
||
# connection target — they happen to point at the same place in local dev.
|
||
# BUZZ_RELAY_URL=ws://localhost:3000
|
||
|
||
# ── Agent subprocess ─────────────────────────────────────────────────────────
|
||
# Binary to spawn as the AI agent (e.g. "goose", "codex-acp", "claude-code").
|
||
# BUZZ_ACP_AGENT_COMMAND=goose
|
||
|
||
# Comma-separated arguments passed to the agent binary.
|
||
# Goose default: "acp". Codex/Claude default: "" (empty).
|
||
# BUZZ_ACP_AGENT_ARGS=acp
|
||
|
||
# Binary for an optional MCP server sidecar (e.g. buzz-dev-mcp for buzz-agent).
|
||
# BUZZ_ACP_MCP_COMMAND=
|
||
|
||
# Number of parallel agent subprocesses (1–32).
|
||
# BUZZ_ACP_AGENTS=1
|
||
|
||
# Desired LLM model ID. Applied to every new ACP session.
|
||
# Use `buzz-acp models` to discover available model IDs.
|
||
# BUZZ_ACP_MODEL=
|
||
|
||
# Optional Databricks model-picker visibility filter. Discovery-only; this does
|
||
# not grant inference access. Comma-separated full-string * / ? patterns are
|
||
# OR-matched against raw workspace endpoint and Unity Catalog model-service IDs.
|
||
# Unset or blank shows every catalog entry. A nonblank value with no usable patterns is invalid.
|
||
# DATABRICKS_MODEL_FILTER=databricks-*,data_tools.goose.*
|
||
|
||
# ── Timeouts & sessions ──────────────────────────────────────────────────────
|
||
# Max seconds per agent turn before timeout (default 320 = ~5 min).
|
||
# BUZZ_ACP_TURN_TIMEOUT=320
|
||
|
||
# Max turns per session before proactive rotation. 0 = disabled (rotate only
|
||
# on MaxTokens / MaxTurnRequests). Recommended: 50 for long-running agents.
|
||
# BUZZ_ACP_MAX_TURNS_PER_SESSION=0
|
||
|
||
# ── Prompts ──────────────────────────────────────────────────────────────────
|
||
# System prompt injected into every agent session (inline text).
|
||
# BUZZ_ACP_SYSTEM_PROMPT=
|
||
|
||
# Path to a file containing the system prompt (mutually exclusive with above).
|
||
# BUZZ_ACP_SYSTEM_PROMPT_FILE=
|
||
|
||
# Message sent to the agent immediately after session creation.
|
||
# BUZZ_ACP_INITIAL_MESSAGE=
|
||
|
||
# ── Heartbeat ────────────────────────────────────────────────────────────────
|
||
# Seconds between heartbeat prompts. 0 = disabled. Must be 0 or ≥10.
|
||
# Recommended: 60 for long-running agents to prevent idle session timeouts.
|
||
# BUZZ_ACP_HEARTBEAT_INTERVAL=0
|
||
|
||
# Heartbeat prompt text (inline). Mutually exclusive with file variant.
|
||
# BUZZ_ACP_HEARTBEAT_PROMPT=
|
||
|
||
# Path to a file containing the heartbeat prompt.
|
||
# BUZZ_ACP_HEARTBEAT_PROMPT_FILE=
|
||
|
||
# ── Desktop development ──────────────────────────────────────────────────────
|
||
# DEV-only: replay first-run onboarding and the Welcome Team kickoff on each
|
||
# app launch while keeping the current identity and relay data.
|
||
# VITE_BUZZ_FORCE_FRESH_ONBOARDING=true
|
||
|
||
# Protected internal builds only: selects the module graph that contains the
|
||
# default-off Bestie experiment. Official OSS builds must leave this unset.
|
||
# VITE_BUZZ_BESTIE=1
|
||
|
||
# ── Subscription & filtering ─────────────────────────────────────────────────
|
||
# Subscribe mode: "mentions" (default), "all", or "config" (rule-based).
|
||
# BUZZ_ACP_SUBSCRIBE=mentions
|
||
|
||
# Comma-separated event kind numbers to subscribe to (overrides mode defaults).
|
||
# BUZZ_ACP_KINDS=
|
||
|
||
# Comma-separated channel UUIDs to limit subscription scope.
|
||
# BUZZ_ACP_CHANNELS=
|
||
|
||
# Set to true to disable the @-mention filter in mentions mode.
|
||
# BUZZ_ACP_NO_MENTION_FILTER=false
|
||
|
||
# Path to TOML config file for rule-based subscriptions (config mode).
|
||
# BUZZ_ACP_CONFIG=./buzz-acp.toml
|
||
|
||
# ── Dedup & self-ignore ──────────────────────────────────────────────────────
|
||
# How to handle duplicate events: "queue" (default) or "drop".
|
||
# BUZZ_ACP_DEDUP=queue
|
||
|
||
# Set to true to process the agent's own messages (default: ignore self).
|
||
# BUZZ_ACP_NO_IGNORE_SELF=false
|
||
|
||
# ── Session scoping ──────────────────────────────────────────────────────────
|
||
# How ACP provider sessions are scoped in channels: "channel" (default) or
|
||
# "thread". "channel" keeps one provider session per channel (legacy). "thread"
|
||
# gives each canonical channel thread its own isolated provider session; direct
|
||
# messages stay conversation-scoped either way. Ships as "channel" so thread
|
||
# scoping can be canaried and rolled back without code changes.
|
||
# BUZZ_ACP_SESSION_POLICY=channel
|
||
|
||
# ── Context ──────────────────────────────────────────────────────────────────
|
||
# Max context messages fetched for thread replies and DMs (0–100). 0 = disabled.
|
||
# BUZZ_ACP_CONTEXT_MESSAGE_LIMIT=12
|
||
|
||
# ── Presence & typing ────────────────────────────────────────────────────────
|
||
# Set to true to disable automatic online/offline presence status.
|
||
# BUZZ_ACP_NO_PRESENCE=false
|
||
|
||
# Set to true to disable typing indicators while the agent is processing.
|
||
# BUZZ_ACP_NO_TYPING=false
|
||
|
||
# ── Advanced tuning ──────────────────────────────────────────────────────────
|
||
# Event channel buffer capacity (WebSocket → harness). Increase for
|
||
# high-throughput agents. Minimum 1.
|
||
# BUZZ_ACP_EVENT_BUFFER=256
|
||
|
||
# ── Legacy aliases ───────────────────────────────────────────────────────────
|
||
# These are accepted for backward compatibility but the canonical names above
|
||
# are preferred:
|
||
# BUZZ_ACP_PRIVATE_KEY → BUZZ_PRIVATE_KEY
|
||
|
||
# Optional relay join policy. Markdown is served by the relay so every join
|
||
# surface can present the same documents. Each document and the independent age
|
||
# attestation are optional; configuring any one enables policy acceptance.
|
||
# BUZZ_TERMS_OF_SERVICE_MARKDOWN="# Terms of Service\n\nFull terms here."
|
||
# BUZZ_PRIVACY_POLICY_MARKDOWN="# Privacy Policy\n\nFull policy here."
|
||
# BUZZ_AGE_ATTESTATION_REQUIRED=true
|
||
|
||
# ── NIP-FI database community source ─────────────────────────────────────────
|
||
# Where NIP-FI communities come from: "env" (BUZZ_NIP_FI_COMMUNITIES, default)
|
||
# or "database" (bindings created by provisioning; needs
|
||
# BUZZ_NIP_FI_PROVISION_BINDING and must not be combined with
|
||
# BUZZ_NIP_FI_COMMUNITIES). See docs/operator-nip-fi-bindings.md.
|
||
# BUZZ_NIP_FI_COMMUNITY_SOURCE=env
|
||
# Most community-issuer rows one snapshot may hold (1..=10000000).
|
||
# BUZZ_NIP_FI_SNAPSHOT_MAX_ROWS=1000000
|
||
# Snapshot load timeout in ms; must be under the 30s refresh interval.
|
||
# BUZZ_NIP_FI_SNAPSHOT_REFRESH_TIMEOUT_MS=10000
|
||
# Hosts whose "not found" is cached for 5s (1..=1000000).
|
||
# BUZZ_NIP_FI_RESOLVER_NEGATIVE_CACHE_MAX=100000
|
||
# Concurrent live lookups; clamped below the database pool size.
|
||
# BUZZ_NIP_FI_RESOLVER_MAX_INFLIGHT=8
|