Security

Authentication

How you prove who you are to Synq. Session cookies with optional two-factor, personal access tokens, MCP OAuth, and opt-in zero-auth loopback mode.

Every request to Synq has to answer one question first: who are you? Synq accepts four different ways to answer it, and which one applies depends on where the request comes from: a browser tab (the desktop app counts as one), a script, an AI agent, or a local single-user instance talking to itself. This page walks through each one.

Base path

Every API route lives under /api on your panel, for example https://app.<domain>/api/projects. The same authentication rules apply to all of them; there is no unauthenticated back door. Access checks after sign-in are covered separately in Permission model.

The four ways in

MethodWhat you sendWhen it applies
Session cookieAn httpOnly cookie the browser holds for youThe dashboard, in a browser or the desktop app
Personal access token (PAT)Authorization: Bearer synq_pat_…The CLI, scripts, server-to-server, and MCP clients without OAuth
MCP OAuthAn OAuth 2.1 access token the client fetches for youAI agents (Claude, Cursor, …) connecting to /api/mcp
Zero-auth loopbackNothing: the request just comes from 127.0.0.1A local single-user instance you opt into

A single request uses exactly one of these. Synq checks for a Bearer token first; if there isn't one, it looks for a session cookie; if there's neither, it falls back to the loopback path (and only if that mode is turned on).

Session cookies (the dashboard)

When you sign in to the dashboard, Synq gives your browser a signed, httpOnly session cookie. "httpOnly" means JavaScript running in the page cannot read it, so even if a bad script somehow ran on the page, it couldn't steal your session. The browser sends the cookie automatically on every request; you never handle it yourself.

On a Synq install the cookie is named synq.session_token. It belongs to the panel's host, so when the panel moves to a new address everyone signs in again there.

Bearer tokens are refused from the dashboard's own origin

If a request arrives with a Bearer header and an Origin matching one of the dashboard's trusted browser origins, Synq rejects it (401 BEARER_NOT_ALLOWED_FROM_BROWSER). Bearer tokens are for CLIs and servers; a token showing up from a browser tab is the signature of a stolen credential being replayed, so the door is closed. The CLI sends no Origin header at all, which is why its tokens are accepted.

Two-factor authentication

Password sign-in can ask for a second step: a 6-digit code from an authenticator app, or one of 10 single-use backup codes. Each person turns it on for their own account under Settings, Security & 2FA, and can trust a device for 30 days. If the owner loses both, synq reset-admin-password on the server resets the password and turns two-factor off. Tokens and MCP clients never ask for a code. Full steps: Two-factor authentication.

Personal access tokens (Bearer)

A personal access token (PAT) is a long string you send in the Authorization header to prove who you are without a browser:

Authorization: Bearer synq_pat_…

Every token looks like synq_pat_ followed by a 43-character random secret (256 bits of entropy). Synq stores only a SHA-256 hash of it: the plaintext is shown to you exactly once, at creation, and can never be retrieved again. Tokens created by earlier versions of Synq start with a different prefix and keep working until they expire or you revoke them. A token always acts as the person who created it and carries that person's role, unless you narrow it (see below).

Create one

In the dashboard, open Settings, API tokens and use the Personal Access Tokens section:

  1. Type a name (for example "MCP client").
  2. Pick an expiry: No expiry, 30 days, 90 days or 1 year.
  3. Optionally tick Read-only, and Limit to specific resources to choose the resources it can reach.
  4. Press Create token and copy it: this is the only time it's shown.

The list shows each token with read-only and scoped badges, when it was last used, when it expires and how many calls it made, with a Revoke button.

From the terminal:

# A full-access token that acts as you
synq token create "my laptop"

# A read-only token that expires in 90 days
synq token create "ci-readonly" --read-only --expires 90

# A token scoped to one project (read + write only on that project)
synq token create "deploy-bot" --grant project:proj_123:read,write

synq token list      # your tokens (never shows secrets)
synq token revoke <id>

Use a token from the CLI

synq login stores a token so the CLI can send it on every call. Run it with --token synq_pat_… for a non-interactive login (handy in CI), or run it bare to open the token settings page and paste one in. The stored token is only ever sent as a Bearer header, never as a cookie.

The request body behind the create call:

Prop

Type

Two ways to narrow a token

  • Read-only: the token can call read endpoints, but any mutating request is refused with 403 TOKEN_READ_ONLY. Great for dashboards, monitoring, and CI that only needs to look.
  • Scoped: instead of acting with your full role, the token is confined to a specific list of resources you grant it (a project, a server, a repository, …), each with read, write, or admin. A scoped token acts as a restricted principal: it can do less than you, never more. You can only grant access you already hold yourself; the server checks this before minting.

A token is tied to one organization

Each token is bound to the organization it was created in. If a request carries an X-Organization-Id header naming a different org, it's rejected with 403 TOKEN_ORG_SCOPE. An invalid or revoked token returns 401 INVALID_TOKEN.

MCP OAuth (AI agents)

Synq is a standards-compliant OAuth 2.1 server for the Model Context Protocol (MCP), the way AI agents call tools. An MCP-capable client (Claude, Cursor, …) points at https://app.<domain>/api/mcp, and the handshake is automatic: the first unauthenticated request returns a 401 pointing at /.well-known/oauth-protected-resource, the client registers itself, runs the PKCE authorize flow (Proof Key for Code Exchange, a way to complete OAuth safely without a stored client secret), and you approve it on a consent screen in your browser.

That consent screen is where you set the agent's limits: read-only and/or a specific set of projects, servers and repositories. Your choices are saved as the client's binding and enforced through the exact same grant model as a scoped PAT. An OAuth token that somehow skips consent has no binding, and is therefore denied everything: there is no "authenticated but unscoped" state. Tokens are also bound to the endpoint address they were issued for.

You manage connected agents under Settings, MCP, where you can see each client, change what it can reach (Edit access), see what it did (Activity) and disconnect it (which revokes its tokens immediately). Clients without OAuth support can instead authenticate with a plain PAT as a bearer credential.

Full MCP setup

Endpoint URLs, client config snippets, and verification steps live on the MCP page.

Zero-auth loopback mode

An API that runs on your own machine, just for you, can skip the login. In zero-auth mode the API auto-provisions a single local admin user (Local User, owner of a personal workspace) and treats local traffic as that user. No password, no cookie to manage.

This is safe only because it is gated, and every gate must pass:

Every condition is required

  1. Not CLI-managed or public. An instance started from the CLI (synq up sets SYNQ_REQUIRE_AUTH) or served publicly (--public-url / SYNQ_PUBLIC_URL) refuses zero-auth outright: it always requires a login.
  2. Opt-in. The instance's auth mode must be none, and the operator must also set SYNQ_ALLOW_ZERO_AUTH=true (an API started with DEPLOY_MODE=desktop, as desktop app versions up to 1.2.5 ran on your computer, doesn't need the variable).
  3. Loopback only. The request must come from a loopback address (127.0.0.1 / ::1), read from the kernel-reported TCP peer, not the Host header, which a client or a misconfigured reverse proxy could forge. A request from anywhere else is rejected with 401, even in zero-auth mode.

Why condition 1 exists

With --public-url, the dashboard's same-origin proxy runs on the box, so it reaches the API from loopback, which would otherwise slip past the loopback gate (condition 3). The explicit CLI/public refusal closes that hole, so a remote visitor can never land on the zero-auth path.

On a normal self-hosted install this path is never even reached: the auth mode defaults to local, so a missing session is a plain 401. Turning it on is a deliberate operator choice (auth mode none and SYNQ_ALLOW_ZERO_AUTH=true), and even then the loopback check keeps it usable only from the machine itself. A request arriving through a network reverse proxy carries the proxy's address as its TCP peer, not loopback, so it is refused.

Synq recognizes three auth modes overall:

  • none: zero-auth loopback, described above. Never used by a CLI-started or public instance.
  • local: standard login required (email + password via Better Auth, with optional two-factor). Default for a fresh self-hosted install, and always used for CLI (synq up) and public instances.
  • cloud: signs in with a hosted-cloud account instead of a local one. It belongs to the hosted cloud, which is switched off in Synq, so a Synq install never uses it.

Creating the first admin

A CLI or self-hosted instance has no public sign-up. The first admin (email + password) is created at install time (the installer asks for its email and password) over an internal-token-gated, one-shot endpoint that a browser can't reach. That account is the instance administrator. After that, add more people from Settings, Users & team in the dashboard (see Teams & members).

One failure that never falls through

If the session machinery itself errors (a database outage, a decryption failure), the request returns 503 AUTH_UNAVAILABLE. It does not quietly drop to the zero-auth path: a broken session check can never be mistaken for "no login required."

Where credentials live

Synq keeps secrets where they're safest instead of copying them around:

  • PAT secrets are stored only as a SHA-256 hash; the plaintext exists just once, in your clipboard.
  • Two-factor secrets and backup codes are stored with the account in Synq's database; synq reset-admin-password deletes them for the owner.
  • GitHub Apps are registered by each workspace in Settings (see Self-hosted GitHub Apps); each private key, client secret, and webhook secret is stored in a versioned AES-256-GCM envelope and is never returned by the API. The legacy process-environment configuration remains supported. Every App path mints short-lived, repository-scoped installation tokens.
  • Per-project git tokens and environment secrets are encrypted at rest with the install's BETTER_AUTH_SECRET. Keep that key safe: panel backups can include it for a restore.

Revocation

  • A token: synq token revoke <id>, or the Revoke button in Settings, API tokens. It stops working on its next request.
  • An MCP client: disconnect it under Settings, MCP: its issued tokens are revoked and the consent is dropped, so reconnecting re-prompts.
  • A person: removing a member ends their access; their session stops working on its next check.

Errors you might see

Status & codeMeaning
401 INVALID_TOKENThe bearer token is wrong, expired, or revoked.
401 BEARER_NOT_ALLOWED_FROM_BROWSERA bearer token arrived from a dashboard browser origin: send it from a CLI or server instead.
403 TOKEN_READ_ONLYA read-only token tried a mutating request.
403 TOKEN_ORG_SCOPEThe token is bound to a different organization than the one requested.
401 UnauthorizedNo valid session, or a zero-auth request that failed the mode/loopback gates.
503 AUTH_UNAVAILABLEThe session check itself failed. Retry; it never silently downgrades to no-auth.

On this page