Skip to content
Get the kit

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).

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.

variablemodulepurpose
WXT_BACKENDbackend-supabaseBackend 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_IDbackend-firebaseGoogle 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_URLbackend-firebase, uninstall-feedbackFirebase 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_IDbackend-firebaseOptional per-build overrides of the web config in utils/firebase.ts, for CI and e2e builds. Commented out in .env.example.
WXT_SUPABASE_URLbackend-supabaseThe Supabase project URL (https://<project-ref>.supabase.co).
WXT_SUPABASE_ANON_KEYbackend-supabaseThe project’s publishable (anon) key. Public by design, like a Firebase web config: row security is the authority.
WXT_ANONYMOUS_AUTHauthtrue = anonymous-first: every install gets a guest uid immediately; sign-in upgrades it in place (uid preserved). Default false.
WXT_API_URLauth, billingDeployed 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_PREMIUMbillingtrue 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).

Loaded from apps/extension/; later files override earlier ones:

  1. .env: base values (untracked; created from .env.example)
  2. .env.local: personal overrides (untracked)
  3. .env.[mode]: per-mode, e.g. .env.development (untracked)
  4. .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.

  • WXT_*: preferred for new variables (exposed on import.meta.env).
  • VITE_*: also exposed by WXT; the kit itself uses only WXT_*.
  • 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.

Secrets, set once per project:

Terminal window
firebase functions:secrets:set STRIPE_SECRET_KEY

That is the sk_… key. The webhook secret (whsec_…) is set for you by pnpm stripe:webhook; by hand it is:

Terminal window
firebase functions:secrets:set STRIPE_WEBHOOK_SECRET

Polar 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:

variablepurpose
PAYMENT_PROVIDERpayment 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_URLcheckout success return page (https)
BILLING_CANCEL_URLcheckout cancel return page (https)
BILLING_PORTAL_RETURN_URLcustomer-portal return URL
BILLING_TRIAL_DAYStrial 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.