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:
- One policy, authored once, enforced everywhere. The plane serves the
team's signed
enterprise-policy.json; every member's Mergen pulls it. - 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:- On the plane host:
mergen-server connect sync-plane --plane. It makes the key pair, writes the private key to the plane's.envand prints the public key. The private key never leaves that host, so keep governed agents off it. - An Owner or Platform Admin pastes the public key in My Organization → Settings → Security → Sync Plane key.
- 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-runsconnect sync-plane, so re-run it on every member after a leak.
- On the plane host:
- 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 throughconnect 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.