MuseMVP Docs
Admin

Runtime Settings

Store payment, storage, newsletter, and Turnstile settings in the database. Admins edit them at /app/admin/settings.

Payment gateway credentials, S3 storage, the public avatar read URL, the newsletter signing secret, and Turnstile live in the app_setting table. Admins maintain them at /app/admin/settings.

Mail delivery, Better Auth, and OAuth stay in env. An empty newsletter secret falls back to BETTER_AUTH_SECRET. An empty avatar read URL falls back to AVATARS_FILE_PROXY_URL_FALLBACK in src/config/storage.ts.

First admin

Registration still uses env-based mail (OTP). Create the first user with pnpm muse:user:create, then promote them with pnpm muse:user:role. After that, open /app/admin/settings.

What stays in env vs what moves to the admin page

SurfaceSourceWhy
Site URL, database, Better Auth, OAuthEnvNeeded before any admin session exists
Mail (MAIL_PROVIDER, Resend / Cloudflare)EnvRegistration OTP must work with an empty settings table
MUSE_SETTINGS_SECRETEnvWrapping key for encrypted settings; never stored in the database
Payment gateways, webhook secrets, default gatewayAdmin settingsServer-only secrets; encrypted at rest
Checkout product IDsAdmin settings (billing.productMap)Checkout source of truth
S3 access key, secret, endpoint, avatars bucket, public read URLAdmin settings (Storage)Credentials are encrypted. The read URL is sent to signed-in clients on /api/auth/get-session
Newsletter signing secretAdmin settings (Newsletter)Optional. Empty uses BETTER_AUTH_SECRET. A new value invalidates existing unsubscribe links
Turnstile site key and secretAdmin settings (Captcha)Both must be saved before forgot-password shows a captcha

First-deploy order

Set BETTER_AUTH_SECRET, MUSE_SETTINGS_SECRET, database, and mail env vars. MUSE_SETTINGS_SECRET must be at least 32 characters.

Apply migrations (pnpm drizzle:migrate) so app_setting and app_setting_revision exist.

Create and promote an admin (pnpm muse:user:create, then pnpm muse:user:role).

Open /app/admin/settings. Save gateway credentials, checkout product IDs, and S3 settings. Use Test connection before going live. The Storage tab also holds the public avatar read URL. Newsletter and Captcha are separate tabs.

The billing tab keeps the Default gateway card at the top. Each provider (Creem, Stripe, Dodo, Waffo) starts collapsed. Expand a provider to edit its credentials and that gateway's checkout product IDs (Pro monthly / yearly / Lifetime) in the same panel. Badges show Default, Configured / Not configured, mapped product-ID count, and Unsaved.

Optional: pnpm muse:settings:import copies leftover local gateway/storage credentials into the database once. Runtime never reads those env vars afterward. Checkout product IDs are entered in Admin → Settings, not env.

Encryption

Secret fields are stored as unprefixed base64url ciphertext. Each row uses HKDF-SHA256 (info bound to scope:key) plus AES-256-GCM with additional data bound to the same identity. Moving ciphertext to another field fails decryption. cipher_version records the algorithm; 0 is plaintext for non-secret fields.

Capability fallback

If no gateway is stored, checkout CTAs on signed-in billing pages are disabled and launch returns a readable error. If S3 is missing, avatar/file APIs return STORAGE_ENV_MISSING (503) and the storage page shows the empty state. The public homepage stays static and does not query settings.

Cache

Settings are cached in-process and keyed by app_setting_revision. Other instances refresh within 30 seconds.

On this page