Environment variables
Environment variables, defaults, and required values in the default template.
Saavo stores local development variables in .env and production variables in .env.production. Both files use example.vars as their template. If .env does not exist when you run npm run saavo:init, the script creates it from the template and generates a missing SAAS_SECRET automatically.
Do not commit environment files
.env and .env.production can contain secrets and are included in .gitignore by default. Do not commit either file to Git or expose sensitive values from them in logs, screenshots, or frontend code.
VITE_ variables are public in the browser
Variables prefixed with VITE_ are written to frontend files at build time and can be viewed by any visitor in the browser. Use them only for public information, never for API keys, access tokens, passwords, or private keys.
Environment files
.env
Used for local development, testing, and local previews. npm run saavo:init preserves an existing file and only adds a missing SAAS_SECRET. It does not overwrite values you have already entered.
.env.production
The production environment file used by deployment scripts. npm run deploy:init creates or updates this file, writes the current Worker's workers.dev URL, and prompts you for the required configuration. Do not assume that all existing values will remain unchanged. Empty strings are not written to the remote Worker, and remote secrets absent from the file are not deleted.
For a standalone local production build, the project also supports a local build workflow that reads .env if .env.production does not exist. This does not mean the project is configured for production deployment.
During production deployment, public variables are passed through Wrangler's --var option. All other nonempty variables are written as Worker secrets through a temporary secrets file, which is deleted immediately after use.
| Storage method | Variables |
|---|---|
| Plain variables | VITE_SITE_URL, VITE_SAAVO_COLLECT_API_HOST, VITE_SAAVO_COLLECT_API_ENDPOINT, CLOUDFLARE_TURNSTILE_SITE_KEY, STRIPE_CONNECTION_ID, PADDLE_CONNECTION_ID, PADDLE_ENVIRONMENT, PADDLE_CLIENT_TOKEN, GITHUB_CLIENT_ID, GOOGLE_CLIENT_ID, R2_ACCOUNT_ID |
| Secret | All other nonempty variables |
Keep Cloudflare deployment credentials out of .env.production
CLOUDFLARE_API_TOKEN, CLOUDFLARE_API_KEY, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_EMAIL, CF_API_TOKEN, CF_ACCOUNT_ID, and all variables prefixed with WRANGLER_ are for deployment tools only. npm run deploy:init and npm run deploy:update reject .env.production files that contain these variables.
Site and analytics
VITE_SITE_URL
Prop
Type
example.vars does not define this variable. Local development generates http://127.0.0.1:<port>. Local builds without .env.production use http://127.0.0.1:4173. On the first deployment, the script writes the current Worker's workers.dev URL.
VITE_SAAVO_COLLECT_API_HOST
Prop
Type
VITE_SAAVO_COLLECT_API_ENDPOINT
Prop
Type
These two variables are used only when building public/js/analytics.js. Rebuild the site after changing them.
Application secret
SAAS_SECRET
Prop
Type
The default value in example.vars is empty. npm run saavo:init generates it automatically when missing. You can also run npm run secret:generate to generate a new value.
SAAS_SECRET must remain unchanged over time
A project's .env and .env.production must use exactly the same SAAS_SECRET. Changing it after the project has created encrypted data can make existing data impossible to decrypt, including two-factor authentication secrets, affiliate payout details, and OAuth client secrets.
Cloudflare Turnstile
In config/deploy.ts, auth.useTurnstile defaults to { threshold: 2, interval: '1h' }, which triggers verification based on a threshold. When this configuration is enabled, both the site key and secret key are required. Set it to false to leave these two variables empty. You cannot use true in place of the configuration object.
CLOUDFLARE_TURNSTILE_SITE_KEY
Prop
Type
CLOUDFLARE_TURNSTILE_SECRET_KEY
Prop
Type
example.vars uses Cloudflare's official test credentials, which are suitable for local development. Before deploying to production, create a Turnstile widget for your production domain and replace them with real credentials.
Stripe
config/payment.ts selects Stripe by default. Checkout requires STRIPE_CONNECTION_ID and STRIPE_SECRET_KEY. Webhooks also require STRIPE_WEBHOOK_SECRET. Checkout cannot be created without the first two values, and Stripe webhooks cannot be received without the last one.
STRIPE_CONNECTION_ID
Prop
Type
Avoid changing this value once an environment is live. Otherwise, stored Stripe objects may be treated as belonging to another connection. Use different connection IDs for test and production environments.
STRIPE_SECRET_KEY
Prop
Type
STRIPE_WEBHOOK_SECRET
Prop
Type
Paddle
The following variables are included in the environment template and type declarations, but the project does not yet implement Paddle Checkout, the customer portal, or webhooks. Setting these variables alone does not enable Paddle payments.
PADDLE_CONNECTION_ID
Prop
Type
PADDLE_ENVIRONMENT
Prop
Type
PADDLE_API_KEY
Prop
Type
PADDLE_WEBHOOK_SECRET
Prop
Type
PADDLE_CLIENT_TOKEN
Prop
Type
RESEND_API_KEY
Prop
Type
The template uses Resend by default, so this variable is required for production deployment. If it is missing locally, npm run doctor issues a warning. If it is missing in production, email sending is unavailable and remote checks fail.
If you change emailProvider.type to cloudflare, you do not need RESEND_API_KEY, but you must configure an EMAIL resource binding in wrangler.jsonc. See Cloudflare resource bindings.
OAuth sign-in
GitHub and Google sign-in each require both a client ID and a client secret. Leaving both values empty disables that sign-in method. If you set only one, the sign-in method remains unavailable and npm run doctor issues a warning.
GITHUB_CLIENT_ID
Prop
Type
GITHUB_CLIENT_SECRET
Prop
Type
GOOGLE_CLIENT_ID
Prop
Type
GOOGLE_CLIENT_SECRET
Prop
Type
R2 API credentials
example.vars labels these as R2 API credentials for signing template version downloads. The template retains the variables and type declarations, and npm run doctor checks that all three are set together. The existing application code does not yet read them. These API credentials are unrelated to the resource binding that lets the Worker access MAIN_R2 directly.
R2_ACCOUNT_ID
Prop
Type
R2_ACCESS_KEY_ID
Prop
Type
R2_SECRET_ACCESS_KEY
Prop
Type
Leave all three empty to keep them unconfigured. If you set only some of them, npm run doctor issues a warning.
External notification variables
External notification variables have no fixed names. Each *Binding field in config/notifications.ts specifies an environment variable name. The application reads the webhook, token, or signing secret from that variable. Choose names that suit your project. You do not need to use the example names in example.vars.
Store only variable names in the configuration file
config/notifications.ts must contain only binding names, never webhook URLs, bot tokens, chat IDs, signing secrets, or custom request headers directly.
Default template configuration
The template does not enable any external notification destinations by default. example.vars lists only example variables corresponding to the source comments in notifications.ts, all with empty-string defaults.
| Notification service | Example variable name | Value |
|---|---|---|
| Slack | SLACK_OPERATIONS_WEBHOOK_URL | Incoming Webhook URL |
| Discord | DISCORD_SALES_WEBHOOK_URL | Incoming Webhook URL |
| Discord | DISCORD_SALES_THREAD_ID | Thread ID |
| Telegram | TELEGRAM_ALERTS_BOT_TOKEN | Bot Token |
| Telegram | TELEGRAM_ALERTS_CHAT_ID | Chat ID |
| Telegram | TELEGRAM_ALERTS_THREAD_ID | Message Thread ID |
| Microsoft Teams | TEAMS_OPERATIONS_WEBHOOK_URL | Workflow Webhook URL |
| Feishu or Lark | FEISHU_RELEASE_WEBHOOK_URL | Custom bot webhook URL |
| Feishu or Lark | FEISHU_RELEASE_SIGNING_SECRET | Bot signing secret |
| DingTalk | DINGTALK_RELEASE_WEBHOOK_URL | Custom bot webhook URL |
| DingTalk | DINGTALK_RELEASE_SIGNING_SECRET | Bot signing secret |
| WeCom | WECOM_RELEASE_WEBHOOK_URL | Group bot webhook URL |
| Generic webhook | INTERNAL_AUDIT_WEBHOOK_URL | HTTPS request URL |
| Generic webhook | INTERNAL_AUDIT_WEBHOOK_HEADERS | JSON object containing HTTP request headers |
The value of INTERNAL_AUDIT_WEBHOOK_HEADERS is a JSON object string, for example:
INTERNAL_AUDIT_WEBHOOK_HEADERS='{"Authorization":"Bearer token"}'Plain variables in wrangler.jsonc
The default names below come from the template source. When the CLI creates a project, it first replaces the saavo-template prefix with the project name.
These variables are defined directly in the vars object in wrangler.jsonc, not in example.vars. npm run deploy:init updates these resource names based on the Worker name. Do not duplicate them in .env.
ASYNC_POLICY_TASK_QUEUE_NAME
Prop
Type
ASYNC_LOGGER_QUEUE_NAME
Prop
Type
ANALYTICS_QUEUE_NAME
Prop
Type
R2_BUCKET_NAME
Prop
Type
Type declarations
Wrangler generates worker-configuration.d.ts to declare types for environment variables and Cloudflare resource bindings. After changing wrangler.jsonc, adding or removing fixed variables, or adjusting resource bindings, run:
npm run cf-typegenThis is a generated file. Do not edit it directly. External notifications use custom binding names, which are not declared individually as fixed fields in CloudflareBindings.