Environment variables
The one rule: the extension bundle is public
Section titled “The one rule: the extension bundle is public”Everything the extension app reads at build time (any WXT_- or
VITE_-prefixed variable) is compiled into the shipped extension and
readable by anyone who downloads it from the store. Treat extension env as
configuration, never secrets:
- OK in extension env: Firebase web config, hosting URLs, feature flags.
- NEVER in extension env: Stripe secret keys, service-account JSON, webhook signing secrets, any API key that grants data access. Those live only in the backend (Secret Manager / function env).
Extension variables (apps/extension/.env)
Section titled “Extension variables (apps/extension/.env)”Created from .env.example by the setup wizard. All are build-time.
Their types are in apps/extension/env.d.ts: a variable you add gets a
line there too.
| variable | module | purpose |
|---|---|---|
WXT_BACKEND | backend-supabase | Backend stack for this build: firebase (default) or supabase. Picks the background wiring at build time (wxt.config.ts); unset, the build uses the stack your clone ships. |
WXT_GOOGLE_OAUTH_CLIENT_ID | backend-firebase | Google OAuth client ID (Web application type; redirect https://<ext-id>.chromiumapp.org/). Set → sign-in uses the chrome.identity web-auth-flow, and the build drops the offscreen permission. Empty → offscreen signInWithPopup fallback (Chromium only). |
WXT_FIREBASE_HOSTING_URL | backend-firebase, uninstall-feedback | Firebase Hosting URL (https://<project>.firebaseapp.com) used by the offscreen sign-in fallback and the uninstall survey page. |
WXT_FIREBASE_API_KEY, WXT_FIREBASE_AUTH_DOMAIN, WXT_FIREBASE_PROJECT_ID, WXT_FIREBASE_STORAGE_BUCKET, WXT_FIREBASE_MESSAGING_SENDER_ID, WXT_FIREBASE_APP_ID | backend-firebase | Optional per-build overrides of the web config in utils/firebase.ts, for CI and e2e builds. Commented out in .env.example. |
WXT_SUPABASE_URL | backend-supabase | The Supabase project URL (https://<project-ref>.supabase.co). |
WXT_SUPABASE_ANON_KEY | backend-supabase | The project’s publishable (anon) key. Public by design, like a Firebase web config: row security is the authority. |
WXT_ANONYMOUS_AUTH | auth | true = anonymous-first: every install gets a guest uid immediately; sign-in upgrades it in place (uid preserved). Default false. |
WXT_API_URL | auth, billing | Deployed API base URL: https://us-central1-<project>.cloudfunctions.net/api on Firebase, https://<project-ref>.supabase.co/functions/v1/api on Supabase. Sign-out (/auth/revoke), checkout, portal, prices, and paywall usage events call routes under it. |
WXT_PREMIUM | billing | true shows premium UI (pricing, portal, status) and turns the paywall on. Any other value hides it and gates nothing: every feature runs free. Plans/copy live in site.config.ts; amounts and trials are server-side. |
The Firebase web config itself is not env; paste it into
apps/extension/utils/firebase.ts (the TODO marker).
File layering (WXT/Vite dotenv order)
Section titled “File layering (WXT/Vite dotenv order)”Loaded from apps/extension/; later files override earlier ones:
.env: base values (untracked; created from.env.example).env.local: personal overrides (untracked).env.[mode]: per-mode, e.g..env.development(untracked).env.[mode].local: personal per-mode overrides (untracked)
For browser-specific values, prefer branching on
import.meta.env.BROWSER (or per-browser manifest fields in
wxt.config.ts) over separate .env.chrome/.env.firefox files.
Prefixes
Section titled “Prefixes”WXT_*: preferred for new variables (exposed onimport.meta.env).VITE_*: also exposed by WXT; the kit itself uses onlyWXT_*.- Unprefixed variables are not available to app code; use that deliberately for build-machine-only values.
Keep .env.example exhaustive, and keep each module’s module.json env
list in sync; that’s what lets the pruner remove template entries with
their module.
Server-side (backend/functions)
Section titled “Server-side (backend/functions)”Secrets, set once per project:
firebase functions:secrets:set STRIPE_SECRET_KEYThat is the sk_… key. The webhook secret (whsec_…) is set for you by
pnpm stripe:webhook; by hand it is:
firebase functions:secrets:set STRIPE_WEBHOOK_SECRETPolar and Paddle have their own pair, bound only when PAYMENT_PROVIDER
names them: POLAR_ACCESS_TOKEN and POLAR_WEBHOOK_SECRET, or
PADDLE_API_KEY and PADDLE_WEBHOOK_SECRET (see
Polar and Paddle).
On the Supabase stack, these server variables are Edge Function secrets
instead, set with supabase secrets set (see
the Supabase guide).
Non-secret knobs, plain env on the function:
| variable | purpose |
|---|---|
PAYMENT_PROVIDER | payment provider for this deploy: stripe (the default when unset) or the provider your clone kept (polar, paddle). Only that provider’s two secrets bind; an unknown value stops the deploy or the function |
POLAR_SANDBOX, PADDLE_SANDBOX | "true" targets that provider’s sandbox API (a separate account and keys) |
BILLING_DISABLED | "true" deploys the API with no provider secrets, for a clone without billing; the billing and credit routes answer 501 |
BILLING_SUCCESS_URL | checkout success return page (https) |
BILLING_CANCEL_URL | checkout cancel return page (https) |
BILLING_PORTAL_RETURN_URL | customer-portal return URL |
BILLING_TRIAL_DAYS | trial started at checkout, in days; 0 = none (the default). A value that is not a whole number stops the deploy or the function. Must equal the days of a trial: { days, card: true } in paywall.config.ts, and stay 0 for any other trial; pnpm run doctor checks the pair. Polar and Paddle keep trials on the product, and their seeds read this value when they create it; at 0 their checkout offers no trial either |
BILLING_AUTOMATIC_TAX | "true" enables Stripe Tax on checkout |
BILLING_ALLOW_PROMO_CODES | "false" hides the promo-code field (default on) |
Credit-based billing needs no env of its own: credit amounts live in
Stripe price metadata (credits on packs, monthly_credits on the metered
plan; pnpm seed:stripe sets them), and site.config.ts → pricing.plans
decides which model is sold. See
the credits model.
For local emulator runs, the same two secrets go in
backend/functions/.secret.local (gitignored); see the
payments guide.