Runtime Settings
Store payment and storage credentials in the database. Admins edit them at /app/admin/settings.
Payment gateway credentials and S3 storage credentials live in the app_setting table. Admins maintain them at /app/admin/settings. Runtime code reads only the database for those two domains — there is no environment-variable fallback.
Mail, authentication, Turnstile, and NEXT_PUBLIC_AVATARS_PROXY_URL stay in env.
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
| Surface | Source | Why |
|---|---|---|
| Site URL, database, Better Auth, OAuth, Turnstile | Env | Needed before any admin session exists |
Mail (MAIL_PROVIDER, Resend / Cloudflare) | Env | Registration OTP must work with an empty settings table |
MUSE_SETTINGS_SECRET | Env | Wrapping key for encrypted settings; never stored in the database |
NEXT_PUBLIC_AVATARS_PROXY_URL | Env | Client components build avatar URLs in the browser |
| Payment gateways, webhook secrets, default gateway | Admin settings | Server-only secrets; encrypted at rest |
| Checkout product IDs | Admin settings (billing.productMap) | Checkout source of truth |
| S3 access key, secret, endpoint, avatars bucket | Admin settings | Server-only secrets; encrypted at rest |
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 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.