Configuration reference
Customer Portal reads deployment settings from the process environment. Copy .env.example for local development, but provide production values through the hosting platform or secret manager.
Environment variables
| Variable | Required | Default | Meaning |
|---|---|---|---|
DATABASE_URL | Yes | None | PostgreSQL connection string used by the shared Drizzle client and migrations. |
PUBLIC_URL | Production | http://localhost:3051 in .env.example | Public origin used to construct application links and as the fallback Better Auth origin. |
BETTER_AUTH_URL | Production | Falls back to PUBLIC_URL | Canonical Better Auth server and client origin. It must address the same deployed portal. |
BETTER_AUTH_SECRET | Yes | None in production | High-entropy secret used by Better Auth for encryption, signing, and hashing. Use at least 32 characters and keep the same value across application instances. |
PORTAL_ENCRYPTION_KEY | Saved credentials | Empty | Base64-encoded 32-byte root key used to derive purpose-specific keys for credentials saved through portal settings. |
ADMIN_EMAILS | No | Empty | Comma-separated, case-insensitive email allowlist for system administrators. Whitespace is trimmed. |
PORTAL_DEMO | No | false | Enables the protected interactive demo mode. Use only for dedicated demo deployments, never as a production access-control mechanism. |
PORTAL_PROVIDER_NAME | No | Empty | Provider organization name bootstrapped by the portal after migrations. Configure together with PORTAL_PROVIDER_SLUG. |
PORTAL_PROVIDER_SLUG | No | Empty | Provider organization slug bootstrapped by the portal after migrations. Configure together with PORTAL_PROVIDER_NAME. |
RESEND_API_KEY | Email fallback | Empty | Resend credential used until a system administrator saves portal-wide email settings. |
RESEND_FROM_EMAIL | Email fallback | Empty | Sender used with the environment Resend credential. Saved Admin → Email settings take precedence. |
PORTAL_EMAIL_BRAND_NAME | Email branding | Nuxt Customer Portal | Brand name used by the default email frame in apps without portal-managed branding. |
PORTAL_EMAIL_BRAND_TAGLINE | Email branding | Customer workspace | Tagline used by the default email frame in apps without portal-managed branding. |
PORTAL_EMAIL_BRAND_LOGO | Email branding | Empty | HTTPS or image data URL used as the default email logo in apps without portal-managed branding. |
PORTAL_EMAIL_BRAND_LOGO_LIGHT | Email branding | PORTAL_EMAIL_BRAND_LOGO | Light-mode logo available to templates as brand_logo_light. |
PORTAL_EMAIL_BRAND_LOGO_DARK | Email branding | PORTAL_EMAIL_BRAND_LOGO | Dark-mode logo available to templates as brand_logo_dark. |
PORTAL_EMAIL_BRAND_ICON_LIGHT | Email branding | Empty | Light-mode icon available to templates as brand_icon_light. |
PORTAL_EMAIL_BRAND_ICON_DARK | Email branding | Empty | Dark-mode icon available to templates as brand_icon_dark. |
PORTAL_EMAIL_PRIMARY_COLOR | Email branding | #0ea5e9 | Six-digit hexadecimal accent color used by the default email frame in apps without portal-managed branding. |
PORTAL_GITHUB_ENABLED | No | false | Set to true to show and configure GitHub sign-in when both GitHub credentials are also present. |
GITHUB_CLIENT_ID | GitHub sign-in | Empty | GitHub OAuth client identifier. |
GITHUB_CLIENT_SECRET | GitHub sign-in | Empty | GitHub OAuth client secret. |
PORTAL_GOOGLE_ENABLED | No | false | Set to true to show and configure Google sign-in when both Google credentials are also present. |
GOOGLE_CLIENT_ID | Google sign-in | Empty | Google OAuth client identifier. |
GOOGLE_CLIENT_SECRET | Google sign-in | Empty | Google OAuth client secret. |
PORTAL_REGISTRATION_MODE | No | open | Account policy: open, invitation-only, or disabled. Invalid values fall back to open. |
PORTAL_TERMS_URL | No | / | Link shown from authentication forms for the deployment's Terms of Service. |
The provider flags use strict string parsing: only the literal value true enables a flag. Set a provider flag to false when the deployment does not configure that provider.
Generate BETTER_AUTH_SECRET with openssl rand -base64 32; never copy a documentation placeholder into a deployment. See the Better Auth secret reference for the upstream authentication contract.
Generate PORTAL_ENCRYPTION_KEY separately with openssl rand -base64 32. Never reuse the authentication secret as the portal encryption key.
Runtime variables
These variables belong to Node or the generated Nitro server rather than a Customer Portal feature:
| Variable | Production behavior |
|---|---|
NODE_ENV | Set to production by the Docker image and normal production runtimes. Customer Portal uses it to avoid retaining the development database singleton. |
HOST | Bind address for the generated server. The Docker image sets 0.0.0.0; use a loopback address only when a local proxy is the sole caller. |
PORT | Listening port for the generated server. Nitro uses port 3000 unless the runtime overrides it. |
Tooling-only variables
Repository verification and release tooling also read environment variables that are not portal deployment settings:
| Variable | Purpose |
|---|---|
CI | Enables continuous-integration behavior in package and starter verification. |
DEMO_TEST_URL | Overrides the deployed demo origin exercised by demo browser tests. |
GITHUB_TOKEN | Authenticates repository metadata and source-link verification in CI. |
PORTAL_STARTER_TEST_DATABASE_URL | Supplies the disposable PostgreSQL database used by generated-starter verification. |
RELEASE_TAG | Confirms that package versions match the GitHub release tag before publication. |
URL precedence
The authentication server resolves its origin in this order:
BETTER_AUTH_URL;PUBLIC_URL;- the local development fallback.
Set both URL variables to the same HTTPS origin in production. A mismatch can produce incorrect email links, rejected OAuth callbacks, or session cookies attached to the wrong host. Do not add a trailing path.
PUBLIC_URL=https://portal.example.com
BETTER_AUTH_URL=https://portal.example.com
Registration and administrators
PORTAL_REGISTRATION_MODE controls who may create an account:
| Value | Behavior |
|---|---|
open | Anyone can use the signup flow. |
invitation-only | Signup is available only in an organization invitation flow. |
disabled | Public signup is unavailable; existing users may still sign in. |
ADMIN_EMAILS grants the system-wide administrator role when a listed account registers or signs in. This is separate from the owner, admin, and member roles inside an organization. Limit this list to accounts that genuinely administer the whole installation.
Email ownership
The Resend environment variables belong to the platform and send authentication mail. Organization-owned transactional mail is configured in the portal UI and stored per organization. The Invoices module uses that organization credential for invoices and reminders, so changing the platform Resend key does not replace an organization's sender.
Secret handling
Keep these server-only values out of source control, client runtime configuration, logs, screenshots, and issue reports:
DATABASE_URL;BETTER_AUTH_SECRET;PORTAL_ENCRYPTION_KEY;RESEND_API_KEY;GITHUB_CLIENT_SECRET;GOOGLE_CLIENT_SECRET;- saved portal email credentials.
Client IDs and public origins are identifiers rather than secrets, but should still be specific to the intended deployment.
Do not rotate BETTER_AUTH_SECRET by simply replacing it on running instances: existing encrypted or signed auth state may depend on the prior value. Plan secret rotation using Better Auth's versioned-secret mechanism and test active sessions and OAuth flows before production rollout.
Keep PORTAL_ENCRYPTION_KEY stable, backed up with the database, and identical on every application instance. Portal Core derives separate keys for email, product storage, Stripe, planning OAuth, and other purposes; one module's ciphertext cannot be decrypted as another module's secret. Replacing this root key without re-encrypting saved credentials makes those credentials unreadable.
Legacy encryption overrides
Upgraded deployments can continue using these former module-specific keys as optional overrides:
PORTAL_EMAIL_ENCRYPTION_KEY;PRODUCTS_STORAGE_ENCRYPTION_KEY;PRODUCTS_STRIPE_ENCRYPTION_KEY;PLANNING_ENCRYPTION_KEY.
An override takes precedence over PORTAL_ENCRYPTION_KEY for its module. Retain the old variable while credentials encrypted with it remain stored. To consolidate an upgraded deployment, re-save or reconnect the affected integration with only PORTAL_ENCRYPTION_KEY configured; verify it, then remove the old override. Stripe continues to accept the storage override as its legacy fallback when no Stripe-specific override is set.
Production verification
After changing configuration:
- restart the application so Nitro reads the new environment;
- open the portal at
PUBLIC_URLand complete a sign-in; - verify signup matches the configured registration mode;
- request an authentication email and inspect its origin and sender;
- test each enabled OAuth provider from the public hostname;
- confirm a normal member does not receive system-administrator access;
- run the deployment checks in Deployment.
