Environment variables

19 min read

Every setting, where it is read, and which the deploy refuses to run without.

Local values belong in ignored .env files: workspace-wide settings live at the workspace root, and site-specific settings live beside the selected site package. The root .env.example indexes every environment name read by Core, separates operator settings from test controls and internal markers, and leaves credentials blank; sites/demo/.env.example shows the demo site's local settings. The docs check verifies the root example against Core's environment reads and source-tracks the repository's purpose and feature overviews separately from this settings reference. Production values live only in the deploy environment and the functions' secrets.

Always needed by the deploy

Variable What
VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY the project's url and the client's key (locally the stack's; platformkit dev sets them per run)
SUPABASE_SERVICE_ROLE_KEY the service-role key: the catalog is pushed with it, the functions write with it, the tts script uploads with it
SUPABASE_ACCESS_TOKEN, SUPABASE_PROD_REF the CLI's personal token and the allow-listed production project
SUPABASE_DB_URL asked for only where there is a backend (deployment.backend is not false) — the production database's connection the plan runs over — the session pooler url (postgres.<ref>@aws-0-<region>.pooler.supabase.com:5432), password percent-encoded; the direct db.<ref>.supabase.co address is IPv6-only, unreachable from a deploy box, and refused by the check
PLATFORMKIT_SHADOW_DB_URL the shadow database's connection — a Postgres kept for nothing else, reset to the declaration every deploy: a Supabase project (the session-pooler form; its user is postgres, which the diff needs since production's objects are owned by postgres and owners are compared), or a plain Postgres whose user is postgres or can create that role (a managed Postgres whose cluster already owns postgres, Render's included, cannot — the plan says so); refused when it is production or holds anything of anyone's
VITE_APP_URL the site's https origin, nothing after the host: auth links, checkout returns and emails point here; written to the hosted auth settings at deploy
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM asked for only where something sends — accounts, the store, the chats, the funnels; a static site is asked for none of it — the mail relay (Mailgun's or any); SMTP_FROM is "Name
" on a verified domain. Locally, Mailpit's container on port 1025 with no login
PLATFORMKIT_CONTENT_DONE set by platformkit build for the vite build it spawns, and read by nothing else: it tells the kit's own Vite plugin that the content and the prepare steps have already run in this build, so velite does not clean and rewrite what it has just written and the prepare steps — the search index's embeddings among them — are not paid for twice. Never set it by hand
PLATFORMKIT_ALLOW_DESTRUCTIVE consent for a plan that would lose data: the digest the refusal printed (eight hex over that plan's own destructive statements — it expires when they change, so an allowance left behind permits nothing), optionally narrowed to objects (<digest>:public.a.col,public.b); a commit sha matched against the commit being deployed; or 1, a standing permission that still works and is reported as one. A statement that would lose nothing needs none of it
PLATFORMKIT_BUILD_HEAP the heap the bundle is built with, in megabytes — 3072 by default, because the build peaks near 4 GB resident and dies below 2.5 GB of heap, and node's own default is derived from the machine, so a laptop is fine and a small deploy instance is not. off leaves node's default alone, and an explicit --max-old-space-size in NODE_OPTIONS always wins. It is a floor, not a cure: the instance still needs the memory to give
CONTENT_ENGINE velite or collections — which engine compiles the content for this run, overriding the site's own content.engine. Both produce byte-identical output into .velite/*.json; velite is the default and the faster of the two on this content (build/collections.mjs)
PLATFORMKIT_VERBOSE 1 makes a build or a deploy print the bodies it summarises — the whole plan, every emitted bundle file. Off, the log says what happened and not how; a refusal, an error and every destructive statement are printed whole either way. --verbose on the command does the same for that run
VITE_VAPID_PUBLIC_KEY, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT the web-push key pair (node build/vapid.mjs prints it); the public key is used by the browser and the function, which accepts either public-key name

Needed by what the content switches on

Variable When
STRIPE_SECRET_KEY (live), STRIPE_WEBHOOK_SECRET anything for sale: a published priced offer, a book in print, a pricing row with a sku
LULU_CLIENT_KEY, LULU_CLIENT_SECRET (LULU_SANDBOX=1 allowed) · MIXAM_USER, MIXAM_PASS (MIXAM_UK=1 selects the UK shop) · BOOKVAULT_API_KEY a book in print: a printer for every printed format it sells (the one its block names; at least one for auto; Bookvault for premium)
PRINTIFY_API_KEY, PRINTIFY_SHOP_ID a published product using Printify as its supplier; PRINTIFY_MOCK=1 is local-only and makes supplier calls mock responses
CAL_WEBHOOK_SECRET, CAL_API_KEY a published booking type that cal.com schedules (a site whose types are all its own needs neither)
CAL_API_BASE where the kit's cal.com client points (https://api.cal.com/v2 by default): another site's Scheduling API, https://<their domain>/v2, books there instead — the closed loop the test runs against the site itself
LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET a published booking type in a video room that records or seats more than five, or the optional Copilot reader transport; the three values are required together
NETWORK_HANDLE_DOMAIN the domain every member's handle sits under (<username>.<domain>), with a wildcard DNS record and certificate pointing at the site; needed while the network module is on, whether or not the site federates (a closed site answers its own members' apps under it). Atproto reserves .localhost, so a local stack uses a name such as network.test; unset, the function falls back to the site's own host name
NETWORK_PLC_ROTATION_KEY the secret key that controls the portable identity made for each member who joins the wider network; needed only where network.federate is on, and never rotated away
NETWORK_PLC_URL the PLC directory a joined member's identity is made at; https://plc.directory unless set — a local stack points it at a stand-in, never at the real directory, where an identity is permanent
NETWORK_RELAY_URL the relay asked to crawl the site when a member joins and once a day; the config's network.relay (Bluesky's) unless set, and none asks nobody
NETWORK_APPVIEW_URL, NETWORK_APPVIEW_DID the app view a joined member reads the world outside through, and the DID its service tokens are addressed to; the config's network.appview and did:web:api.bsky.app unless set
NETWORK_FEDERATE=1 makes the atproto function federate whatever the config says — a local stack's tests only; the deploy refuses it
WEBRTC_STUN where a browser finds its own public address, comma separated; a default is used when it is unset
WEBRTC_TURN_URL with WEBRTC_TURN_SECRET a relay for the two people in a call who cannot reach each other directly — a coturn the site runs, or anything that speaks the standard. The function mints <expiry>:<person> and its HMAC; nothing is required and no provider is named
WEBRTC_TURN_URL with WEBRTC_TURN_USERNAME / WEBRTC_TURN_PASSWORD the same, with the long-term credentials some relays hand out instead
STORAGE_S3_ACCESS_KEY, STORAGE_S3_SECRET_KEY a room type that records: the film lands in the recordings bucket through Storage's S3 endpoint (STORAGE_S3_ENDPOINT, STORAGE_S3_REGION optional, defaulting to the project's own)

Optional, with a working fallback

Variable What
SUPPORT_EMAIL where the support digest goes (else every admin and support address)
NVM_DIR the nvm installation directory (~/.nvm by default), used when the platformkit CLI restarts under Node 22 because npm run dev started with another Node version
PLATFORMKIT_SITE_<NAME>_GIT_URL the HTTPS, SSH, or local Git URL used by platformkit sites fetch when --url is omitted; uppercase the site name and replace hyphens with underscores
PLATFORMKIT_GITHUB_TOKEN (else GITHUB_TOKEN / GH_TOKEN) authenticates managed site fetches and updates for private GitHub HTTPS remotes. Git receives it through temporary process configuration (GIT_CONFIG_COUNT is adjusted internally); do not set that variable yourself. The URL and local site registry never contain the token
UNSPLASH_ACCESS_KEY the admin offers an Unsplash tab on every picture field; unset, the library is the only source and no tab is drawn. A free key from unsplash.com/developers; the server holds it, never the browser
REDDIT_USER_AGENT optional Reddit API identification string; the generic kit value is used when unset
LINKEDIN_API_VERSION optional LinkedIn API version; the adapter uses its supported default when unset
SHARE_FONT optional font family for generated share-card images; defaults to Newsreader
FORMS_WEBHOOK_SECRET the key a form's webhook deliveries are signed with (X-Form-Signature); else derived from the service key
PUBLIC_FUNCTIONS_URL, VITE_PUBLIC_FUNCTIONS_URL the functions on the site's own domain (https://<site>/v1), with the host's rewrite; off by default
VITE_AUTH_MAGIC_LINK, VITE_AUTH_GOOGLE the extra sign-in methods (Google also needs the provider enabled in Supabase)
IN_BROWSER_TTS false switches the person's in-browser voice off for everyone
LOCKDOWN_MODE 1 closes the built site to everyone but the staff
CONTENT_STORE cloud selects row-backed content: the admin function serves the production CMS, rows are exported before build, and the content API reads rows; with a connected repository, publishing writes back and rebuilds, while without one rows are the sole source. Otherwise the build uses the resolved deployment.content setting. The CLI and deploy pass that resolved value into the browser build; a backend site registering admin.models is refused if it would compile file-backed content. Read by the build and inlined into the bundle under its exact name; invalid values are refused
VITE_SUPABASE_STUDIO_URL the local Studio's address, linked from the admin
VITE_BANK_SHEET a site with an instrument: swap the statement bank from a Google Sheet without a release
MATRIX_API_TEST 0 skips the Matrix lane’s live half, as the other API lanes are skipped
MATRIX_FN_URL where that lane looks for the served function; the local stack’s address by default
SOCIAL_TEST_MAILPIT_URL the social invitation integration test’s local Mailpit URL; defaults to the stack’s Mailpit port
TTS=1 the deploy speaks what is missing (hours) and mirrors the weights
PRINT_BUILD=1 explicitly enables the deploy's printed-edition render and upload step; unset or any other value skips it. This does not disable printer-credential checks for published print offers or change local print commands. When skipped, the cover image in each book's markdown cover field shows beneath any unavailable generated print art
TTS_VOICE, TTS_STEPS, TTS_SPEED, TTS_THREADS, TTS_BITRATE the voice and its settings (a change renames every recording)
TTS_COMMIT=1 / local, TTS_GITHUB_TOKEN (else GITHUB_TOKEN / GH_TOKEN), TTS_COMMIT_BRANCH, TTS_COMMIT_NAME, TTS_COMMIT_EMAIL a tts run commits and pushes its store; a build box's token; the committer it signs as
TTS_GAP the silence between spoken pieces, in seconds
FFMPEG a path to ffmpeg, when not the machine's or ffmpeg-static
SUPABASE_URL the project url for the tts script's uploads (else VITE_SUPABASE_URL)
APP_URL the functions' side of the app's origin: platformkit dev writes it into the functions' env for the run; the deploy sets it from VITE_APP_URL
SITE_URL the site's public address for the prerender's absolute links, when content/site.md sets none
PRINT_COMMIT=1 / local, PRINT_GITHUB_TOKEN / GITHUB_TOKEN a title made at the printer committed back
PLATFORMKIT_ALLOW_DESTRUCTIVE=1 for one run: a plan that loses data is applied, and the log says so
SCHEMA_TEST=0 skips the schema test in platformkit test
SQL_COLUMNS=0 skips the column guard (every column a SQL function names, against the kit's and the site's declarations read as one) in platformkit test; it needs no database
PLATFORMKIT_SKIP_STATIC_LANE=1 skips the static lane — a site with no backend built, served and walked with the console and the network watched. It is the slow one (a real build per fixture), and skipping it is reported at the end of the run rather than passing quietly, since a shape nothing exercises is a shape that rots
PLATFORMKIT_SHARE_STACK=1 runs the share sidecar check against a real compose stack — a Playwright image and a compose build, so it is out of the default run; without it the nginx config is still checked both ways (with a sidecar and without) and the skip says what went unchecked
PLATFORMKIT_SKIP_CLOUD_LANE=1 skips the cloud lane — the publish cycle over rows against a local stack. It skips itself when no stack is up, and either way says which shape went uncovered
POLICY_TEST=0 skips the policies test (who may read what, against the local stack's seed) in platformkit test; it skips itself when the stack is down
ENCRYPTION_TEST=0 skips the encryption test (what a sealed message leaves in the row and in the index, and that the seal is one-way) in platformkit test; it skips itself when the stack is down
ANALYTICS_TEST=0 skips the database half of the analytics test (the channel rule, the audience partition, recirculation, right now, the channels over the seed) in platformkit test; the arrival parser is still checked; it skips itself when the stack is down
INTERACTION_URL where the interaction lane (build/interaction.test.mjs) finds the site to use — a browser that types into the palette, sends a line from the support widget, answers a diagnostic screen and opens the bag, with the console watched. Default http://localhost:5173; --url overrides it. With nothing serving there the lane skips loudly rather than passing
BROWSER_WAIT_MS the floor on every browser timeout in the repository, set on each context by build/browser.mjs. Default 60000. Playwright’s own default is 30000 and applies to any call given no explicit budget, so a file that writes 60 on the waits it thought about still ran the rest at 30 — and under load those are the ones that fail. One number, and it is a minimum: a call that needs longer still passes its own
INTERACTION_WAIT_MS how long the interaction lane waits for any condition before giving up. Default 60000 — generous on purpose, since the lane drives a real browser while the rest of the suite loads the same machine, and a timeout tuned to an idle laptop makes it fail for reasons that have nothing to do with the site
CONTENT_API_TEST=0 skips the content index test (every published entry rendered, an unpublished one absent, the ids stable) in platformkit test
FORMS_API_TEST=0 skips the Forms API test (the shapes, the official client against the served function, the webhooks, the embed) in platformkit test
FORMS_API_SHOTS a directory the Forms API test writes its embed screenshot into (none by default)
SCHEDULING_API_TEST=0 skips the Scheduling API test (the shapes, cal.js pointed at the site, the judge, the webhooks, cal.com's embed) in platformkit test
RECOMMENDATIONS_TEST=0 skips the recommendations test (what somebody is interested in, against the local stack: a re-tagged piece changing it with nothing re-run, a partial read counting, a purchase outweighing browsing) in platformkit test; it skips itself when the stack is down
REWRITES_TEST=0 skips the rewrites test (the nginx instance in Docker against the local stack) in platformkit test; it skips itself when Docker or the stack is down
SCHEDULING_API_SHOTS a directory the Scheduling API test writes its embed screenshots into (none by default)
STOREFRONT_API_TEST=0 skips the Store API test (the products and the schema, the official client against the served function, the cart, the checkout url followed) in platformkit test
PROMO_TEST=0 skips the discount-budget test (a limit counted in the database, and ten checkouts racing for three remaining uses) in platformkit test; it skips itself when the stack is down, naming what it did not cover
INVENTORY_TEST=0 skips the stock test (the decrement racing two connections for the last unit, reservations, a refund putting the count back) in platformkit test; it skips itself when the stack is down, naming what it did not cover
ORDERS_TEST=0 skips the orders test (the transactions summing to the total and refused at the write, fulfilment and payment derived rather than stored, an edit raising the version, stock coming back on received and never on requested) in platformkit test; it skips itself when the stack is down, naming what it did not cover
STOREFRONT_API_SHOTS a directory the Store API test writes the handoff page's screenshots into (none by default)
SUPPORT_API_TEST=0 skips the Support API test (the shapes, the Client and Application APIs, the webhooks, the snippet) in platformkit test
SUPPORT_API_SHOTS a directory the Support API test writes the snippet's screenshots into (none by default)
SDK_TEST=0 skips the analytics script's test (the built file loaded in a headless page against the served ingest door: PostHog's surface, the size, a page view, identify, an opted-out page, PostHog's snippet) in platformkit test; it skips itself when the stack is down
FUNNELS_TEST=0 skips the funnel analytics test (the journey and the conversions against the local stack's seed, the unsubscribe link against the served function) in platformkit test; it skips itself when the stack is down
WEBHOOKS_TEST=0 skips the shipped-subscriptions test (declared, pushed, a secret from the environment, one made by hand untouched) in platformkit test; it skips itself when the stack is down
WH_TEST_SECRET set by that test alone, to prove a secret reaches a subscription from the environment and never from the file; nothing else reads it
SPACES_TEST=0 skips the spaces' database test (the fill sweep, the isolation, the counters against a walk, the cursor, the live topic) in platformkit test; it skips itself when the stack is down
NETWORK_TEST=0 skips the network's database tests (the audiences, a reply never wider than its post, blocks and mutes, the timeline, moderation — and the community's one notification list) in platformkit test; it skips itself when the stack is down
ATPROTO_TEST=0 skips the atproto test (the network as an atproto server, driven by Bluesky's own client against the served function) in platformkit test; it skips itself when the function is not served
ATPROTO_INTEROP_TEST=0, ATPROTO_PLC_PORT skip the federation's interop test; the port its PLC directory listens on (2582, and its app view the next) — it skips itself when the function does not federate
COPILOT_TEST=0 skips what a person told an assistant being theirs (the policies, the recall floor, the deletion) in platformkit test; it skips itself when the stack is down
COPILOT_TTS_URL, COPILOT_TTS_KEY an OpenAI-shaped /v1/audio/speech the site is heard in instead of the kit's own voice. The key stays here and never reaches the browser; with neither set the page speaks for itself
COPILOT_TTS_MODEL, COPILOT_TTS_VOICE which model and voice that endpoint is asked for (tts-1, alloy)
COPILOT_TURN_MS maximum time the Copilot completion function waits for one turn; defaults to 60 seconds
COPILOT_BASE_URL optional LiveKit worker completion base URL; if unset, the worker uses APP_URL plus /v1
COMMUNITY_TEST=0 skips the community's database test (the three kinds in one instance, invitations, moderating and inviting as grants, the four acts, the mute as a fence) in platformkit test; it skips itself when the stack is down
AWARDS_TEST=0 skips the achievements and traits database test (the event stream awarding a badge when its rule is met and not before, and a client able neither to award itself one nor to read what the site believes about it) in platformkit test; it skips itself when the stack is down
AGENT_TEST=0 skips the agent loop's database test (a run answered, a worker killed mid-run and picked up by another, a cancel that stops a tool before it runs, a budget that holds) in platformkit test; it skips itself when the stack or the functions are down
AGENTS_LIVE=1 bots may buy thinking. Without it nothing reaches a vendor whatever keys are set, because a key in the environment is not consent to spend money on bots; the rule is in the provider rather than in a variable somebody remembers
PLATFORMKIT_AGENT_MOCK=1 every bot answers from a script instead of a model, so nothing is bought. A run that carries a script is never bought anyway, whatever this says
AGENT_API_KEY where bots buy their thinking; OPENAI_API_KEY is used when it is unset. With neither, bots answer from the mock
AGENT_API_BASE an OpenAI-shaped endpoint other than OpenAI's — Azure, Groq, Together, OpenRouter, vLLM, Ollama. Defaults to https://api.openai.com/v1
AGENT_MODEL the model a bot uses when its own file does not name one
ANTHROPIC_API_KEY a SECOND provider, tried when the first fails for a reason of its own — no credits, a bad key, a rate limit, their server down. A malformed request is not retried: it fails the same way everywhere
ANTHROPIC_MODEL what the fallback uses. It uses its OWN model, never the one asked for: a model is a provider's own word, and sending gpt-4o-mini to Anthropic is a second failure rather than a fallback
ANTHROPIC_API_BASE somewhere other than https://api.anthropic.com
AGENT_PROVIDERS the order, by name (anthropic,openai). Anything not named is left out. Default: the OpenAI-shaped one, then Anthropic
AGENT_MAX_TOKENS the longest answer Anthropic may give (its API requires a limit; 4096 by default)
DESK_TEST=0 skips the staff desk's database test (three sources in one list, each answered where it lives, and nothing of anyone's disclosed to somebody) in platformkit test; it skips itself when the stack is down
COMMENTS_TEST=0 skips the comments' database test (who is held and who is not, the flagged word beating trust, what an approval writes down, the embed's three declarations) in platformkit test; it skips itself when the stack is down
PEOPLE_TEST=0 skips the People panel's database test (every account, one person's whole picture, and never the last administrator away) in platformkit test; it skips itself when the stack is down
PAYOUTS_TEST=0 skips the ledger's database test (the sweep run twice, the delay, and a refund either side of the window) in platformkit test; it skips itself when the stack is down
HOLDS_TEST=0 skips the holds test (nobody's time taken twice: the hold written with the booking, two at the same instant, a group holding its host once) in platformkit test; it skips itself when the stack is down
TEAMS_TEST=0 skips the teams membership test (the rule filling a team, an invitation surviving a change to the rule, a membership that is not a directory) in platformkit test; it skips itself when the stack is down
PROVIDERS_TEST=0 skips the stack half of the providers test (a signed version frozen, who is owed the new one, an offering theirs and never deleted, failing to sign stopping selling and nothing else) in platformkit test; the pure half still runs, and it skips itself when the stack is down
HUDDLE_LIVE=0 skips the two-browser call in the huddle test (the rest of it still runs); it skips itself when no dev server answers
HUDDLE_APP where that two-browser call looks for the site, when it is not http://localhost:5173
HUDDLE_ROOM the room it opens, as a path (/spaces/<space>/<channel>); by default the site's staff space and its general
PLATFORMKIT_ICONS_ALL=1 the icons step writes every Lucide glyph, not only the ones the pages name (the dev server does this by itself, so the builder's palette is whole)
OPENAI_API_KEY, REPLICATE_API_TOKEN, COMPANION_MODEL, COMPANION_THINKING the enrichment's model
PRERENDER=0, SHARE_IMAGES=0, PLAYWRIGHT_CHROMIUM_PATH the build without the prerender or the images; a browser of your own
DEPLOY=1 what lets deploy.mjs run at all

Several sites on one host

Variable What
PLATFORMKIT_DOMAIN the parent domain that enables named site hosts; use localhost in development and the real domain in production. Unset, the existing single-site host stays in place
PLATFORMKIT_ROOT_SITE optional DNS name for a site configured at the checkout root, which has no directory name; the tooling-only root is not a site and this is normally unset
PLATFORMKIT_DEFAULT_SITE site used by root CLI and npm site commands, and by workspace development when no explicit multi-site selection is set; defaults to sites/docs. It may name a site or give a path to a tracked, fetched, or separately linked site; --site takes precedence
PLATFORMKIT_SITES comma-separated site names to include. In workspace development, an explicit value selects a multi-site set and takes precedence over PLATFORMKIT_DEFAULT_SITE; in production files mode it is required so a fixture or other unselected site is never published
PLATFORMKIT_SITES_MODE proxy for the local Vite front door or files for built production sites; defaults to files
PLATFORMKIT_APEX the selected site that also answers the bare domain; www redirects to its subdomain. Unset, the bare domain and www return 404
PLATFORMKIT_DEV_DIRECTORY internal workspace-dev opt-in; enables the development-only directory at the unqualified front-door host. Do not set for production
PLATFORMKIT_DEV_DIRECTORY_PORT internal host port for the workspace directory server, allocated for that dev run and passed to nginx
PLATFORMKIT_DEV_RUN_ID internal ownership token set on a managed dev run; a run is recognised by its recorded PID and the moment that PID started (the token is the check only for a record written before start times were kept)
PLATFORMKIT_DEV_PARENT_RUN_ID internal marker for site dev servers launched by the workspace runner; prevents registering workspace-owned children as independent runs, and lets a later start or platformkit stop find a dead run's servers and wait for them to finish stopping
PLATFORMKIT_DEV_PARENT_PID internal parent PID passed to a workspace-launched site server; the server shuts down when it no longer has that parent (the run was killed or crashed)
Per-site Supabase URL each selected backend site needs an absolute project origin setting named by appending its directory name, uppercased with hyphens replaced by underscores, to the SUPABASE_URL prefix. Required per backend site in production files mode; a site declaring no backend needs none

Production TLS is enabled when both /etc/nginx/certs/fullchain.pem and /etc/nginx/certs/privkey.pem are mounted; without the pair, the host serves HTTP and reports that it has no certificate. The multi-site guide shows the development front door and production arrangement.

Local only — the deploy refuses them

Variable What
DEV_ACCOUNT an address npm run db:reset makes again after emptying the database, so a developer is not locked out of their own app mid-session: made staff, given a live membership, and put into every space and every open room. DEV_ACCOUNT_NAME, DEV_ACCOUNT_PASSWORD (default password123) and DEV_ACCOUNT_ROLES (a comma-separated set — admin by default; DEV_ACCOUNT_ROLE is read too, for one) go with it. Unset, nothing happens; it refuses any database that is not a local stack
STRIPE_MOCK=1 checkout completes on the spot with Stripe's objects made up
PRINT_MOCK=1, LULU_MOCK=1, MIXAM_MOCK=1, BOOKVAULT_MOCK=1 every printer call (or one printer's) answered locally; a mock order walks to shipped
PAYOUTS_MOCK=1 local only (the deploy refuses it): stands in for the two acts that touch the payment processor — a provider's account connects in one step and a transfer sends nothing — while the ledger is written exactly as it would be
PRINT_DRIVER which Typst compiles a book: node (the default, the native compiler on a machine) or wasm (the same compiler built for an Edge Function, where a cloud-only site has nothing else). Both set the same edition — build/print-parity.test.mjs is what holds them to it
PRINT_NO_SHOTS=1 skip the page pictures on the listing. They are photographed by pdf.js in a headless Chromium, which a function has not got, so they are best-effort: a cloud-only site gets the book and the EPUB and no page pictures. The edition is the same either way — they are a listing nicety and never part of the book
PRINT_PARTS=1 say what an edition's hash is made of, part by part, as each book is set — the templates, the typeset source, the trim, the bindings, the colours, the picture. It is how you answer "why did this book come out as a different edition in the cloud?" without diffing a typeset source by eye; the function answers the same thing when a call asks for parts
IDENTITY_PROBE=0 skip the identity probe (build/identity-probe.mjs — does signing in on a page give you the page reloading it gives?). It is not in npm test: it wants a browser, a dev server and a couple of minutes
IDENTITY_PROBE_ROUTES which routes it asks about, comma separated (default /spaces,/app,/app/chat,/content,/store)
IDENTITY_PROBE_USER, IDENTITY_PROBE_PASS who it signs in as (default the seed's member@example.com / password123)
IDENTITY_PROBE_WAIT milliseconds to let a surface settle before it is read (default 6000)
PRINT_TEST_BOOK which book print-cloud.test.mjs sets against the running stack; the-golden-key by default
SUPABASE_ANON_KEY the publishable key, read by the tests that call a function as a person would; the local stack's is the default
LIVEKIT_MOCK=1 (or no LiveKit keys) the site's video rooms on a LiveKit dev server (LIVEKIT_MOCK_API its address from the functions, http://host.docker.internal:7880 by default) and a recording a fixture film the mock lands