Account
Security & access
Sign in without passwords, protect accounts with two-factor authentication, require it for your whole team, and see who did what in the audit log.
Signing in
There are no passwords. You sign in at /login with one of:
- Email: we send a 6-digit code and a magic link. Either one works.
- Google or GitHub: OAuth 2.0 with PKCE. Only verified email addresses are accepted. A provider identity links to the account with the same verified email, so you can mix methods freely. An identity that already belongs to a different account can't be linked again.
Sessions last 30 days. Connected providers can be disconnected under /app/settings/security (DELETE /me/identities/:provider). Your account keeps working with email sign-in.
Two-factor authentication
UpButler supports authenticator apps (TOTP: RFC 6238, SHA-1, 30-second steps, 6 digits), which covers 1Password, Google Authenticator, Authy, Bitwarden and others. Set it up under /app/settings/security:
- Scan the QR code, or enter the secret, in your authenticator app.
- Confirm with a current code within 30 minutes.
- Save the 10 one-time recovery codes. They're shown once.
From then on, every sign-in method (email code, magic link, Google, GitHub) asks for a code at /login/2fa. Each code works only once, so a code that was intercepted can't be replayed. A recovery code works in place of a code once and is then used up. You can issue a fresh set of recovery codes at any time (the old ones stop working), and turning 2FA off needs a current code.
- A pending two-factor sign-in expires after 10 minutes or 5 wrong codes.
- Code attempts are rate-limited (10 per 15 minutes per account, 30 per 15 minutes per IP).
- Authenticator secrets are stored encrypted. Recovery codes are stored only as hashes.
Requiring 2FA for a workspace
The workspace owner can require two-factor for every member in /app/settings/security (PATCH /workspace/security with {"require2fa": true}, dashboard session only). The owner must have 2FA on first. The response reports how many members don't have it yet.
- Members without 2FA are sent to the security settings when they open the dashboard. Their session can't act on the workspace until they've enrolled.
- Members of a workspace that requires 2FA can't turn it off. An owner has to lift the requirement, or they leave the workspace.
- The members list shows who has 2FA.
- API keys are not affected. They're separate credentials, governed by their scopes. See below.
Sessions
/app/settings/security lists your active sessions with device (“Chrome on macOS”), IP, sign-in method, and when each was created and last seen. Sign out a single session, or every session except the current one. These are dashboard-only operations (GET /me/security, DELETE /me/sessions/:id, POST /me/sessions/revoke-others).
API keys
- Keys (
ub_live_…) belong to a workspace and carry thereadand/orwritescope. Only a hash is stored. The secret is shown once. - Give each agent or integration its own key with an
agentname, so its actions are attributed in incident timelines and the audit log. SetexpiresInDaysfor temporary access. - Revoke a key with
DELETE /keys/:id. It stops working immediately. - Cookie-authenticated (dashboard) writes must come from the same origin. API keys are meant for servers and agents, so never ship a write-scoped key to a browser.
Audit log
Security- and admin-relevant actions are recorded with who, what, target, IP and time, and kept for 400 days. A workspace's log contains its own actions plus the account-level events (sign-ins, 2FA changes) of its current members. Owners and admins see it at /app/settings/audit. Over the API, it needs a key with the write scope.
Events are recorded on every plan. The viewer and GET /audit show the full 400 days on the Business plan. On other plans they show the last 7 days, and the response carries limitedToDays: 7 and a hint. Older entries are kept and come into view after an upgrade.
curl "https://upbutler.com/api/v1/audit?action=auth&limit=2" \
-H "Authorization: Bearer $UPBUTLER_API_KEY"{
"data": [
{
"id": "aud_0n3qd2k8m1p4r7t0w3y",
"action": "auth.sign_in",
"actor": { "type": "user", "id": "usr_0n3q8jy7w2e4r6t8y0u", "name": "Dana Weber" },
"target": null,
"meta": { "method": "github", "twoFactor": "totp" },
"ip": "203.0.113.24",
"at": "2026-10-09T07:58:12.004Z",
"scope": "account"
},
{
"id": "aud_0n3qd1x2c5v8b1n4m7q",
"action": "auth.2fa_failed",
"actor": { "type": "user", "id": "usr_0n3q8k01d5f7h9j1k3l", "name": "Sam Lee" },
"target": null,
"meta": { "method": "email_code" },
"ip": "198.51.100.7",
"at": "2026-10-09T07:41:50.873Z",
"scope": "account"
}
],
"next": "aud_0n3qd1x2c5v8b1n4m7q"
}Filters: action (an exact action such as api_key.created, or a prefix such as auth or member.), actorId, from/to, and limit (max 200). Newest first. Page with before=<next>. scope tells workspace actions from account events.
| Action | Recorded when |
|---|---|
auth.sign_in | Successful sign-in (meta: method, and twoFactor = totp | recovery when used) |
auth.sign_out | Signed out |
auth.2fa_failed | Wrong two-factor code at sign-in |
auth.session_revoked / auth.sessions_revoked | One session, or all other sessions, signed out |
user.2fa_enabled / user.2fa_disabled | Two-factor turned on or off |
user.recovery_codes_regenerated | New recovery codes issued |
user.identity_linked / user.identity_unlinked | Google or GitHub sign-in connected or disconnected |
workspace.2fa_required / workspace.2fa_optional | Owner changed the two-factor requirement |
workspace.renamed | Workspace renamed |
member.invited / member.invite_revoked / member.removed | Membership changes |
api_key.created / api_key.revoked | API key lifecycle |
billing.checkout_started / billing.portal_opened | Billing actions |
monitor.deleted / page.deleted | Deletions |
page.domain_changed | A status page custom domain was set, changed or removed |