Configuration
Every environment variable Subsweep reads, with defaults and aliases, which ones do nothing yet, and the settings that live in code rather than in a panel.
8 min readUpdated Oct 17, 2026Applies to Subsweep 0.1.0
On this page
Subsweep is configured entirely through environment variables. There is no admin settings screen in version 0.1.0: keys, email, billing and feature switches are all read from the environment when the server starts. Workspace owners change their own workspace (name, currency, digest timing, notifications) under Settings in the app.
On your own server, put the variables in .env.production.local (Next.js loads it for both the build and npm start). On Vercel, add them under the project's environment variables.
Important
Variables starting with NEXT_PUBLIC_ are compiled into the browser bundle. Set them before npm run build, and rebuild after changing them.
How values are read
- An empty value counts as unset, so
FOO=behaves like leavingFOOout. - Trailing newlines and a literal
\nat the end of a value are stripped, which helps when values are piped into a host's CLI. - Where a variable has aliases, the first non-empty one wins, in the order listed.
NODE_ENV=production(set automatically bynext buildandnext start) switches off every development fallback and turns on the production checks below.
Required in production
Until all five are set, npm run build fails and a running server answers every request (including /api/health) with a 500 error, logging Missing production env: ....
| Variable | Aliases | What it does |
|---|---|---|
DATABASE_URL |
POSTGRES_URL, POSTGRES_PRISMA_URL, DATABASE_URL_UNPOOLED, POSTGRES_URL_NON_POOLING |
Postgres connection string. In development, leaving it empty uses PGlite in .data/subsweep. |
BETTER_AUTH_SECRET |
AUTH_SECRET |
Signs sessions and sign-in tokens. Development falls back to a fixed built-in string. |
BETTER_AUTH_URL |
NEXT_PUBLIC_APP_URL |
Your public origin, with no trailing slash. Builds every link, OAuth redirect URI and Stripe return URL. Default http://localhost:3000. |
ENCRYPTION_KEY |
None | Encrypts stored bank, email, Slack and accounting tokens with AES-256-GCM. 64 hex characters are used as the key directly; any other string is hashed (SHA-256) into one. In development an empty value derives the key from the auth secret. |
CRON_SECRET |
JOB_SECRET |
Protects /api/cron/tick and /api/jobs/tick. If both are set, JOB_SECRET is the one checked. In development an empty value leaves the endpoint open. |
App and sign-in
| Variable | Default | What it does |
|---|---|---|
NEXT_PUBLIC_APP_URL |
Unset | Fallback for BETTER_AUTH_URL. Set it to the same origin. |
DEMO_LOGIN_ENABLED |
On in development, off in production | Shows the one-click demo shop on the sign-in page and enables /api/demo/login. Production needs the exact value true; development is on unless the value is exactly false. While it is off, the published demo password no longer signs in. See The demo account. |
ALLOW_UNVERIFIED_SIGNUP |
Off | Only matters in production with neither SMTP nor Resend. Exactly true accepts email sign-ups whose address is never verified. Magic links stay refused. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
Unset | "Continue with Google" appears when both are set. Redirect URI: <BETTER_AUTH_URL>/api/auth/callback/google. Also used for Gmail scanning when the Gmail variables are empty. |
PORT |
3000 |
Port for npm start. You can also pass -p to next start. |
Passwords need at least 10 characters, and sessions last 30 days. When an email provider is configured in production, new email accounts must verify their address before they can sign in. In production without an email provider, email sign-up is refused ("Sign-up is unavailable because email delivery isn't set up, so we can't verify your address.") unless ALLOW_UNVERIFIED_SIGNUP=true, and magic links are refused in every case. Signing in with an existing password and Google sign-in still work. Development accepts sign-ups without verification.
Outbound email
SMTP is used when a user and password are set. Resend is used when an API key is set. If both are configured, SMTP is tried first and Resend is the fallback. With neither, nothing is sent: each message is logged as [subsweep] Email to .... Development also prints magic links in the log; production refuses email sign-up and magic links instead (see App and sign-in).
| Variable | Aliases | Default | What it does |
|---|---|---|---|
SMTP_HOST |
None | smtp.gmail.com |
Mail server. |
SMTP_PORT |
None | 465 |
Mail server port. |
SMTP_SECURE |
None | true |
Implicit TLS. Set false for port 587 (STARTTLS) or 25. |
SMTP_USER |
GMAIL_SMTP_USER |
Unset | SMTP username. SMTP is off until this and the password are set. |
SMTP_PASS |
GMAIL_SMTP_PASS, GMAIL_APP_PASSWORD |
Unset | SMTP password. Spaces are removed, so a Gmail app password can be pasted as shown. |
SMTP_FROM |
RESEND_FROM, EMAIL_FROM |
Subsweep <SMTP_USER> |
Sender for SMTP. |
RESEND_API_KEY |
None | Unset | Turns on Resend. |
RESEND_FROM |
EMAIL_FROM, SMTP_FROM |
Subsweep <hello@localhost> |
Sender for Resend. Must use a domain verified in Resend. |
EMAIL_PAUSED |
None | Off | Exactly true stops every outgoing email (sign-in links included) and logs it instead. |
Email is sent for: email verification, magic links, team invites, renewal reminders, manual charge checks, new-subscription, price-increase and trial alerts, digests, the monthly statement reminder and the account-deleted confirmation. Addresses ending in @northwind.studio (the demo team) are never mailed.
Billing
See Plans, billing and selling it as SaaS for the setup steps.
| Variable | What it does |
|---|---|
STRIPE_SECRET_KEY |
Turns on Stripe Checkout and the customer portal. |
STRIPE_WEBHOOK_SECRET |
The endpoint's signing secret, used to verify POST /api/webhooks/stripe. Required for the webhook in every environment: without it the endpoint answers 503 to every event, and an unsigned or wrongly signed event gets 400. |
STRIPE_PRICE_PRO_MONTHLY |
Price ID for Pro billed monthly. Also used for yearly when the yearly price is empty. |
STRIPE_PRICE_PRO_YEARLY |
Price ID for Pro billed yearly. Also used for monthly when the monthly price is empty. |
STRIPE_PRICE_ACCOUNTANT |
Price ID for the Accountant plan, charged per client with a quantity of at least 3. |
Bank feeds
| Variable | Default | What it does |
|---|---|---|
PLAID_CLIENT_ID, PLAID_SECRET |
Unset | Plaid for US and Canadian workspaces. |
PLAID_ENV |
sandbox |
sandbox, development or production. |
PLAID_REDIRECT_URI |
Unset | Passed to Plaid Link for OAuth institutions. |
TRUELAYER_CLIENT_ID, TRUELAYER_CLIENT_SECRET |
Unset | TrueLayer for UK workspaces. Redirect URI: <BETTER_AUTH_URL>/api/oauth/truelayer/callback. |
TRUELAYER_ENV |
Sandbox | Exactly production uses the live TrueLayer hosts. |
SALTEDGE_APP_ID, SALTEDGE_SECRET |
Unset | Salt Edge for EU workspaces. Not tested against a live Salt Edge account; see Integrations. |
SETU_AA_ENABLED |
false |
India Account Aggregator through Setu. Off unless exactly true and both Setu keys are set. |
SETU_CLIENT_ID, SETU_CLIENT_SECRET |
Unset | Setu credentials. |
SETU_ENV |
Sandbox | Exactly production uses the live Setu host. |
What each provider actually does in this version is on Integrations.
Email receipt scanning
| Variable | Default | What it does |
|---|---|---|
GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET |
The Google sign-in client | Google OAuth client for Gmail scanning (gmail.readonly). Redirect URI: <BETTER_AUTH_URL>/api/oauth/gmail/callback. |
MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET |
Unset | Microsoft app for Outlook scanning (Mail.Read). Redirect URI: <BETTER_AUTH_URL>/api/oauth/outlook/callback. |
MICROSOFT_TENANT |
common |
Tenant ID or domain for single-tenant apps. |
Slack, accounting and usage
| Variable | What it does |
|---|---|
SLACK_CLIENT_ID, SLACK_CLIENT_SECRET |
Slack app for posting alerts. Redirect URI: <BETTER_AUTH_URL>/api/oauth/slack/callback. |
QUICKBOOKS_CLIENT_ID, QUICKBOOKS_CLIENT_SECRET |
QuickBooks Online connection. Redirect URI: <BETTER_AUTH_URL>/api/oauth/quickbooks/callback. |
XERO_CLIENT_ID, XERO_CLIENT_SECRET |
Xero connection. Redirect URI: <BETTER_AUTH_URL>/api/oauth/xero/callback. |
GOOGLE_WORKSPACE_CLIENT_ID, GOOGLE_WORKSPACE_CLIENT_SECRET |
Usage connection for Google Workspace. No screen links to it, and no data is fetched in 0.1.0. |
NOTION_CLIENT_ID, NOTION_CLIENT_SECRET |
Usage connection for Notion. Same status. |
ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET |
Usage connection for Zoom. Same status. |
Read Integrations before relying on Slack, QuickBooks or Xero: each has a gap in version 0.1.0.
Storage, exchange rates and jobs
| Variable | Default | What it does |
|---|---|---|
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET |
Unset | Cloudflare R2 for attachments. When all four are set, uploads go to R2 instead of .data/uploads. See the limitation. |
R2_PUBLIC_URL |
Unset | Public base URL for R2 files. |
OPEN_EXCHANGE_RATES_APP_ID |
Unset | Live USD-based exchange rates, refreshed by the hourly job. Without it, fixed fallback rates are used (1 USD = 83 INR, 0.78 GBP, 0.92 EUR, 1.36 CAD). |
INNGEST_EVENT_KEY |
Unset | Sends one subsweep/jobs.tick event to Inngest each time the hourly job runs. It does not run any jobs in Inngest. |
Analytics and errors
| Variable | What it does |
|---|---|
NEXT_PUBLIC_SENTRY_DSN |
Loads Sentry's browser bundle and reports front-end errors (10% trace sampling). Server errors are not sent. |
NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST |
Intended for PostHog analytics, but the loader does not initialise PostHog in 0.1.0. |
Variables that do nothing yet
These appear in .env.example but nothing in version 0.1.0 reads them:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY(Checkout is a redirect, so it is not needed)SLACK_SIGNING_SECRETINNGEST_SIGNING_KEYSENTRY_DSN(only theNEXT_PUBLIC_variant is used)
QUICKBOOKS_ENV is read but has no effect: the export always calls QuickBooks' sandbox API.
Values that live in code
Several things a SaaS owner might expect to configure are constants in the source. Change them there and rebuild.
| What | Value | Where |
|---|---|---|
| Free plan subscription cap | 10 | lib/billing/plan.ts (FREE_SUBSCRIPTION_CAP) |
| Trial length | 14 days | lib/workspace/provision.ts, and app/api/clients/route.ts for client workspaces |
| Plan names, prices and bullet points shown in the app | Pro $24/mo or $199/yr, Accountant $15 per client | lib/billing/plan.ts, app/(marketing)/pricing/page.tsx, the billing tab in app/(app)/settings/page.tsx, and the landing page in components/landing/landing-experience.tsx |
| Prices created in Stripe by the setup script | 2400, 19900 and 1500 cents, USD | scripts/setup-stripe-prices.mjs |
| Renewal reminder schedule | 30, 14 and 7 days before yearly charges or charges of 100.00 or more, otherwise 3 days | lib/jobs/reminders.ts |
| Negotiation suggestions | Over 100 a month, renewing within 60 days | app/(app)/actions/page.tsx |
| Countries offered in onboarding | United States, Canada, United Kingdom, Germany, India | lib/country.ts |
| Currencies | USD, CAD, GBP, EUR, INR | lib/fx.ts |
| Share link lifetime, invite lifetime | 30 days, 7 days | app/api/share/route.ts, app/api/team/route.ts |
| Upload limits | Statements 10 MB; attachments 20 MB, 10 per subscription | app/api/sources/upload/route.ts, app/api/attachments/route.ts |
| Demo account | priya@northwind.studio, Northwind Studio |
lib/demo-account.ts |
| Product name in emails and the app | Subsweep | lib/email/templates.ts, lib/email/send.ts, app/layout.tsx and the components |
A complete production example
# --- Required ---
DATABASE_URL=postgres://user:password@db-host:5432/subsweep
BETTER_AUTH_SECRET=base64-32-bytes
BETTER_AUTH_URL=https://subs.example.com
ENCRYPTION_KEY=64-hex-characters
CRON_SECRET=random-string
# --- Recommended ---
NEXT_PUBLIC_APP_URL=https://subs.example.com
# The demo shop stays off in production unless DEMO_LOGIN_ENABLED=true
# --- Email (without it, production refuses email sign-up and magic links) ---
SMTP_HOST=smtp.example.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=postmaster@example.com
SMTP_PASS=...
SMTP_FROM=Subsweep <no-reply@example.com>
# --- Google sign-in (optional) ---
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# --- Stripe (optional; needed to charge customers) ---
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_PRO_MONTHLY=price_...
STRIPE_PRICE_PRO_YEARLY=price_...
STRIPE_PRICE_ACCOUNTANT=price_...
# --- Bank feeds (optional) ---
PLAID_CLIENT_ID=
PLAID_SECRET=
PLAID_ENV=production
# --- Exchange rates (optional) ---
OPEN_EXCHANGE_RATES_APP_ID=