Workspace

Onboarding a teammate

Add a second person to a workspace end to end: the invite and its role, the 3 day expiry, acceptance rules, and the passphrase handoff Envless deliberately leaves to you.

Adding someone to an Envless workspace has two halves, and only one of them is automated. The first half is membership: an invite is created, an email goes out, the invited address accepts, a member row appears with a role. The server owns all of that. The second half is decryption, which the server cannot own, because variable values are encrypted in the browser with a key derived from the workspace passphrase and that passphrase never reaches Envless. A brand new member therefore lands in a workspace where they can read every variable name and not a single value, and they stay there until an existing member hands them the passphrase out of band.

The envless command ships in @goenvless/cli. Install it with npm i -g @goenvless/cli@latest.

Before you invite

RequirementDetail
The members:invite permissionOwner and Admin hold it by default. Viewer does not. A custom role holds it only if it was granted, see Role. Without it the invite is refused with The user is not authorized to invite members to this workspace.
A plan with more than one seatThe member cap is 1 on Free, 1 on Dev, 15 on Team and unlimited on Enterprise. On a plan whose cap is 1 the invite is refused with Your current plan does not include team members. Upgrade your plan to invite members. A second person is a Team plan feature, not a setting.

The cap counts active members and invites that have not expired, so three outstanding invites on a 10 seat Team workspace with 7 members fills it. An invite past its 3 days stops counting, even though the team table still shows it as pending. Past the cap the message is You have reached the maximum of 10 members for your plan. The check runs again at acceptance time, where it counts active members only and answers 403 with This workspace has reached its plan member limit. A workspace that downgrades between sending an invite and the invite being opened will refuse the acceptance.

Send the invite

Open the team page

Go to Team and press Invite member. The button only renders for a member who holds members:invite.

Enter the address and pick a role

One address per invite, plus a role from the picker. The picker defaults to Viewer when the workspace has one.

Send it

The invite row appears on the team table as pending, and the email goes out immediately. If the email fails to send, the invite is rolled back rather than left dangling, and the response is We could not send the invitation email. Please try again.

The role, and why Owner is never in the list

Every invite carries a role, and the role is fixed at invite time. A pending invite cannot be moved to another role: revoke it and send a new one.

Owner can never be assigned by invite. The dashboard picker filters the Owner role out of the list entirely, and the API refuses it anyway with The owner role cannot be assigned via invite. There is no flag and no plan that changes this. Ownership moves by other means, never by inviting someone as Owner.

Two more limits apply to the role you choose. A role that grants workspace:delete or billing:manage is not assignable through an invite at all, and a role that grants any permission you do not personally hold is refused with You cannot invite a member with a role that grants permissions you do not hold yourself: followed by the offending permissions. An Admin can therefore mint an Admin, but cannot mint something stronger than themselves.

The email contains a button pointing at the dashboard root with the invite token in the query string, as ?workspace-invite=<token>. That link is the whole credential, so treat it like one.

PropertyBehavior
Expiry3 days from the moment it was created, recorded on the row as expiresAt. The email says so in as many words.
Single useAccepting soft-deletes the invite row inside the same transaction that creates the membership, so the same link cannot be used twice. A second attempt answers 404 with Invitation not found or expired.
ResendResending reuses the same token and pushes expiresAt to 3 days from now, so an already expired link starts working again and the old email in the recipient's inbox is still valid.
RevokeRevoking soft-deletes the row, which retires the token immediately.
DuplicatesA second invite to an address that already holds a live one is refused with An invite for this email is already pending. Resend instead. Once the first invite is past its 3 days that refusal stops firing, and a second invite creates a separate row with its own token, so the same address can sit on the table twice.
Self inviteInviting your own address is refused with You cannot invite yourself to the workspace.

The dashboard team table has no expired state. A pending invite keeps its pending badge after its 3 days are up, and the only columns that hint at age are Joined / Invited and Last activity (which shows the last resend, or Never resent). The public API is the surface that computes it: list-invites returns status: "expired" once expiresAt has passed. If someone says the link does not work, resend before you debug anything else.

Accepting the invite

Acceptance is checked against the signed-in account, not against whoever happens to be holding the link.

Open the link

The app reads the token out of the query string (or the URL fragment), stores it, and strips it back out of the address bar.

Sign in as the invited address

The invite carries no account with it, so a new teammate signs in with the ordinary email code flow first. The sign-in link keeps the invite token attached through /auth and /otp, so the invite survives the round trip and there is no need to open the email a second time.

Accept in the modal

Once the invite resolves for the signed-in user, an Accept Workspace Invitation modal opens showing the workspace icon, its name, and a member count. The count already includes the person accepting, so a workspace with one owner reads 2 Members.

Land in the workspace

Accepting creates the member row, consumes the invite, makes that workspace the selected one for the account, and redirects to the products page. A member.accepted webhook event is emitted, and any API keys the user already held in that workspace have their scopes re-clamped to the new role.

The signed-in email must match the invited address exactly, compared without case and nothing else. No plus addressing equivalence, no alias resolution, no domain matching. A mismatch answers 403 with This invitation was sent to ana@acme.com, but you are signed in as ana@personal.com. Sign out, sign back in with ana@acme.com, then open the invitation link again. The same check runs before the modal is even allowed to open, so a mismatched session sees an Invite details not loaded error and no modal at all.

Refusal at acceptanceCodeMessage
Signed in as someone else403The mismatch message above, naming both addresses
Past its 3 days400Invitation has expired.
Link already used, or revoked404Invitation not found or expired.
Already in the workspace400You are already a member of this workspace.
Role deleted since the invite went out400The role attached to this invitation no longer exists. Ask an admin to send a new invite.
Workspace is full for its plan403This workspace has reached its plan member limit.

On any 400, 403 or 404 the modal closes and the stored token is dropped, because none of those are fixed by pressing Accept again. Fixing the cause means opening the link a second time, after signing in as the right address or having the invite resent (a resend keeps the same token, so the original email still works).

What the new member sees before they have the passphrase

This is the part that surprises people. Membership grants server-side access, and server-side access is not read access to values. A Viewer or Developer who has just accepted can immediately see:

  • Every product, project and environment their role and product access allow.
  • Every variable name, in the clear, along with its type, visibility, required flag and timestamps. Names are metadata, not ciphertext.
  • Version history and its entry counts, the change history behind a single variable, members, roles, keys and webhooks, as their role permits.

What they cannot see is any value. Until the workspace is unlocked on their device:

SurfaceBehavior while locked
Variables tableEvery value renders as ••••••••••••
The reveal (eye) toggleRaises a Key not unlocked toast reading Unlock your workspace key first to continue., with an Unlock action that navigates to the encryption settings
SearchValues are locked, so only names are being searched.
Environment compareUnlock this workspace to compare values. Key names are shown below.
envless syncRefuses with <workspace> is locked. Run envless passphrase set, or set ENVLESS_KEY. until one of those two has happened, see Passphrase

Nothing about this state is broken and nothing is pending on the server. It is the design: the ciphertext is on the new member's screen already, and the key to it is not.

Handing over the passphrase is a human problem

Envless does not solve passphrase distribution for you, and does not pretend to. There is one workspace passphrase, every member derives the same key from it, and there is no per-member key envelope anywhere in the system for the server to hand out. No invite, no role and no permission carries the passphrase or any part of it. If you do nothing, the new member stays locked out of values forever.

Put the workspace passphrase in a shared password manager entry (a 1Password shared vault, a Bitwarden collection, or the equivalent) that the new member already has access to through your identity provider, and let them read it from there. That keeps it out of any channel with a searchable history and gives you one place to revoke.

Do not send it in chat, email, a ticket, a pull request comment or a screen share. If it has already gone out over something you cannot retract, treat it as leaked: rotate the passphrase from the dashboard, then have everyone unlock again in their browser and run envless passphrase set again on every machine. Rotation re-encrypts the workspace variables and version history under the new key, so it is a real remedy and not a cosmetic one.

The raw key export on CI / Machine Access is not a shortcut around this. Producing an ENVLESS_KEY requires typing the workspace passphrase in the first place, and the result decrypts the entire workspace with no key derivation step in front of it. It is a credential for machines, covered in Passphrase, not a way to onboard a person.

Because the passphrase is shared rather than per member, removing someone from the workspace revokes their server access and nothing else. They still know the passphrase, and if any environment is served as a virtual env feed the ciphertext is still reachable without any account at all. Offboarding a person who held the passphrase means rotating it.

The exact order of operations

Accept the invite

Open the invite link while signed in as the invited address, and press Accept in the modal. The workspace becomes the selected one right away.

Unlock at /settings/encryption

Go to Encryption. While the workspace is locked the page shows an Unlock workspace panel with a single passphrase field. Enter the passphrase you were given. The key is derived with PBKDF2 (200,000 iterations, SHA-256) into an AES-GCM-256 key, verified by decrypting one existing variable from the workspace, and then stored non-extractable in that browser's IndexedDB. A wrong passphrase fails with That passphrase does not match the encrypted variables in this workspace. and nothing is saved. Unlocking is per device and per browser profile, so it is repeated on every machine that member uses.

envless login

Only needed if they will use the CLI. Runs the device-code browser flow and writes the session to ~/.envless/config.json. See Login.

envless link

In the project directory, binds it to a workspace, product, project and environments, writing a .envless file that holds slugs and no secrets. See Link.

envless passphrase set

Prompts for the same workspace passphrase and caches the derived key for that workspace on that machine. It verifies by decrypting one existing variable, so a wrong passphrase caches nothing. Run once per workspace per machine, then envless sync works silently. See Passphrase.

Steps 3 to 5 are independent of steps 1 and 2 in one direction only: the CLI needs the membership from step 1, but it does not need the browser unlock from step 2. Someone who only ever uses the CLI can skip step 2 entirely, and someone who only ever uses the dashboard can stop after step 2.

Unlocking verifies against real ciphertext, and a workspace with no variables the new member can read has none to verify against. In that case the passphrase is only checked against the strength floor and is then cached unverified. On an empty workspace a typo is therefore accepted silently, and anything written under that wrong key is unreadable to everyone else. Add and read back one variable before you trust a first unlock. The same caveat applies to envless passphrase set.

Private products need a separate grant

Membership is workspace wide, but a product marked private is not. A member who does not hold products:manage_private, which Owner and Admin do hold by default, sees only the public products plus the private ones they have been explicitly granted. So a teammate who accepted, unlocked, and still sees nothing is usually missing a product grant rather than a passphrase. Grant it with envless access.

When onboarding goes wrong

SymptomCauseFix
Invite link opens the dashboard with no modalSigned in as an address other than the invited one, or the invite expired or was revokedSign out, sign back in as the invited address, and reopen the link. Resend if it is past 3 days.
Invitation has expired.More than 3 days since it was sent or last resentResend the invite. The token and the old email stay valid.
Invitation not found or expired.The link was already used, or the invite was revokedSend a new invite.
The owner role cannot be assigned via invite.An Owner role was passed by ID or name, usually from the CLIPick any other role. Owner is never assignable this way.
Your current plan does not include team members.Free or Dev, both capped at 1 seatUpgrade to Team (10 seats) or Enterprise (unlimited).
Accepted, but every value shows as dotsExpected. The device has no workspace key yetUnlock at Encryption with the passphrase.
That passphrase does not match the encrypted variables in this workspace.Wrong passphrase, or a passphrase from a different workspaceGet the right one from the shared vault. Nothing was cached.
Could not check that passphrase against this workspace. Nothing was saved, please try again.The verification lookup could not reach the APIRetry. This is a network failure, not a wrong passphrase.
Workspace key not saved on this deviceThe browser blocked site data, so the key is held in memory onlyAllow site data for the dashboard, otherwise the unlock is lost on reload.
Signed in and unlocked, but a product is missingThe product is private and they have no grantGrant access with envless access.