--- title: Permissions & roles description: How Openship decides who can do what — 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 Openship 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. Openship 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. ## 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, Openship 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. | 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. 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. ## 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 — 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 Openship 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 (owner-level territory). | | **Audit log** | The record of who did what. | | **GitHub orgs** | Every repo under one GitHub installation. | | **GitHub repos** | A single repository. | The picker adapts to your instance. A **self-hosted** instance shows Servers and Mail servers; **Openship Cloud** shows Billing in their place. Projects, Backup destinations, the Audit log, and GitHub are offered on both. ## Inviting people and setting roles Team management lives under **Settings → Team**. ### Open the invite dialog Go to **Settings → Team** and press **Invite member**. Enter the person's email address. Settings → Team tab, member list with the **Invite member** button. *(screenshot pending)* ### 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. The Invite member dialog with the three role cards (Member, Admin, Restricted) and, for Restricted, the resource picker pane on the right. *(screenshot pending)* ### Send it Openship 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. ## 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 opsh_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. 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. ## Denied looks like "not found" When you try to touch something you're not allowed to, Openship returns **404 Not Found**, not 403 Forbidden. 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. ## What next?