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
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>
660 lines
36 KiB
Bash
660 lines
36 KiB
Bash
# =============================================================================
|
||
# 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
|