← All docs

Self-hosted sync plane (Team and up)

For security-conscious teams that want shared policy and a unified audit trail across every developer, tool, and machine — without any data leaving their infrastructure. The sync plane is a single Mergen instance you run yourself; there is no Mergen cloud in the loop and no Postgres/Redis required.

It delivers two things the individual per-laptop gates can't on their own:

  1. One policy, authored once, enforced everywhere. The plane serves the team's signed enterprise-policy.json; every member's Mergen pulls it.
  2. One audit trail across the whole fleet. Every member pushes its cross-tool rollup (which IDE did what), and the plane aggregates them into a single pane no native tool config produces.

Combined with cross-tool policy compile-down, this is the full platform-risk answer: Mergen authors the policy for every tool and is the only place that sees what all of them did — on your own hardware.


Get the two shared secrets

The plane and every member need to agree on the same MERGEN_POLICY_HMAC_SECRET and MERGEN_POLICY_SYNC_TOKEN. On a paid plan (Team and up), fetch them instead of generating and distributing them by hand — every teammate resolves to the same org and gets back the identical values independently, so nothing has to cross Slack, 1Password, or email:

mergen-server connect sync-plane

Without a paid plan (or if you'd rather not fetch them), generate the pair yourself and distribute it out of band:

export MERGEN_POLICY_HMAC_SECRET=$(openssl rand -hex 32)   # signs the served policy
export MERGEN_POLICY_SYNC_TOKEN=$(openssl rand -hex 32)    # authorizes member pull/push

Run the plane

docker compose -f docker-compose.sync-plane.yml up -d

The plane binds to 0.0.0.0:3000 (team mode) and persists everything to a local volume (mergen-sync-data). No cloud, no external database — the two secrets above are the only thing this plane and Mergen's own infra ever have in common, and they never touch Mergen's infra either.

Point each member at it

On every developer machine and CI runner running Mergen, set:

MERGEN_POLICY_URL=http://<plane-host>:3000/policies/sync   # pull the shared policy
MERGEN_FLEET_SYNC_URL=http://<plane-host>:3000             # push this member's audit rollup
MERGEN_POLICY_HMAC_SECRET=<same value as the plane>        # verify the policy signature
MERGEN_POLICY_SYNC_TOKEN=<same value as the plane>         # authorize pull + push
# Optional: a stable name for this machine in the fleet view (defaults to hostname)
MERGEN_MEMBER_ID=alice-laptop

mergen-server connect sync-plane prompts for the plane host and writes all four vars at once when you already know it; otherwise it writes just the two secrets and you add MERGEN_POLICY_URL/MERGEN_FLEET_SYNC_URL once the plane is up. Check a member's connection afterward with GET /health/integrations (the team-sync entry) — it reports which half (policy pull, audit push) is configured and when each last synced successfully.

Members pull the policy on startup and every 5 minutes (cheap ETag/304 polling), and push their audit rollup on the same cadence. The plane's block_destructive_commands and other immutable hard-safety rules always survive a policy sync — a misconfigured or compromised plane can never disable the destructive-command gate on a member.

Endpoints

Endpoint Who calls it Auth
GET /policies/sync member Mergen (pull policy) X-Mergen-Sync-Token + ETag; body HMAC-signed
POST /audit/ingest member Mergen (push rollup) X-Mergen-Sync-Token
GET /audit/fleet operator (read the fleet view) operator local secret (x-mergen-secret)

GET /audit/fleet returns the whole team in one object: which machines are reporting, per-tool (per-IDE) verdict totals summed across every member, and a merged newest-first decision timeline.

Deployment boundary — Layer-1 self-hosted, NOT the multi-tenant SaaS

The sync plane, and the Layer-1 features it aggregates, are single-team, own-infra by design. Keep them out of the shared multi-tenant cloud SaaS instance (MERGEN_CLOUD_MODE=true serving many tenants).

Specifically, these features are single-tenant / local-first and are not tenant-partitioned:

  • The durable action ledger (~/.mergen/action-ledger.db, GET /audit/cross-tool)
  • The fleet audit aggregation (fleet-audit.json, POST /audit/ingest, GET /audit/fleet)
  • Detection packs + the community regression corpus (gate-reports.json, community-corpus.json, POST /gate-reports, /packs/detection/*)

They are backed by process-global stores scoped to one team's own deployment. If they were exposed inside a shared multi-tenant instance they would mix data across tenants — so don't run them there.

The division of responsibility:

Plane What runs there Isolation
Multi-tenant cloud SaaS (MERGEN_CLOUD_MODE=true) Telemetry ingest, OTLP, incident/credential paths Postgres, cloudAuthMiddleware sets req.tenantId from the API key; every read filters by tenant_id
Layer-1 self-hosted (this sync plane) The gate, action ledger, fleet audit, detection packs Single team, own infra; not tenant-partitioned by design

This isn't a limitation to fix — it's the architecture. The gate is local-first (Layer 1); the sync plane is one team running their own instance. If these ever become cloud-managed per-tenant capabilities, they would first need cloudAuthMiddleware on their routes and tenant-scoped stores (thread tenantId into the ledger callers, partition the three file stores by tenant).

Security model

  • Integrity (Ed25519, 2026-10-26): the plane signs the served policy with a private key only it holds (MERGEN_POLICY_SIGNING_KEY, X-Mergen-Policy-Signature-Ed25519), and members verify with its public key (MERGEN_POLICY_PUBLIC_KEY). A member with a public key refuses an unsigned, tampered or HMAC-only policy, so neither an attacker on the path nor another member (or an agent on their machine) can serve it a policy that verifies. To set it up:
    1. On the plane host: mergen-server connect sync-plane --plane. It makes the key pair, writes the private key to the plane's .env and prints the public key. The private key never leaves that host, so keep governed agents off it.
    2. An Owner or Platform Admin pastes the public key in My Organization → Settings → Security → Sync Plane key.
    3. Each member re-runs mergen-server connect sync-plane, which now writes the public key and no longer receives the shared secret. Publishing a new key keeps the one it replaced handed out to members for 7 days, so they can move over. If the old private key may have leaked, tick "The old key may have leaked" when publishing: it is dropped at once. Either way a member keeps whatever keys it last wrote until it re-runs connect sync-plane, so re-run it on every member after a leak.
  • Legacy HMAC (until a public key is published): the served policy is also HMAC-signed with MERGEN_POLICY_HMAC_SECRET. HMAC is symmetric: every member holds the secret to verify, so every member can also sign, and a member who can intercept another member's pull could serve them a policy that verifies. An org with no published key still receives the secret through connect sync-plane (each hand-out is logged on api.mergen.app); once a key is published it no longer does. Rotate the shared secret after publishing, so copies already handed out stop working on the plane.
  • Access: cross-machine pull/push present MERGEN_POLICY_SYNC_TOKEN, never the operator's local secret. The operator-only fleet view stays behind the local secret.
  • Least data: the plane aggregates per-member summaries, not raw command streams, and holds a bounded, per-member snapshot. Everything stays in the local volume you control.

Last updated October 31, 2026