Accounts and team access
A fresh Flaghoist server has one admin token, and whoever holds it can do anything. That is fine while it is just you. Once a team shares it, you lose track of who changed what, and rotating it locks everyone out at once.
With accounts, each person signs in with their own email, gets a role, and has their changes recorded under their name. New projects have them on from the start. Everything on this page is part of the open-source server. There is no separate edition.
Turn on accounts
Section titled “Turn on accounts”npm create flaghoist@latest asks whether you want accounts and roles, and the answer is yes unless
you say otherwise. Accounts need one secret, AUTH_PEPPER: a random value of at least 32
characters that the server mixes into every password hash. Setup generates it for you (or takes
one you paste), prints it once so you can save it in your password manager, and writes it to a
git-ignored .env. It never goes in flaghoist.toml.
Where it goes from there depends on the platform:
- Cloudflare Workers:
npx flaghoist deploysets it as a Worker secret from.env. If the Worker already has one, it is left alone. - A container host: add it to the host’s environment variables, the same place as
ADMIN_TOKEN. Locally,docker run --env-file .envpasses it in.
Back it up and keep it the same for the life of the server. A copy of your database is useless to an attacker without the pepper, but if you lose or change it, every password has to be set again.
On an existing project
Section titled “On an existing project”For a project that uses flaghoist.toml, add this and deploy again:
[accounts]enabled = trueThen give the server a pepper. Generate one with openssl rand -hex 32 and either put it in .env
as AUTH_PEPPER=... before npx flaghoist deploy, or set it yourself with
npx wrangler secret put AUTH_PEPPER (or your container host’s settings).
If you ejected, add users to the server config instead:
createFlagServer((env) => ({ storage, auth: { admin: bearerToken(env.ADMIN_TOKEN), read: apiKey(env.READ_API_KEY), }, users: { pepper: env.AUTH_PEPPER },}))Accounts are kept in the same storage as your flags, through the adapter’s record store. Every
bundled adapter has one. If a custom adapter does not, the server refuses to start with users
set, rather than keep accounts in memory and lose them on the next restart.
Create the first owner
Section titled “Create the first owner”With accounts on and none created yet, the sign-in screen asks for the admin token. Sign in with it, open Account, and create your account. It gets the owner role. From then on, sign in with your email.
Keep the admin token. It still works as an owner credential, for recovery when nobody can sign in,
and the audit log records its changes as admin token so they are easy to spot.
There are four roles. Each one can do everything the role before it can.
| Role | Can |
|---|---|
viewer |
See flags, environments, exports and the audit log |
editor |
Also create, edit, toggle, archive and restore flags |
admin |
Also delete flags, import, manage webhooks, manage members, and see the security log |
owner |
Everything, including managing other owners |
The server checks the role on every request. The dashboard follows it too, so a viewer sees flags with the switches turned off, and Members and Webhooks only appear for admins and owners.
A different role in some environments
Section titled “A different role in some environments”If you use environments, a member can have a different role in some of them: a viewer who can edit in staging, or an editor who is read-only in production. Set it on the Members page, or from the CLI:
npx flaghoist users role ada@example.com editor --env stagingAn environment role applies to that environment’s flags, exports, imports and history. Managing members, webhooks and the security log always uses the main role. Owners have full access everywhere, so they take no environment roles.
Invite people
Section titled “Invite people”Admins and owners invite from the Members page: enter an email and a role, and you get a link
to send. The link works once, only for that email, and expires after 7 days
(users.invites.expiresInDays changes that). The person opens it, picks a password, and lands in
the dashboard signed in.
From the terminal:
npx flaghoist users invite ada@example.com --role editorIf an invite gets lost, create a new link for it; the old one stops working. You can also cancel an open invite.
Email invites instead of copying links
Section titled “Email invites instead of copying links”Flaghoist sends no email unless you give it a way to. Pass users.email an object with a send
method, and invites and password reset links are emailed as well as shown. There is no bundled
email provider; anything with an HTTP API works. With Resend:
users: { pepper: env.AUTH_PEPPER, email: { async send({ to, subject, text, html }) { const res = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { authorization: `Bearer ${env.RESEND_API_KEY}`, 'content-type': 'application/json', }, body: JSON.stringify({ from: 'Flaghoist <flags@acme.com>', to, subject, text, html }), }) if (!res.ok) throw new Error(`Resend answered ${res.status}`) }, },},Postmark works the same way: post to https://api.postmarkapp.com/email with an
X-Postmark-Server-Token header and a body of { From, To, Subject, TextBody, HtmlBody }. If
sending fails, the error is logged and the link is still shown to the admin, so nobody is stuck.
Manage members
Section titled “Manage members”From the Members page you can change someone’s role, disable them, or remove them.
- Disabling someone signs them out straight away and stops their access tokens. Enable them again to let them back in.
- Demoting someone also signs them out, so they come back with the new role.
- Only an owner can make someone an owner, or change, disable or remove an owner.
- The last active owner cannot be demoted, disabled or removed, and nobody can change their own role or remove themselves.
The same actions are in the CLI: flaghoist users list, role, disable, enable, remove. See
the CLI reference.
Forgotten passwords
Section titled “Forgotten passwords”An admin creates a reset link for the member on the Members page, or with
flaghoist users reset <email>. It works once, for 24 hours, and setting a new password signs out
every session that member had. An admin cannot reset an owner’s password; another owner can, or
the admin token.
Sign in with your identity provider
Section titled “Sign in with your identity provider”Turn on SSO and the sign-in screen offers Continue with your provider: Okta, Microsoft Entra, Google Workspace, Auth0, Keycloak, or anything that speaks OpenID Connect.
Setup asks whether you want it. Say yes and it asks for the issuer URL, the client ID, the group
whose members become admins, and the client secret. The secret goes in .env as
SSO_CLIENT_SECRET, and the rest lands in flaghoist.toml:
[accounts.sso]issuer = "https://acme.okta.com"clientId = "0oa1example"adminGroup = "flag-admins"Everyone else who signs in through the provider starts as a viewer. For more than that, such as
several group mappings, a button label or allowed email domains, eject and set users.sso in code:
users: { pepper: env.AUTH_PEPPER, sso: { issuer: 'https://acme.okta.com', clientId: env.OIDC_CLIENT_ID, clientSecret: env.OIDC_CLIENT_SECRET, label: 'Okta', allowedDomains: ['acme.com'], roleMapping: { 'flag-admins': 'admin', engineering: 'editor' }, },},Register https://<your server>/api/v1/auth/sso/callback as the redirect URL in your provider.
OIDC provider setup has the steps for each one.
How it behaves:
- New people get an account on their first sign-in, as long as their email domain is in
allowedDomainsand the provider says the email is verified. Someone who already has a password account is linked to it by email. Someone with an open invite joins with the invited role. - Roles come from the provider’s groups when you set
roleMapping. They are applied at every sign-in, the highest mapped role wins, and they cannot be changed on the Members page. Someone in no mapped group is refused, unless you setdefaultRole. WithoutroleMapping, new people getdefaultRoleand you manage roles in Flaghoist. - SSO only: set
passwordSignIn: falseto turn passwords off for everyone. Invited people then join by signing in with SSO. The admin token still works, in case the provider is down.
The server runs the sign-in itself (the authorization code flow with PKCE), so a client secret never reaches the browser, and it checks the ID token’s signature, issuer, audience, expiry and nonce. No cookies are used, and the session token never appears in a URL.
Two-factor codes
Section titled “Two-factor codes”Anyone who signs in with a password can add a code from an authenticator app (1Password, Google
Authenticator, Authy and others). On the Account page, scan the QR code, enter one code to
confirm, and save the ten recovery codes it shows. From then on, signing in asks for a code after
the password, in the dashboard and in flaghoist login.
Each code works once, and wrong codes count toward the sign-in lockout. A recovery code gets you in once if you lose your phone.
To make it required, set users.twoFactor to 'admins' (admins and owners) or 'everyone'. Anyone
who has to use it but has not set it up is asked to straight after signing in, and can do nothing
else until they have. People who sign in with SSO are exempt, because the identity provider’s own
two-factor already covers them.
If someone loses their phone and their recovery codes, an admin can turn two-factor off for them on the Members page. That signs them out everywhere; they sign in with their password and set it up again.
Access tokens for the CLI, CI and scripts
Section titled “Access tokens for the CLI, CI and scripts”Each member can create personal access tokens on the Account page. A token acts as the person who made it, so changes made with it are recorded under their name.
- Give a token a lower role than your own to limit what it can do. It can never have a higher one.
- Tokens expire after 90 days by default. You can pick up to ten years, or no expiry if you really mean it.
- The token is shown once. After that the list shows its first few characters and when it was last used.
- If the owner is demoted, their tokens are narrowed to match. If they are disabled or removed, their tokens stop working.
Use a token anywhere the admin token went: Authorization: Bearer fh_pat_..., FLAGS_ADMIN_TOKEN
for the CLI, or FLAGHOIST_ADMIN_TOKEN for the MCP server.
flaghoist login creates one for you from your email and password, and saves it so later commands
need no token:
npx flaghoist login --url https://flags.example.com --email ada@example.comSessions and passwords
Section titled “Sessions and passwords”- Passwords need at least 12 characters, with no other rules.
- The password never leaves the browser. The dashboard stretches it with PBKDF2 (600,000 rounds)
and sends only the result, and the server stores a keyed hash of that. The server’s work per
sign-in is well under a millisecond, which keeps it inside the CPU limit of the Cloudflare
Workers free plan. Because of the hashing, password sign-in needs HTTPS (or
localhost). - A session ends after 30 minutes without a request, or 12 hours after sign-in, whichever comes
first.
users.session.idleMinutesandusers.session.maxHourschange those. - The Account page lists your sessions and signs out any of them. Changing your password signs out the others.
- After five wrong passwords for one email in 15 minutes, that email is locked for 30 seconds, and the lock doubles with each further failure up to 15 minutes. Thirty failures from one IP address lock it for 15 minutes. Unknown emails get exactly the same answers and lockouts, so neither reveals which accounts exist.
- On Cloudflare KV, a sign-out or a disabled account can take up to about a minute to reach every location, because KV itself is eventually consistent. Redis, Postgres and SQLite apply it at once.
The security log
Section titled “The security log”Sign-ins and failed sign-ins, password changes and resets, invites, member changes, access tokens, two-factor changes and webhook changes go to a separate security log. Admins and owners see it on the Audit log page, under Security. Flag changes stay in the main audit log, now recorded against each person’s email.
Webhooks can also be told about team changes (someone invited, joined, changed role, disabled or removed). These events are opt-in; see the API reference.