# Permissions & roles
URL: https://synquic.tech/docs/security/permissions.md

How Synq decides who can do what, through organizations, the four roles, and per-resource grants, all enforced by one permission plane.

import { Step, Steps } from 'fumadocs-ui/components/steps';

Every request into Synq passes through one gate, the **permission plane**, that answers a single question: *is this person allowed to do this thing to this resource?* Think of it like the front desk of a building: your keycard says which floors you can reach, and the desk checks it every single time, not just once at the door.

Access is decided by three things working together: which **organization** a resource belongs to, your **role** in that organization, and (for the tightest role) the specific **grants** you've been given.

<Callout title="One gate, no side doors" type="info">
Synq refuses to boot if any API route forgets to declare a permission. There is no "unprotected by accident" route: the safe default is enforced at startup, not left to reviewers to catch.
</Callout>

## Organizations

An **organization** (or "org") is the container that owns everything: projects, servers, domains, backups, billing. You're a **member** of one or more orgs, and your access is decided *per organization*: being an owner of one org says nothing about your role in another.

When you make a request, Synq figures out which org you mean from the `X-Organization-Id` header (the dashboard and other API clients set this) or, as a fallback, your session's default org. Every resource carries its own `organization_id`, so the platform can always trace a project or domain back to the org that owns it and check your membership there.

## The four roles

Your role in an org sets your baseline access. From most to least powerful:

| Role | What they can do |
|---|---|
| **owner** | Everything, including **billing**. Usually the person who created the org. |
| **admin** | Everything **except billing**, including managing members and invitations, and reading the audit log. |
| **member** | Full access (read, write, **and delete**) to **all resources except billing and the audit log**. Can't manage other members. |
| **restricted** | **No access by default.** Can only reach the specific resources they're explicitly granted. |

<Callout title="Owner vs admin, in plain words">
An **owner** holds the keys to the money: plans, payment, and cancellation. An **admin** runs the day-to-day: they can invite teammates, deploy, add domains, and read the audit log, but they can't touch billing. A **member** is a trusted teammate who ships work but doesn't manage the team or see billing. **Restricted** starts with an empty keyring and gets keys one at a time.
</Callout>

<Callout title="The standard roles are coarse by design (beta)" type="warn">
Owner, admin, and member are graded by resource **type**, not by action: a member has read, write, **and delete** across projects, servers, deployments, domains, jobs, and environment variables, the same as an admin apart from managing people and the audit log. Per-**action** (read vs write vs admin) and per-**resource** limits are enforced only through the **restricted** role and **scoped access tokens** described below. Treat member and admin as trusted, near-equal roles, and reach for a restricted member or a scoped token whenever you need real least-privilege. Fine-grained delegation for the standard roles is still maturing.
</Callout>

## Permission tags: `resource:action`

Under the hood, each route declares a short tag that names a resource and an action, like `project:read` or `domain:write`. Sub-resources add a middle segment: `project:service:write` means "edit a service that lives inside a project."

The **action** is the verb, and it lines up with the HTTP method:

| Action | HTTP method | Meaning |
|---|---|---|
| `read` | `GET` (one resource) | View a single thing by id. |
| `write` | `POST` / `PUT` / `PATCH` | Create or change something. |
| `admin` | `DELETE` (and other destructive actions) | Remove, reset, or otherwise destroy it. |
| `list` | `GET` (a collection) | List things in the org, with no specific id. |

You'll see these tags in the [API reference](/docs/api) next to each endpoint, so you always know which action a call performs.

## Restricted members and resource grants

A **restricted** member is the interesting one. They start with zero access. To let them do anything, an owner or admin gives them **grants**: small records that say "this person may `read` / `write` / `admin` *that* resource."

Each grant lists one or more of these levels. When Synq checks an action, a higher level also satisfies the lower ones:

- **read**: view and list the resource.
- **write**: create and change it (this also satisfies a `read` check).
- **admin**: delete and fully manage it (this also satisfies `write` and `read`).

A grant targets a resource by its id, or uses a wildcard `*` to mean *every resource of that type in the org*. So a wildcard **write** grant on Projects lets someone deploy to all of them, while a single project id keeps them to just one.

### Grants flow downhill

You grant on a top-level resource, and the access **inherits down its tree**: you don't grant each child separately:

| Grant on a… | …also covers |
|---|---|
| **Project** | its deployments, domains, services, and environment variables |
| **Backup destination** | its backup policies, runs, and restores |
| **Server** | terminal access and runtime operations (logs, restarts, start/stop) on that server |
| **Mail server** | webmail, branding, and mail settings |

### What you can grant

The resource picker offers these grantable types:

| Type | Covers |
|---|---|
| **Projects** | Apps and everything inside them (deployments, domains, env vars). |
| **Servers** | Connected machines and their runtime/terminal access. |
| **Mail servers** | Email hosting and webmail. |
| **Backup destinations** | Where backups are stored, and their runs. |
| **Billing** | Plans and payment (hosted cloud only; switched off in Synq). |
| **Audit log** | The record of who did what. |
| **GitHub orgs** | Every repo under one GitHub installation. |
| **GitHub repos** | A single repository. |

<Callout title="Which types you'll see" type="info">
The picker adapts to your instance. A Synq install is self-hosted, so it shows Projects, Servers, Mail servers, Backup destinations, the Audit log, and GitHub. Billing belongs to the hosted cloud, which is switched off in Synq, so it isn't offered.
</Callout>

## Inviting people and setting roles

Team management lives under **Settings**, **Users & team**.

<Steps>

<Step>

### Open the invite dialog

Go to **Settings**, **Users & team** and press **Invite member**. Enter the person's email address.

</Step>

<Step>

### Choose a role

Pick **Member**, **Admin**, or **Restricted**. (Owner is the org creator's role; you don't hand it out from the invite dialog.)

- **Member** and **Admin** get their access immediately on joining.
- **Restricted** opens a resource picker on the right, where you choose exactly what the invite unlocks.

</Step>

<Step>

### Send it

Synq emails a join link. When the invitee accepts, their membership is created and any pending grants are applied. You can always widen or narrow a restricted member's grants later from their row in the team list.

</Step>

</Steps>

## Instance administrators

Roles belong to a workspace. One level above them is the **instance administrator**: the admin account created when
Synq was installed (the admin email the installer asked for). Some things concern the whole install rather than one workspace,
so only an instance administrator can do them:

- create **new accounts** by inviting an email address that has no account yet;
- turn on and configure [panel backups](/docs/guides/panel-backups) (**Settings**, **Instance**);
- run or schedule a [Docker cleanup](/docs/guides/docker-cleanup) on a server;
- change the [Let's Encrypt contact email](/docs/guides/server-domains#lets-encrypt-contact-email);
- change or run the built-in [system jobs](/docs/guides/jobs#built-in-jobs);
- export or import the whole instance.

`synq reset-admin-password` on the server always recovers the first account created on the install.

## Access tokens follow the same rules

For scripts, the CLI, and AI agents, you use a **personal access token** (a revocable, per-user key sent as `Authorization: Bearer synq_pat_...`). A token resolves to your user and org and then obeys the exact same permission model a browser session does.

Tokens can be tightened two ways:

- **Read-only**: the token rejects every mutation (`POST`/`PUT`/`PATCH`/`DELETE`).
- **Scoped**: the token carries its *own* grants and is treated as a **restricted** principal, limited to exactly those grants **even if you're an owner**. A scoped token can never do more than it was scoped for.

<Callout title="A scoped token can't exceed its grants" type="warn">
This is deliberate: hand a scoped, read-only token to a script or agent and the worst it can do is read the handful of resources you granted, regardless of how much power your own account has.
</Callout>

## Denied looks like "not found"

When you try to touch something you're not allowed to, Synq returns **404 Not Found**, not 403 Forbidden.

<Callout title="Why 404 instead of 403?" type="info">
A 403 quietly confirms the resource *exists*; you just can't see it. A 404 gives nothing away. This protects against **IDOR** (Insecure Direct Object Reference: guessing ids to probe for resources you shouldn't know about). If a project id isn't yours, the platform behaves as if it simply isn't there.
</Callout>

## Related guides

<Cards>
  <Card title="Authentication" href="/docs/security/auth" description="Sessions, personal access tokens, and how you prove who you are." />
  <Card title="Isolation" href="/docs/security/isolation" description="How one organization's data stays walled off from another's." />
  <Card title="Domains API" href="/docs/api/domains" description="A reference page showing permission tags in action." />
</Cards>
