Skip to content

This document was written by AI and has been manually reviewed.

Configuration

Site configuration is stored in the site_config D1 table and editable at runtime through Admin → Settings. No redeployment is needed to change any of these values.

Sensitive keys (captcha secret, social client secrets, SMTP/IMAP passwords, the GitHub README PAT) are encrypted at rest with AES-GCM via the SECRETS_KEY Cloudflare Secrets Store binding. The admin panel transparently decrypts on read; values are never exposed via the config API.

General

KeyTypeDefaultDescription
site_namestring"Prism"Displayed in the browser title and emails
site_descriptionstring"Federated identity platform"Shown on the login page
site_icon_urlstring?nullURL to a favicon / logo
allow_registrationbooleantrueAllow new users to self-register
invite_onlybooleanfalseRequire an invite token to register, even when allow_registration = true
require_email_verificationbooleanfalseBlock login until email is verified
accent_colorstring"#0078d4"Primary brand color (hex). Drives FluentUI theme
custom_cssstring""Injected as a <style> block on every page
disable_user_create_teambooleanfalseHide the "New team" button — only admins can create teams
disable_user_create_appbooleanfalseHide the "New application" button — only admins can create OAuth apps
allow_alt_email_loginbooleantrueLet users sign in with any verified secondary email, not just primary
initializedbooleanfalseSet to true after first-run setup. Do not change manually

Sessions & tokens

KeyTypeDefaultDescription
session_ttl_daysnumber30Session JWT lifetime. Per-user override via users.access_token_ttl_minutes / refresh_token_ttl_days (admin-only)
access_token_ttl_minutesnumber60OAuth access token lifetime (default; per-user override available)
refresh_token_ttl_daysnumber30OAuth refresh token lifetime (default; per-user override available)

Bot protection (captcha)

Exactly one provider can be active at a time. The captcha is challenged on register, login, password change, email-verification resend, and any flow the admin enables.

KeyTypeDefaultDescription
captcha_providerstring"none"none | turnstile | hcaptcha | recaptcha | pow
captcha_site_keystring""Public site key for the chosen provider
captcha_secret_keystring""Server-side secret for the chosen provider (encrypted at rest)
pow_difficultynumber20Leading zero bits required for proof-of-work (higher = harder)

Proof-of-work requires no third-party service. The Rust→WASM solver in pow/ runs ~10× faster than the JS fallback. Difficulty 20 takes ~0.1–2 s depending on device. Values above 24 may time out on low-end mobile devices. PoW is single-use and replay-protected via the pow_used table.

Two-factor / step-up

KeyTypeDefaultDescription
sudo_mode_ttl_minutesnumber5After a successful step-up, subsequent challenges from the same (user, session, app) skip the TOTP/passkey prompt for this many minutes. 0 disables sudo mode entirely
require_captcha_for_2fabooleanfalseSite-wide: every step-up confirmation must solve the active captcha. Apps can also opt in per challenge. No-op when captcha_provider = none

Public profiles

User and team public profiles are off until the user (or team owner) explicitly opts in. Site defaults apply only to fields the user has not customized — they never silently flip a private profile to public.

User profile defaults

KeyTypeDefaultDescription
enable_public_profilesbooleantrueMaster kill switch. false ⇒ both /u/:username and /t/:id always 404
default_profile_show_display_namebooleantrue
default_profile_show_avatarbooleantrue
default_profile_show_emailbooleanfalseSensitive — opt-in even when the rest of the profile is public
default_profile_show_joined_atbooleantrue
default_profile_show_gpg_keysbooleantrue
default_profile_show_authorized_appsbooleanfalseReveals the user's connected services — opt-in
default_profile_show_owned_appsbooleantrue
default_profile_show_domainsbooleantrue
default_profile_show_joined_teamsbooleanfalseAlso gates appearing in any team's public member list
default_profile_show_readmebooleantrueREADME is itself opt-in (empty = hidden); this only matters if the user has written one
profile_readme_max_bytesnumber65536Hard cap on README markdown source size

Team profile defaults

KeyTypeDefaultDescription
default_team_profile_show_descriptionbooleantrue
default_team_profile_show_avatarbooleantrue
default_team_profile_show_ownerbooleanfalseOpt-in: would otherwise reveal the owner's username
default_team_profile_show_member_countbooleantrue
default_team_profile_show_appsbooleantrue
default_team_profile_show_domainsbooleantrue
default_team_profile_show_membersbooleanfalseThe full member list. Each member's own profile_show_joined_teams still applies
default_team_profile_show_sub_teamsbooleantrueSub-team listing. Each child must also be public to actually appear

There is no site default for the master profile_is_public flag — privacy-first. The team owner (or admin) must always set it explicitly.

Sub-teams (nested teams)

Master switch and inheritance toggles for the sub-team feature. See that page for the full semantics; the keys themselves:

KeyTypeDefaultDescription
enable_sub_teamsbooleantrueMaster switch. When false every sub-team endpoint returns 403.
max_team_depthinteger5Hard cap on nesting depth (root = 0). Admin API validates 1–20.
inherit_team_membershipbooleantrueCascade member roles to descendants (effective role = max(direct, inherited)).
inherit_team_domainsbooleantrueSurface ancestor-owned domains on sub-team listings + use them for auto-verify.

Team join requirements (site floor)

Site-wide minimums every team is forced to require, regardless of the team-level flag. Owners can opt their team in further but cannot opt out below the floor.

KeyTypeDefaultDescription
default_team_require_2fabooleanfalseFloor: every team requires at least one TOTP authenticator or passkey
default_team_require_verified_emailbooleanfalseFloor: every team requires a verified primary email

WARNING

Turning these on retroactively forces every existing member to satisfy the factor — anyone who hasn't enrolled is locked out of team operations until they do. Roll them out behind a member-side notice.

GitHub README sync

Users can opt to sync their public profile README from a GitHub user repo. Cache respects ETag and serves stale-on-error.

KeyTypeDefaultDescription
github_readme_tokenstring""Site-global GitHub PAT used as the last-resort token for README fetches. Empty = unauthenticated 60 req/h per IP. Encrypted at rest
github_readme_cache_ttl_secondsnumber3600Serve cached README for this long before issuing a conditional GET
github_readme_token_failuresnumber0Auto-managed: site PAT 401 counter. Auto-clears the token at 3 failures

GPG login

KeyTypeDefaultDescription
gpg_challenge_prefixstring""Extra lines inserted between the site header and the random challenge in the clearsign payload. Use this to add a human-readable marker so users can verify the challenge they're signing comes from your site

Telegram notifications

KeyTypeDefaultDescription
tg_notify_source_slugstring""Slug of an enabled Telegram OAuth source whose bot is used to deliver Telegram notifications. Leave empty to disable Telegram delivery. The source's bot token doubles as the bot used to message users

Social login

Each OAuth source (GitHub, Google, Microsoft, Discord, Telegram, X, Generic OIDC, Generic OAuth 2) is now a row in the oauth_sources table — managed in Admin → OAuth Sources, not here. The legacy keys below remain readable for backwards compatibility but new deployments should use OAuth Sources directly.

Key (legacy)Description
github_client_idGitHub OAuth App Client ID
github_client_secretGitHub OAuth App Client Secret
google_client_idGoogle Cloud OAuth 2.0 Client ID
google_client_secretGoogle Cloud OAuth 2.0 Client Secret
microsoft_client_idAzure AD Application (client) ID
microsoft_client_secretAzure AD Client Secret
discord_client_idDiscord Application ID
discord_client_secretDiscord Client Secret

All *_client_secret values are encrypted at rest. Callback URL format for sources is:

https://your-domain/api/connections/<slug>/callback

Email — Sending

KeyTypeDefaultDescription
email_providerstring"none"none | resend | mailchannels | smtp
email_api_keystring""API key for Resend or Mailchannels (encrypted)
email_fromstring"noreply@example.com"From address for outgoing emails
smtp_hoststring""SMTP server hostname (when provider is smtp)
smtp_portnumber587SMTP server port
smtp_securebooleanfalseUse SSL/TLS (true) or STARTTLS (false)
smtp_userstring""SMTP username
smtp_passwordstring""SMTP password (encrypted)

Email — Receiving

KeyTypeDefaultDescription
email_verify_methodsstring"both"link (system sends email) | send (user sends email to verify) | both
email_receive_providerstring"cloudflare"cloudflare (Email Workers) | imap (poll via IMAP) | none
email_receive_hoststring""Domain for verify-<code>@<host> emails (Cloudflare only). Blank = derive from APP_URL
imap_hoststring""IMAP server hostname (when receive provider is imap)
imap_portnumber993IMAP server port
imap_securebooleantrueUse implicit TLS (true, port 993) or STARTTLS (false, port 143)
imap_userstring""IMAP username — also shown to users as the destination address (with code as subject)
imap_passwordstring""IMAP password (encrypted)
social_verify_ttl_daysnumber0When non-zero, an email verified through a social provider stays trusted for this many days before re-verification is requested. 0 disables expiry

Domain verification

Domains can be verified via DNS TXT, an HTML meta tag, or a .well-known file — whichever the user picks at add time. Verified domains are re-checked on the configured cron interval.

KeyTypeDefaultDescription
domain_reverify_daysnumber30Days between automatic re-verification checks for domains

Diagnostics & rate limiting

KeyTypeDefaultDescription
login_error_retention_daysnumber30How long failed-login rows in the login_errors table are kept before the cron purges them
ipv6_rate_limit_prefixnumber64Prefix length used to bucket IPv6 addresses in the rate limiter (so a /64 doesn't get unlimited retries)

Wrangler bindings & variables

These are configured in wrangler.jsonc and not editable from the admin panel.

Variables

VariableRequiredDescription
APP_URLYesFull origin of the deployment, e.g. https://auth.example.com

Bindings

BindingKindRequiredNotes
DBD1 databaseYesAll persistent state
KV_SESSIONSKV namespaceYesSession JWT secret, RSA keypair (for ID token signing), per-session metadata
KV_CACHEKV namespaceYesRate-limit counters, IMAP poll cursors, image-proxy cache
ASSETSWorkers AssetsYesBuilt SPA. html_handling: "none" so SSR can render / itself
SECRETS_KEYSecrets Store secretStrongly recommended32-byte base64url AES-GCM master key. When bound, all sensitive D1 fields encrypt at rest

SECRETS_KEY setup

Generate a 32-byte master key:

bash
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"

Create the Secrets Store, store the key under name prism-secrets-key, and add the secrets_store_secrets binding shown in wrangler.jsonc.

After redeploying, run the migration once from Admin → Settings → Danger Zone → "Migrate secrets to Secrets Store" to encrypt existing OAuth/source/SMTP/IMAP/captcha credentials in D1. Bearer-style secrets (PATs, OAuth codes, OAuth tokens, invite tokens, email-verify codes, 2FA codes, individual backup codes) are migrated to a keyed HMAC-SHA256 hash in a companion endpoint ("Migrate D1 secrets") so they remain look-up-able by value but are not recoverable from the database.

If SECRETS_KEY is not bound, encryption/hashing is a no-op — the legacy plaintext path keeps working until you opt in.

Cron triggers

jsonc
"triggers": { "crons": ["0 */6 * * *"] }

Every 6 hours the worker:

  • re-verifies domains whose next_reverify_at has passed,
  • polls the IMAP mailbox (when email_receive_provider = imap),
  • purges the app_event_queue and expired pow_used rows,
  • sweeps orphaned image_proxy_mappings (mappings whose source row no longer exists).

Released under the GPL-3.0 License.