Nobody can grant what they do not hold

44 permissions, three built-in roles, private products isolated to named members, and a clamp that makes privilege escalation structurally impossible rather than merely discouraged.

How access is decided

Every workspace member holds one role, and a role is a list drawn from 44 named permissions across 11 resource groups. Three roles are seeded when the workspace is created: Owner with all 44, Admin with 42, and Viewer with 11 read-only ones. Below the workspace, a product can be marked private, which limits it to an explicit list of members regardless of their role. API keys carry a separate scope map that is clamped to the permissions of whoever created them, and re-clamped automatically whenever that person's role changes. Permissions are workspace-scoped rather than per-environment: the unit of isolation below the workspace is the product, not the environment.

The model, exactly

Including where the boundary is authorization rather than cryptography, because the difference matters when someone leaves.

44 / 11

Permissions across resource groups

Namespaced as resource:action, covering workspace, members, keys, billing, roles, products, projects, environments, variables, versions and webhooks. The same catalogue ships in the CLI and is readable over the API.
44 / 42 / 11

Owner, Admin, Viewer

Seeded in the same transaction that creates the workspace. Admin holds everything except deleting the workspace and managing billing. System roles are immutable: update and delete both return role.immutable.
25

Custom roles per workspace

Names are 2 to 40 characters and unique case-insensitively. Two permissions can never be granted to a custom role at all, workspace:delete and billing:manage, and the validation refuses them for every caller including Owners.
clamped

You cannot grant above yourself

Creating a role, editing a role, inviting a member or changing someone's role is refused when the result would exceed the permissions you personally hold, and the error names the exact offending permission strings.
404

Returned by a private product you lack

Private-product access is re-checked server-side on every read and write beneath the product, across 21 controllers, and a denied request answers 404 rather than 403, so a restricted product is indistinguishable from one that does not exist.
1 transaction

To remove a member completely

Soft-deletes the membership, hard-revokes every API key they hold in that workspace, revokes their CLI tokens, and drops their private-product grants. The key middleware independently re-checks membership on every request.

Setting a team up

Start from the catalogue, build the role you actually mean, then isolate anything that should not be workspace-wide.

  1. 1

    Read the catalogue first

    All 44 permissions are enumerable from the CLI, the API and the SDK, each with its resource and a flag saying whether a custom role may hold it. Build roles from this list rather than from memory.

    envless role permissions
    envless role permissions --json
  2. 2

    Create a role that means one thing

    A role is just a permission list, so name it after the job. The clamp applies here: you cannot include a permission you do not hold yourself, which means a compromised Admin still cannot mint an Owner.

    envless role create "Deploy bot" \
      --description "Read-only access for CI" \
      --permissions variables:read,versions:read,environments:read,projects:read
  3. 3

    Invite into that role

    Invites are bound to both the email and the role, expire after three days, and can never assign Owner. Acceptance is refused unless the signed-in account's email matches the invited address exactly.

    envless member invite dev@acme.inc --role "Deploy bot"
    envless member list
  4. 4

    Isolate what should not be workspace-wide

    Marking a product private takes it out of the default view entirely and limits it to members you name. This is the finest isolation boundary that exists, and it covers the whole product, project and environment subtree beneath it.

    envless product update billing-platform --private
    envless access grant ann@acme.inc bob@acme.inc --product billing-platform
    envless access list --product billing-platform
  5. 5

    Mint keys narrower than the person holding them

    A key carries a scope map over 11 resources at none, read or write, defaulting to none, plus an optional project allowlist. It cannot exceed its creator's permissions, and demoting that person silently narrows every key they already issued.

    const key = await envless.keys.create({
        name: 'ci-reader',
        scopes: { variables: 'read', environments: 'read', projects: 'read' },
        projectIds: ['<project-uuid>']
    })
    
    console.log(key.key) // shown exactly once, stored only as a SHA-256 hash

Team access control FAQ

Straight answers about how this works in practice.

Not through roles, and this is the most important limitation to understand. Permissions are workspace-scoped, so variables:read applies to every environment in the workspace at once. There is no per-environment ACL. What you can do is put the sensitive work in its own product, mark that product private, and grant it only to the people who should reach it, which isolates the entire project and environment subtree beneath it. For machines, a key can additionally be pinned to a project allowlist.

No, and it is important not to imply otherwise. There is one encryption key per workspace, derived from one shared passphrase. Private products and role checks are server-side authorization: they stop new requests, they do not make previously readable ciphertext unreadable. Anyone who ever held the passphrase can still decrypt anything they kept a copy of. Rotating the workspace passphrase is what actually retires that access.

No. Any role holding products:manage_private bypasses grant checks entirely, and both built-in Owner and Admin hold it. Private products protect against broad workspace-wide visibility, not against your own administrators. If you need a boundary that administrators cannot cross, that is a separate workspace.

A clamp on every path that could widen a permission set: creating a role, editing a role, inviting a member, changing a member's role, and creating an API key. Each is refused when the result would exceed what you personally hold, with the offending permissions named. Two permissions are additionally hard-blocked from custom roles for everyone, including Owners: deleting the workspace and managing billing.

They are hard-revoked in the same transaction as the removal, along with their CLI tokens and their private-product grants. That matters more than it sounds: a key outliving its owner is the classic way access survives an offboarding. The API-key middleware also re-checks workspace membership on every request, so there is a second line of defence.

Exactly five, all in the dashboard: deleting an API key, rotating an API key, deleting a webhook, rotating a webhook secret, and revealing a webhook secret. Envless emails a six-character code, valid ten minutes, capped at five attempts, and one success elevates you for ten minutes. It does not gate reading variables, deleting environments, removing members or changing roles, and it is not enforced on the public API surface at all.

Team members are plan-gated: Free and Starter are single-seat, and Business allows up to 100. Both inviting and accepting count active members plus pending invites against that ceiling, so an invite that would exceed it is refused at send time rather than at acceptance.

Not today, and it is a gap worth knowing about. Member changes, role changes, key changes and CLI-token changes all emit workspace events that reach your webhook endpoint. Granting and revoking product access does not emit an event and has no webhook type. If you need that trail, record it on your side when you make the call.

Get Started

Ship secrets, not chaos.

Start free today and discover why developers trust Envless for end-to-end encrypted, versioned secrets across every environment.