R2D2-MERIDIAN/.env.example
Joshua Belke b81fefb5da
Some checks failed
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Meridian Harness / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Meridian Harness / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Meridian Harness / Publish rolling release (push) Has been cancelled
Meridian Harness / Publish tagged release (push) Has been cancelled
control plane / chart (push) Has been cancelled
control plane / test (push) Has been cancelled
control plane / browser-e2e (push) Has been cancelled
control plane / Build control plane image (linux/amd64) (push) Has been cancelled
control plane / Build control plane image (linux/arm64) (push) Has been cancelled
control plane / Publish signed control plane image (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Isolated DB Gate (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Admin Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Server Cross-Compile (push) Has been cancelled
CI / Server Cross-Compile-1 (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
fix(bus): the relay is a client of the router tier, not a peer of it
meridian-2m45. Two relays in the documented default `peer` mode, both
connected to a `zenohd` router, exchanged NOTHING while both reported
`session_open`, `endpoints_connected 1` and `zenoh_ready 1`. Not a drop
either: no drop counter on the receiver was ever created, because the
sample never left the publisher.

The mechanism, in the pinned tree rather than in a blog post:

  zenoh-1.8.0/src/net/routing/hat/router/pubsub.rs:200-222 — the router
  refuses to propagate a subscription declaration from one Peer face to
  another Peer face unless `failover_brokering(src, dst)`. A Client face
  is exempt; the `src_face.whatami != WhatAmI::Peer` arm short-circuits.

  hat/router/mod.rs:287-303 — `failover_brokering` needs
  `linkstatepeers_net`, and hat/router/mod.rs:373 builds it only
  `if peer_full_linkstate | gossip`. With `routing.peer.mode` at its
  `peer_to_peer` default and `scouting.gossip.enabled: false` it is
  `None`, so the answer is always false and the publisher never learns a
  remote subscriber exists.

Upstream states it plainly at DEFAULT_CONFIG.json5:225-226 — "The
failover brokering only works if gossip discovery is enabled". So the
charter's gossip refusal and session mode `peer` are mutually
incompatible through a router. Confirmed both ways on a live router:
gossip on (autoconnect still empty) makes `peer` deliver; gossip off with
`routing.peer.mode: "linkstate"` on BOTH ends also makes it deliver.

`linkstate` was measured working and rejected anyway, unanimously
(Architecture & Scale council, parallel seats, minutes in the bead):

  - it converts a per-pod value into a fleet-wide invariant that fails
    SILENTLY in both directions when the halves disagree — measured, both
    directions — and every rolling restart passes through that state;
  - eclipse/zenoh 1.9.0 and 1.10.0 ACCEPT `routing.peer.mode` and
    silently drop it (absent from the daemon's own `Initial conf`, router
    starts healthy), so the fix evaporates on a minor upgrade and this P0
    returns wearing a different hat;
  - `endpoints` has no cardinality bound, so a second router endpoint
    added for availability — posture-compliant, no refusal fires — would
    make every relay a multihop forwarder between communities.

`client` needs no cross-node agreement: verified delivering against a
`peer_to_peer` router AND a `linkstate` one. `hat/client/` holds no
routing Network at all, so transit is impossible by type rather than by
topology. It is also the documented target shape — one pod, one regional
router (feature-zenoh-transport.md § Topology) — and the only shape this
repo has ever actually measured through a router
(perf/run_0a2_matrix.sh:71 already passes `--zenoh-session-mode client`).

`peer` stays a supported mode for a DIRECT pod-to-pod link, which is what
`zenoh_bus.rs::peer_pair` tests and what the benchmark arms use. Nothing
in that suite changes; all 21 still pass, because all 21 cross-connect
two peers directly and none of them ever touched a router. That is why
they were green through the whole defect.

The reversal condition, and the whole trade, sit on `ZenohMode` where the
next person to change it will be looking, not only in the tracker.

New gate `just zenoh-router-check` is the live half: it starts a real
`zenohd` from `deploy/compose/zenoh/zenohd.json5` with the deployed
command line, routes two `ZenohEventBus` sessions through it, and pins
BOTH the mode that delivers and the mode that silently does not. Proven
to fail against the pre-fix default. Opt-in behind
MERIDIAN_ZENOH_DAEMON_CHECK=1 and out of `just check`, same as
`zenoh-config-check`, because it needs Docker and the pinned image.

Signed-off-by: Joshua Belke <joshua@innovationhub-act.org>
2026-08-20 16:43:34 -04:00

660 lines
36 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# =============================================================================
# Meridian 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
# Dragonfly → localhost:6379 (Redis wire protocol)
# Typesense → localhost:8108
# Adminer → localhost:8082 (DB browser UI)
#
# Note: If port 8082 conflicts, change the adminer port in docker-compose.yml
#
# -----------------------------------------------------------------------------
# Machine-specific settings do NOT belong in this file
# -----------------------------------------------------------------------------
# .env is what the team shares. For anything true only on your machine —
# relocated ports, local URLs, which optional services you run — use:
#
# cp .env.local.example .env.local
# cp docker-compose.override.yml.example docker-compose.override.yml
#
# Both are gitignored. Which one you need depends on how you start things:
#
# docker-compose.override.yml auto-loaded by Compose, no wiring. The ONLY
# mechanism that survives a bare
# `docker compose up -d`.
# .env.local read by Compose only when COMPOSE_ENV_FILES is
# set IN THE SHELL — putting COMPOSE_ENV_FILES
# in this file is silently ignored. `just up`
# and the other recipes export it for you.
#
# Precedence: .env < .env.local < docker-compose.override.yml < shell env
# Run `just env-doctor` to see which layers are active and the resolved ports.
#
# =============================================================================
# -----------------------------------------------------------------------------
# Compose profiles — which optional services start
# -----------------------------------------------------------------------------
# Core services (postgres, dragonfly, minio*) have no profile and
# always start. The extras below are opt-in. tools/auth/observability stay
# enabled here so a fresh `cp .env.example .env` brings up the same stack as
# before profiles existed; trim or comment out for a leaner core-only stack.
# tools → adminer (:8082) · auth → keycloak (:8180) · observability → prometheus (:9090)
# bus → zenohd (:7447) — DEFAULT OFF, do not add it here
#
# `bus` is deliberately absent from this line. zenohd carries events only, and
# only once MERIDIAN_BUS names a Zenoh mode; starting a router that nothing
# talks to is inventory, and the profile is how that stays a choice.
COMPOSE_PROFILES=tools,auth,observability
# -----------------------------------------------------------------------------
# Database (Postgres 17)
# -----------------------------------------------------------------------------
DATABASE_URL=postgres://meridian:meridian_dev@localhost:5432/meridian
# Optional read-replica URL; unset/blank keeps all reads on the writer.
# READ_DATABASE_URL=postgres://meridian:meridian_dev@localhost:5433/meridian
PGHOST=localhost
PGPORT=5432
PGUSER=meridian
PGPASSWORD=meridian_dev
PGDATABASE=meridian
# -----------------------------------------------------------------------------
# Dragonfly (drop-in Redis replacement — same wire protocol, reports
# redis_version 7.4). Every REDIS_* name below is unchanged on purpose:
# clients, URLs and the relay's pool all keep working as-is.
# -----------------------------------------------------------------------------
REDIS_URL=redis://localhost:6379
# Relocate the host port if another project already holds 6379. Note that a
# container from another Compose project publishing 6379 will silently win the
# bind, so change BOTH values together.
# REDIS_HOST_PORT=6390
# Max connections in the relay's shared Redis pool (default 16).
# MERIDIAN_REDIS_POOL_SIZE=16
# Dragonfly sizing. Defaults suit a dev laptop; raise for load testing.
# CONSTRAINT: maxmemory must be >= 256MiB per thread or Dragonfly refuses to
# start and the container just exits 1 ("There are N threads, so X MiB are
# required"). Raise both together — e.g. 4 threads needs at least 1024mb.
# DRAGONFLY_MAXMEMORY=512mb
# DRAGONFLY_THREADS=2
# Max connections in each of the relay's Postgres pools — writer and, when
# READ_DATABASE_URL is set, reader (default 50).
# MERIDIAN_DB_POOL_SIZE=50
# -----------------------------------------------------------------------------
# Cross-pod event bus (MERIDIAN_BUS)
# -----------------------------------------------------------------------------
# Which transport carries events, cache invalidation and connection control
# between relay pods. Unset or empty means `redis`, which is what every
# untouched deployment keeps running.
#
# Unlike MERIDIAN_MESH — where an unrecognised value means "off", because the
# failure mode is a feature that quietly does not run — an unrecognised value
# here is a HARD STARTUP ERROR. This variable selects the transport every
# cross-pod lane rides, and a relay that read `MERIDIAN_BUS=zenho` as "redis"
# and started healthy is how a fleet ends up believing it migrated.
#
# redis Redis/Dragonfly PUB/SUB. The default.
# zenoh Zenoh carries every cross-pod lane. Redis keeps presence, rate
# limits and the NIP-98 replay set: it stops carrying events, it does
# not leave.
# shadow Dual publish, Redis-authoritative delivery, parity counted. The
# reversible soak step.
#
# `zenoh` and `shadow` need a relay BUILT with the `zenoh` feature
# (`cargo build -p meridian-relay --features zenoh`). A build without it
# refuses them by name rather than falling back to Redis. The published relay
# image carries the feature — `Dockerfile` and `REMAPPING/cicd/Dockerfile` both
# build with it — so these names are readable by the artifact, not only by a
# local build. Carrying the feature is not the same as being reachable: the
# endpoint rule below still confines this to one host.
# MERIDIAN_BUS=redis
# Comma-separated Zenoh endpoints this pod connects to. REQUIRED whenever
# MERIDIAN_BUS is zenoh or shadow — there is no default and no discovery.
# Both scouting mechanisms (multicast AND gossip) are pinned off, so an
# endpoint-less session opens, reports a healthy process, and reaches nothing.
# Locators are `tcp/host:port`; this build compiles the TCP link only.
#
# SECURITY — a non-loopback endpoint is refused unless EVERY lane floor sits
# strictly above the authenticated link profile. That is not a formality: with
# the default in-deployment floors (all P0) the relay accepts anything arriving
# on the bus without re-verifying its signature, so a reachable router would be
# an event-injection primitive. Raise the floors and the same endpoint is
# accepted, because every arriving message is then either re-verified
# (BIP-340, signed events) or refused (control payloads, which prove nothing
# about themselves). `0.0.0.0` and `::` count as reachable, not as loopback.
#
# KNOWN CONSEQUENCE of raising the floors on a build with no mutual link
# authentication (this one — see the TLS block below): the Q0 control lane
# carries cache invalidation and connection control, and neither payload
# carries a signature to re-verify, so both are REFUSED rather than delivered.
# Cross-pod cache drops and live ban enforcement therefore stop crossing a
# reachable Zenoh link. This is counted, not silent —
# `meridian_bus_admission_total{decision="reject",lane="Q0"}` rises and each
# refusal logs — but a deployment that needs those lanes cross-pod must keep
# them on Redis until a mutually-authenticated link profile exists.
# MERIDIAN_ZENOH_ENDPOINTS=tcp/127.0.0.1:7447
# Session mode: `client` (default) or `peer`. `router` is deliberately not a
# value — the relay attaches to the router tier, never becomes one.
#
# `peer` is NOT a drop-in alternative here. Two relays in `peer` mode connected
# to a `zenohd` router exchange nothing while both report healthy: with gossip
# scouting off (this deployment refuses it) the router will not forward one
# peer's subscription declaration to another peer. `peer` is for a DIRECT link
# between two pods — the benchmark and single-node development shape. See
# `ZenohMode` in crates/meridian-pubsub/src/zenoh/config.rs for the mechanism
# and what would move this default back.
# MERIDIAN_ZENOH_MODE=client
# Explicit listen endpoint. Defaults to the pinned fixture's ephemeral loopback
# port (`tcp/127.0.0.1:0`), because a relay pod is dialled by the router rather
# than by name. Subject to the same reachability rule as the connect endpoints
# above — `tcp/0.0.0.0:7447` binds every interface and is refused.
# MERIDIAN_ZENOH_LISTEN=tcp/127.0.0.1:0
# TLS material. This build compiles NO TLS link, so setting any of these is a
# hard startup error rather than a silent downgrade to unauthenticated TCP:
# supplying certificates states an intent the binary cannot honour, and
# honouring it partially is the worst of the three outcomes.
# MERIDIAN_ZENOH_TLS_ROOT_CA=
# MERIDIAN_ZENOH_TLS_CERT=
# MERIDIAN_ZENOH_TLS_KEY=
# -----------------------------------------------------------------------------
# Typesense (search)
# -----------------------------------------------------------------------------
TYPESENSE_API_KEY=meridian_dev_key
TYPESENSE_URL=http://localhost:8108
# -----------------------------------------------------------------------------
# Relay (WebSocket server)
# -----------------------------------------------------------------------------
# Bind address for the relay (host:port)
MERIDIAN_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. Set this in dev if you want REST-created forum posts
# to keep resolving to the original author across relay restarts.
# MERIDIAN_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).
# MERIDIAN_WEB_DIR=./REMAPPING/meridian-web/dist
# -----------------------------------------------------------------------------
# Relay ownership (who may administer this relay)
# -----------------------------------------------------------------------------
# The pubkey bootstrapped into `relay_members` as `owner` on every startup.
#
# Set this on any self-hosted relay, not only one that enforces membership.
# Every relay-admin command — the workspace icon (kind:9033), the member roster
# (9030/9032) and the moderation kinds — is refused unless the sender holds
# `admin` or `owner`, invite codes are capped at `member`, and nothing else
# grants the first role. Leave it unset and the relay runs fine but can never
# be administered by anyone: the desktop's "Edit relay" icon picker, for one,
# has its every save rejected.
#
# Changing this rotates ownership: the new key becomes `owner` and any previous
# owner is demoted to `admin`.
# RELAY_OWNER_PUBKEY=<64-char hex pubkey>
#
# Extra hosts this relay answers on for communities of its own.
#
# A community is keyed by the connection's `Host`, so one relay process reached
# at `localhost:3000`, at `127.0.0.1:3000` and at a LAN address serves three
# separate communities — each with its own roster, and each needing the owner
# above bootstrapped into it. Only the RELAY_URL host is seeded automatically;
# list every other name clients actually dial here, or those clients land in a
# community nobody can administer.
#
# Comma-separated authorities (`host` or `host:port`), no scheme and no path.
# Control-plane-provisioned tenants have their own owners and must NOT be
# listed — this is for the deployment's own aliases only.
# RELAY_DEPLOYMENT_HOSTS=127.0.0.1:3000,relay.lan:3000
# -----------------------------------------------------------------------------
# Relay operator API (community provisioning)
# -----------------------------------------------------------------------------
# Deployment-root authority for `POST /operator/communities*`. An empty
# allowlist (the default) disables community provisioning entirely, so the
# control plane cannot create communities until a pubkey is listed here.
# Comma-separated 64-char hex pubkeys. `just control-plane` generates a keypair
# on first run and prints the value to paste in.
# RELAY_OPERATOR_PUBKEYS=<64-char hex pubkey>
# Sole key allowed to create or transfer ownership. A single allowlisted
# operator is selected automatically; this is required when the allowlist has
# multiple keys and must name the control-plane key.
# RELAY_OWNERSHIP_WRITER_PUBKEY=<64-char hex pubkey>
#
# Canonical origin the relay uses to rebuild the NIP-98 `u` tag for operator
# requests. Required whenever RELAY_OPERATOR_PUBKEYS is non-empty, and it must
# match the control plane's own setting byte-for-byte — `localhost` and
# `127.0.0.1` are NOT interchangeable here, and a trailing slash breaks every
# call with an opaque signature error.
# RELAY_OPERATOR_API_ORIGIN=http://127.0.0.1:3000
# -----------------------------------------------------------------------------
# Hosted-community control plane (`just control-plane`)
# -----------------------------------------------------------------------------
# Replaces the hosted control plane for local and self-hosted deployments:
# browser sign-in, account <-> Nostr identity binding, and community
# provisioning via the relay operator API above.
#
# Scoped database. MUST NOT be the relay's `meridian` database — the two
# would collide on sqlx's `_sqlx_migrations` table. `just setup` creates it.
# MERIDIAN_CONTROL_DATABASE_URL=postgres://meridian:meridian_dev@localhost:5432/meridian_control_plane
# Per-replica connection budget and maximum time to wait for the pool. For an
# HPA deployment, reserve maxReplicas * maxConnections plus headroom in Postgres.
# MERIDIAN_CONTROL_DATABASE_MAX_CONNECTIONS=10
# MERIDIAN_CONTROL_DATABASE_ACQUIRE_TIMEOUT_SECONDS=5
# MERIDIAN_CONTROL_BIND_ADDR=0.0.0.0:8090
# MERIDIAN_CONTROL_HEALTH_ADDR=0.0.0.0:8091
# This service's externally reachable origin.
# MERIDIAN_CONTROL_PUBLIC_ORIGIN=http://127.0.0.1:8090
#
# DNS suffix for minted community hosts. `*.localtest.me` is public wildcard
# DNS that resolves to 127.0.0.1, which avoids editing /etc/hosts; note that
# `*.localhost` does NOT resolve on macOS. Fully offline? Add an explicit
# /etc/hosts entry per community and set the suffix to match.
# MERIDIAN_CONTROL_COMMUNITY_HOST_SUFFIX=relays.meridian.localtest.me
# Port appended to minted authorities. The relay's host resolution preserves a
# non-default port, so this must match the relay's listening port or no
# community will resolve. Leave unset behind a :443 ingress.
# MERIDIAN_CONTROL_COMMUNITY_HOST_PORT=3000
# MERIDIAN_CONTROL_RELAY_SCHEME=ws
#
# Browser relays page at {base}/relays. `off` makes the route 404
# rather than hiding its controls; the API it fronts stays available either way.
# MERIDIAN_CONTROL_COMMUNITIES_UI=on
# Keep this equal to MERIDIAN_MAX_COMMUNITIES_PER_OWNER on the relay.
# MERIDIAN_CONTROL_COMMUNITY_LIMIT=3
#
# Must match RELAY_OPERATOR_API_ORIGIN exactly (see the warning above).
# MERIDIAN_CONTROL_RELAY_OPERATOR_API_ORIGIN=http://127.0.0.1:3000
# Secret key whose pubkey appears in RELAY_OPERATOR_PUBKEYS. nsec or 64-char hex.
# MERIDIAN_CONTROL_RELAY_OPERATOR_SECRET_KEY=<nsec or 64-char hex>
#
# Identity provider: `dev` or `oidc`.
# dev — accepts ANY email address with no verification whatsoever. Refuses to
# start unless MERIDIAN_CONTROL_ALLOW_DEV_LOGIN=1 is also set, and
# reports `degraded: dev-login` on its readiness probe. Local use only.
# oidc — authorization-code + PKCE against a real issuer. Use this for any
# deployment reachable by anyone else. Keycloak is already in
# docker-compose.yml on :8180 for local OIDC testing.
# MERIDIAN_CONTROL_IDENTITY_PROVIDER=dev
# MERIDIAN_CONTROL_ALLOW_DEV_LOGIN=1
# MERIDIAN_CONTROL_OIDC_ISSUER=http://localhost:8180/realms/meridian
# MERIDIAN_CONTROL_OIDC_CLIENT_ID=meridian-control-plane
# MERIDIAN_CONTROL_OIDC_CLIENT_SECRET=<client secret>
#
# Point the desktop app at this control plane. There is no default and no
# hosted fallback: with either of these missing the app reports community
# creation as unavailable rather than contacting an endpoint the operator never
# chose. Both are read once at startup.
#
# A packaged app launched from Finder, the Dock, or a deep link inherits no
# shell environment, so a build that must create communities has to set these at
# BUILD time — they are compiled in as a fallback to the runtime values.
# MERIDIAN_CONTROL_PLANE_URL=http://127.0.0.1:8090/api/meridian
# MERIDIAN_CONTROL_PLANE_ORIGIN=http://127.0.0.1:8090
# Shared Redis-backed admission limits. Defaults shown below; each value must
# be a positive integer.
# MERIDIAN_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN=60
# MERIDIAN_RATE_LIMIT_HUMAN_API_CALLS_PER_MIN=300
# MERIDIAN_RATE_LIMIT_HUMAN_WS_EVENTS_PER_SEC=10
# MERIDIAN_RATE_LIMIT_AGENT_STANDARD_MESSAGES_PER_MIN=120
# MERIDIAN_RATE_LIMIT_AGENT_STANDARD_API_CALLS_PER_MIN=600
# MERIDIAN_RATE_LIMIT_AGENT_ELEVATED_MESSAGES_PER_MIN=300
# MERIDIAN_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).
# MERIDIAN_GIT_REPO_PATH=./repos
# MERIDIAN_GIT_MAX_PACK_BYTES=524288000
# MERIDIAN_GIT_MAX_REPO_BYTES=1048576000
# Process-local immutable pack/index cache. Zero disables retention.
# MERIDIAN_GIT_PACK_CACHE_PATH=./repos/.pack-cache
# MERIDIAN_GIT_PACK_CACHE_MAX_BYTES=5368709120
# MERIDIAN_GIT_PACK_CACHE_MAX_CONCURRENT_POPULATIONS=2
# Periodically repair relay-authored 30618 state from authoritative manifests.
# MERIDIAN_GIT_STATE_RECONCILE_INTERVAL_SECS=60
# -----------------------------------------------------------------------------
# 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.
MERIDIAN_S3_ENDPOINT=http://localhost:9000
MERIDIAN_S3_ACCESS_KEY=meridian_dev
MERIDIAN_S3_SECRET_KEY=meridian_dev_secret
MERIDIAN_S3_BUCKET=meridian-media
MERIDIAN_S3_REGION=us-east-1
MERIDIAN_S3_ADDRESSING_STYLE=path
# -----------------------------------------------------------------------------
# ReductStore — artifact/blob tier (opt-in, not started by default)
# -----------------------------------------------------------------------------
# A time-indexed blob store with an HTTP REST API, for artifact bytes whose
# access shape is "give me this range of this stream": huddle recordings, large
# attachments, telemetry. Its batching and range reads beat per-object S3
# GET/PUT for that shape. It is NOT a system of record — event truth is
# Postgres, and no reader resolves chat history from ReductStore.
#
# Two mutually exclusive profiles (same host port, two backings of one tier):
# COMPOSE_PROFILES=artifacts → local disk, free/OSS, works today
# COMPOSE_PROFILES=artifacts-s3 → MinIO behind it, REQUIRES a Pro licence
#
# The S3 remote backend is a ReductStore Pro commercial feature (v1.18+). Drop
# the key at .secrets/reductstore-license.key (gitignored) before using
# `artifacts-s3`; without it the container will not serve. When the remote
# backend is on, RS_DATA_PATH is ignored and local disk becomes a hot cache.
# REDUCTSTORE_VERSION=v1.20.11
# REDUCTSTORE_HOST_PORT=8383
# REDUCTSTORE_API_TOKEN=meridian_dev_reduct_token
# REDUCTSTORE_LOG_LEVEL=INFO
# REDUCTSTORE_MEMORY_LIMIT=512m
# `artifacts-s3` only — bucket is auto-created by the reduct-bucket-init one-shot.
# REDUCTSTORE_S3_BUCKET=meridian-artifacts
# REDUCTSTORE_CACHE_SIZE=10GB
# REDUCTSTORE_SYNC_INTERVAL=60
# REDUCTSTORE_LICENSE_DIR=./.secrets
# -----------------------------------------------------------------------------
# Event bus — zenohd router (COMPOSE_PROFILES=bus, default off)
# -----------------------------------------------------------------------------
# The bus is the pipe, not the plumbing. Postgres stays the system of record;
# Dragonfly keeps presence TTL, rate limits, NIP-98 replay and the fenced
# generation. zenohd routes events and stores nothing — no storage-manager
# plugin, no REST plugin, no dynamic plugin loading, no multicast scouting and
# no gossip scouting. The full contract lives in
# deploy/compose/zenoh/zenohd.json5 and is asserted by `just compose-check`.
#
# MERIDIAN_BUS and MERIDIAN_ZENOH_* are documented once, in the `Cross-pod
# event bus` block near the top of this file. That block owns the relay side;
# this one owns the router and the host port it is published on.
#
# WHAT THIS PROFILE IS FOR: one host. The relay refuses any non-loopback
# endpoint while the link profile cannot rise above the lane floors, so
# `tcp/localhost:<port>` against a router on this machine is the whole of what
# a Zenoh mode can reach today. A multi-pod endpoint (`tcp/zenohd:7447`,
# `tcp/<pod>.<svc>:7447`) is refused by the relay at startup — it is not a
# configuration this repo ships, and the adapter's own tests assert the refusal.
#
# THESE THREE MOVE TOGETHER. ZENOH_HOST_PORT is what Compose publishes;
# MERIDIAN_ZENOH_ENDPOINTS is what the relay dials; docker-compose.override.yml
# is what survives a bare `docker compose up -d`. Move one without the others
# and the relay reports a connect timeout while the container is healthy —
# the REDIS_HOST_PORT/REDIS_URL failure in a new costume. Discovery cannot
# paper over it: multicast and gossip are both off by design, so an endpoint
# the relay was not told about does not exist.
# ZENOH_HOST_PORT=7447
# MERIDIAN_ZENOH_ENDPOINTS=tcp/localhost:7447
#
# Image tag. Pinned in ONE place so the `zenoh` crate and the router image are
# the same release — the charter refuses a 1.8-library/1.9-router pair. Tags
# that exist on Docker Hub: 1.8.0, 1.9.0, 1.10.0 (also 0.11.0 and 1.0.x–1.7.x).
# Never :latest or :nightly, both of which exist and both of which float.
# ZENOH_IMAGE_TAG=1.8.0
# ZENOH_MEMORY_LIMIT=128m
# -----------------------------------------------------------------------------
# Media Upload Admission
# -----------------------------------------------------------------------------
# Bound image/video parser and storage work admitted by one relay process.
# MERIDIAN_MEDIA_MAX_CONCURRENT_UPLOADS=8
# MERIDIAN_MEDIA_MAX_CONCURRENT_UPLOADS_PER_PUBKEY=2
# MERIDIAN_MEDIA_UPLOADS_PER_MINUTE=30
# -----------------------------------------------------------------------------
# Product Feedback Destination
# -----------------------------------------------------------------------------
# Where the desktop "Send feedback" dialog's kind:42000 events go.
#
# local (default) stored in this deployment's product_feedback table and
# read only by this deployment's operators, through the admin API.
# Nothing leaves the deployment.
# disabled refused outright, with an explicit denial the submitter sees. The
# desktop disables the entry point and says why.
#
# This setting belongs in THIS file, not .env.local: .env.local is gitignored,
# so a feedback destination configured there would be invisible to code review
# and to git history — and feedback carries user-chosen screenshots plus a
# diagnostics blob.
#
# It is an enumeration, NOT a URL, and the relay REFUSES TO START on a URL-shaped
# value rather than storing locally while you believe you configured egress.
# Forwarding (upstream relay, webhook, SMTP) is deliberately not implemented:
# attachments are pinned to the accepting tenant, so a forwarded event arrives
# with references the destination cannot resolve, and the only outbound HTTP
# client in the tree rejects RFC1918 — where a sovereign deployment's own
# destinations live. See bead meridian-8i6m for the council record and the
# evidence that would reopen it.
#
# The client's privacy disclosure is derived from this value, so the two cannot
# drift: see desktop/src/features/settings/lib/feedbackPolicy.ts.
# MERIDIAN_FEEDBACK_DESTINATION=local
# -----------------------------------------------------------------------------
# Desktop Dev Signing (macOS only, optional)
# -----------------------------------------------------------------------------
# Name of a self-signed Code Signing certificate in your login keychain.
# When set, the desktop launch recipes re-sign freshly built dev binaries with
# it, so keychain "Always Allow", firewall, and privacy grants survive rebuilds
# instead of re-prompting on every build. One-time certificate setup:
# desktop/src-tauri/AGENTS.md, "Stable dev code signing".
# MERIDIAN_DEV_SIGN_IDENTITY="Meridian Dev"
# Require Blossom t=get auth and relay membership for GET/HEAD /media/*.
# Keep off until desktop/mobile/CLI clients that attach media read auth are deployed.
# MERIDIAN_REQUIRE_MEDIA_GET_AUTH=false
# Legacy alias accepted by the relay while rollout docs catch up:
# MERIDIAN_REQUIRE_MEDIA_READ_AUTH=false
# -----------------------------------------------------------------------------
# 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.
# MERIDIAN_EPHEMERAL_TTL_OVERRIDE=60
# How often the reaper checks for expired ephemeral channels (default: 60s).
# MERIDIAN_REAPER_INTERVAL_SECS=5
# -----------------------------------------------------------------------------
# Logging / Tracing
# -----------------------------------------------------------------------------
RUST_LOG=meridian_relay=debug,meridian_datastore=info,meridian_db=debug,meridian_auth=debug,meridian_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.
# MERIDIAN_OTEL_FILTER=meridian_relay=info,meridian_datastore=info
# OTLP tracing endpoint (optional — leave unset to disable)
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# -----------------------------------------------------------------------------
# ACP (Agent Communication Protocol — meridian-acp harness)
# -----------------------------------------------------------------------------
# The ACP harness bridges Meridian 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:
# MERIDIAN_PRIVATE_KEY=<hex> MERIDIAN_RELAY_URL=ws://localhost:3000 meridian-acp
# ── Identity & auth ──────────────────────────────────────────────────────────
# Nostr private key (hex or bech32). REQUIRED — identifies the agent on the relay.
# MERIDIAN_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.
# MERIDIAN_RELAY_URL=ws://localhost:3000
# ── Agent subprocess ─────────────────────────────────────────────────────────
# Binary to spawn as the AI agent (e.g. "goose", "codex-acp", "claude-code").
# MERIDIAN_ACP_AGENT_COMMAND=goose
# Comma-separated arguments passed to the agent binary.
# Goose default: "acp". Codex/Claude default: "" (empty).
# MERIDIAN_ACP_AGENT_ARGS=acp
# Binary for an optional MCP server sidecar (e.g. meridian-dev-mcp for meridian-agent).
# MERIDIAN_ACP_MCP_COMMAND=
# Number of parallel agent subprocesses (1–32).
# MERIDIAN_ACP_AGENTS=1
# Desired LLM model ID. Applied to every new ACP session.
# Use `meridian-acp models` to discover available model IDs.
# MERIDIAN_ACP_MODEL=
# ── Timeouts & sessions ──────────────────────────────────────────────────────
# Max seconds per agent turn before timeout (default 320 = ~5 min).
# MERIDIAN_ACP_TURN_TIMEOUT=320
# Max turns per session before proactive rotation. 0 = disabled (rotate only
# on MaxTokens / MaxTurnRequests). Recommended: 50 for long-running agents.
# MERIDIAN_ACP_MAX_TURNS_PER_SESSION=0
# ── Prompts ──────────────────────────────────────────────────────────────────
# System prompt injected into every agent session (inline text).
# MERIDIAN_ACP_SYSTEM_PROMPT=
# Path to a file containing the system prompt (mutually exclusive with above).
# MERIDIAN_ACP_SYSTEM_PROMPT_FILE=
# Message sent to the agent immediately after session creation.
# MERIDIAN_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.
# MERIDIAN_ACP_HEARTBEAT_INTERVAL=0
# Heartbeat prompt text (inline). Mutually exclusive with file variant.
# MERIDIAN_ACP_HEARTBEAT_PROMPT=
# Path to a file containing the heartbeat prompt.
# MERIDIAN_ACP_HEARTBEAT_PROMPT_FILE=
# ── Map tile source (Apps → Map) ─────────────────────────────────────────────
# There is NO default tile host, deliberately. A map that silently reaches a
# public CDN is an outbound connection nobody authorised, which is the fail-open
# shape MERIDIAN_CONTROL_PLANE_URL already refuses; the rejection is recorded in
# .settings/features/feature-tactical-picture.md.
#
# Unset, the map draws the bundled 1:110m Natural Earth coastline and a
# graticule. That is a usable picture with no tile network at all (DDIL,
# MIP-DD) — coarse, and the caption under the map says so.
#
# Set this to a MapLibre style URL to draw real tiles. For a sovereign
# deployment that means a self-hosted style. Two keyless public styles, for
# development and demos only — each one is a third-party host that will see
# your viewport, so do not point a deployment carrying real tracks at them:
# VITE_MERIDIAN_MAP_STYLE_URL=https://tiles.openfreemap.org/styles/bright
# VITE_MERIDIAN_MAP_STYLE_URL=https://demotiles.maplibre.org/style.json
# VITE_MERIDIAN_MAP_STYLE_URL=
#
# Optional: terrain-RGB raster-DEM tiles (enables the elevation readout), and
# an equirectangular photosphere image.
# VITE_MERIDIAN_MAP_TERRAIN_URL=
# VITE_MERIDIAN_MAP_PHOTOSPHERE_URL=
# ── 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_MERIDIAN_FORCE_FRESH_ONBOARDING=true
# DEV-only: resolve the dev identity before launch and pass it to the desktop
# app as MERIDIAN_PRIVATE_KEY, so the app never opens the OS keyring.
# Two effects:
# - macOS stops asking for the login-keychain password after every rebuild
# (`tauri dev` emits an ad-hoc signature whose hash changes each build, so
# the keychain ACL sees a new app every time and "Always Allow" never
# sticks). /usr/bin/security does the read instead, and its own
# "Always Allow" holds because Apple's signature is stable.
# - worktrees share the main checkout's identity and skip onboarding.
# `just desktop-standalone` ignores this and stays on its isolated identity.
# NOTE: an explicit MERIDIAN_PRIVATE_KEY above wins. Setting that var for
# the ACP harness therefore also becomes the desktop app's identity.
# MERIDIAN_SHARE_IDENTITY=1
# ── Subscription & filtering ─────────────────────────────────────────────────
# Subscribe mode: "mentions" (default), "all", or "config" (rule-based).
# MERIDIAN_ACP_SUBSCRIBE=mentions
# Comma-separated event kind numbers to subscribe to (overrides mode defaults).
# MERIDIAN_ACP_KINDS=
# Comma-separated channel UUIDs to limit subscription scope.
# MERIDIAN_ACP_CHANNELS=
# Set to true to disable the @-mention filter in mentions mode.
# MERIDIAN_ACP_NO_MENTION_FILTER=false
# Path to TOML config file for rule-based subscriptions (config mode).
# MERIDIAN_ACP_CONFIG=./meridian-acp.toml
# ── Dedup & self-ignore ──────────────────────────────────────────────────────
# How to handle duplicate events: "queue" (default) or "drop".
# MERIDIAN_ACP_DEDUP=queue
# Set to true to process the agent's own messages (default: ignore self).
# MERIDIAN_ACP_NO_IGNORE_SELF=false
# ── Context ──────────────────────────────────────────────────────────────────
# Max context messages fetched for thread replies and DMs (0–100). 0 = disabled.
# MERIDIAN_ACP_CONTEXT_MESSAGE_LIMIT=12
# ── Presence & typing ────────────────────────────────────────────────────────
# Set to true to disable automatic online/offline presence status.
# MERIDIAN_ACP_NO_PRESENCE=false
# Set to true to disable typing indicators while the agent is processing.
# MERIDIAN_ACP_NO_TYPING=false
# ── Advanced tuning ──────────────────────────────────────────────────────────
# Event channel buffer capacity (WebSocket → harness). Increase for
# high-throughput agents. Minimum 1.
# MERIDIAN_ACP_EVENT_BUFFER=256
# ── Legacy aliases ───────────────────────────────────────────────────────────
# These are accepted for backward compatibility but the canonical names above
# are preferred:
# MERIDIAN_ACP_PRIVATE_KEY → MERIDIAN_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.
# MERIDIAN_TERMS_OF_SERVICE_MARKDOWN="# Terms of Service\n\nFull terms here."
# MERIDIAN_PRIVACY_POLICY_MARKDOWN="# Privacy Policy\n\nFull policy here."
# MERIDIAN_AGE_ATTESTATION_REQUIRED=true
# 32-byte hexadecimal root for encrypted workflow credentials. Required before
# any {{secret.NAME}} reference can be written or executed. Supply it through
# the deployment secret/KMS path and include the key version in recovery drills.
#
# Leaving this unset also disables the WEBHOOK TRIGGER entirely: the relay has
# nowhere to mint the trigger secret, so it refuses to save any workflow with
# `trigger: {on: webhook}` — including the "Deploy Approval" template in the
# desktop gallery. That is a deployment choice, not a broken template. Generate
# one with `openssl rand -hex 32` to turn webhook triggers on locally.
# MERIDIAN_WORKFLOW_SECRET_KEY=
# Expand/contract barrier. Keep false while N-1 is a supported rollback target.
# Enabling permits {{secret.NAME}} definitions and removes legacy webhook
# credentials as definitions are migrated/edited.
MERIDIAN_WORKFLOW_SECRET_REFS_ENABLED=false