SynapSynapDocs

Connect to the Control Plane

Let people sign in to a self-hosted pod with a Synap account, register the pod with Synap Cloud, and what the pod keeps for itself.

For Self-hostersOpen-source podSynap CloudbetaReviewed 2026-10-20

A pod is a complete, independent server. It owns its users, memberships, workspaces, projects and sessions, and it trusts no outside service by default. Connecting it to the Synap Control Plane (or to another account service) is optional, and does not put that service's user or pod identifiers into the pod's identity model.

Sign in with a Synap account

By default, people sign in to a self-hosted pod with email and password. To also offer "Continue with Synap Cloud":

cd /opt/synap/deploy
sudo synap federation connect-cloud      # uses CONTROL_PLANE_URL, else https://api.synap.live
sudo synap federation status
sudo synap federation disable            # back to password sign-in only

connect-cloud accepts --control-plane <url> for another Control Plane and --label <text> for the button's label. It sets CONTROL_PLANE_URL, regenerates the sign-in configuration and restarts Kratos. The first time someone uses the button, they sign in once with their pod password to link the two accounts; after that, the button signs them in directly.

Register the pod with the Control Plane

A pod can be registered with the Control Plane at install time, with a single-use registration code (spr_…) created for you by the Control Plane:

curl -fsSL https://github.com/synap-core/backend/releases/download/channel-stable/install.sh \
  | sudo bash -s -- --name <name> --register-token spr_… [--domain pod.example.com]
  • --name is required with a registration code: it is the name the code was created for. Without --domain, the pod is served at pod.<name>.synap.live.
  • Once the pod is healthy, it registers: the person the code was created for becomes its owner. A registered pod cannot also be given a pre-seeded admin (--admin-password); the two are exclusive.
  • --control-plane <url> selects another Control Plane (default https://api.synap.live).

sudo synap policy show prints what the Control Plane may ask this pod to do, and sudo synap policy set-mode managed|self-hosted changes it. See Security hardening.

The pod's identity key

A registered pod signs its own calls to the Control Plane with a P-256 key generated on the server, in deploy/state/pod-identity/pod-key.pem (mode 600). Only its public half is ever sent: at registration, or with enroll for a pod that was installed without a registration code.

sudo synap pod-key show                                   # the public key (creates the key if missing)
sudo synap pod-key enroll --pod-id <id> --code spr_…      # a pod installed by hand; the code comes from the Control Plane

enroll also takes --control-plane <url>; by default it uses CONTROL_PLANE_URL, then https://api.synap.live.

What each side owns

SideOwns
The account service (for example the Synap Control Plane)Its accounts, passwords, sessions and invitations, and its record of which people relate to which pods
The podWhich issuers it trusts, its local identities, memberships and access decisions, workspaces, projects and its own sessions
The browser or appTalks to both directly; carries only short-lived assertions or one-time receipts between them

The account service never receives a pod session token.

The federation endpoints

For people building an account service or debugging one. All are under /api/federation on the pod:

EndpointPurpose
POST /bootstrapCreates the first local owner of a fresh pod from a short-lived signed assertion, authorized by the pod's local installation secret. A second owner cannot be bootstrapped this way.
POST /identity-linksA signed-in person links their pod account to an account service. Returns a one-time receipt.
POST /identity-links/consumeThe account service redeems the receipt, server to server.
POST /access-grantsApplies an accepted invitation: a signed assertion naming one workspace or project on the pod.

Assertions are ES256-signed by the issuer, addressed to the pod's exact public origin, short-lived and single-use. The issuer publishes its keys as a JWKS over HTTPS. The pod rejects issuers and key locations that resolve to private or local network addresses.

How the pod checks an issuer

The pod accepts a signed assertion only from an issuer in its trusted-issuer registry, approved, and holding the scope the endpoint requires. Setting CONTROL_PLANE_URL (which synap federation connect-cloud does) makes the pod, each time it starts, add that Control Plane to the registry with a fixed set of scopes. It reads the issuer name the Control Plane signs with from <CONTROL_PLANE_URL>/federation/metadata, and falls back to CONTROL_PLANE_URL itself. Extra scopes the Control Plane asks for wait for a pod admin's approval, unless the pod setting trust.autoAcceptCpScopes is on (it is off by default). An environment variable is never trusted on its own: without a registry entry, nothing is accepted.

For every assertion, the pod:

  • selects the signing key by the token's kid and accepts only ES256;
  • checks the issuer, the expiry and the audience, which must be this pod;
  • checks the token's purpose (type), so a token minted for one endpoint cannot be used on another;
  • requires a jti and records it, so the same token is refused the second time.

Keys are fetched from <issuer>/.well-known/jwks.json over HTTPS, with a 10-second timeout and a 256 KB limit, and cached for 24 hours. A token signed with a kid the cache does not hold triggers a fresh fetch, so an issuer can rotate its key without any change on the pod. If a fetch fails, the pod retries after 1, 5 and 30 seconds, then falls back to the last key set it fetched, for up to 48 hours.

The older provisioning endpoints /api/provision/connect, /authorize-issuer, /seed-trust, /seed-admin and /activate-member are retired and answer with a retirement response.

On this page