Skip to content

Configuration reference

Every environment variable, secret and Cloudflare binding Margin reads, where each one goes, and what happens without it.

Margin needs no configuration to run. With empty files it works fully on one device: accounts are off and study data stays in the browser. Each setting on this page switches one more thing on. The AI features are switched off in code, not by configuration; see AI.

Where settings go#

FileHoldsRead
.env.local (development) or .env (production builds)Public VITE_ variablesAt build time. They are baked into the client bundle, so never put a secret here.
.dev.varsServer secrets in development, plus any VITE_ variable you want there tooBy the local Workers runtime when pnpm dev starts. Once .dev.vars exists, the runtime ignores .env and .env.local; VITE_ lines in .dev.vars still reach the browser under pnpm dev (never in a build).
wrangler secret put <NAME>Server secrets in productionBy the deployed Worker at request time
wrangler.jsoncCloudflare bindings: D1, KV, R2, Vectorize, Workers AIBy wrangler and the Vite Cloudflare plugin
Your shellDevelopment-only switchesBy the dev server when it starts

Start from the examples in the repository:

cp .env.example .env.local
cp .dev.vars.example .dev.vars

Both .env.local and .dev.vars are ignored by Git. Restart pnpm dev after changing either.

Clerk public settings live in src/config/auth.json. Development uses the development instance; production builds use the production instance at studymargin.com. Only CLERK_SECRET_KEY belongs in .dev.vars or the production secret store.

Try accounts locally#

There are two ways to try sign-in and onboarding on your own machine. docs-notes/local-auth.md in the repository has the details.

A Clerk development instance gives you the real sign-in. It is free and runs on localhost:

  1. At the Clerk dashboard, choose Create application, tick Email and Google, and create it. (Margin's own development instance is already set up in src/config/auth.json.) Stay on its Development instance.

  2. Under User & authentication: turn on sign-up and sign-in with email using an email verification code, and turn passwords off (and phone numbers and usernames). Under SSO connections, keep Google on. Development instances use Clerk's shared Google credentials, so there is nothing to set up with Google.

  3. Under Paths: sign-in /sign-in, sign-up /sign-up, after sign-up /onboarding, after sign-in /app, and the development host you use, such as http://localhost:3000.

  4. Under API keys, put the publishable key in src/config/auth.json (development.publishableKey) and the secret key in .dev.vars:

    CLERK_SECRET_KEY=sk_test_...

    Never put the secret key in a .env file or in src/config.

  5. Run pnpm db:migrate:local once, then restart pnpm dev and open /sign-up.

Local test accounts need no external account at all. With no CLERK_SECRET_KEY, add MARGIN_DEV_AUTH=1 to .dev.vars and restart pnpm dev. The sign-in pages then accept any email: the one-time code is shown on the page and printed in the dev server console, and "Continue with Google" lets you pick any address. Everything after sign-in (onboarding, profiles in the local database, sign-out) runs as in production, and the account menu says Local test account. pnpm dev:reset-auth forgets every test account and profile in the local database. Local test accounts exist only under pnpm dev: production builds remove the code, and Clerk keys switch them off.

Public variables#

VariableUsed forWithout it
src/config/auth.json publishable keysAccounts. Clerk's publishable key (pk_test_… or pk_live_…), which the browser uses to show sign-in.Accounts are off. The sidebar shows Guest · saved on this device.
VITE_SITE_URLThe public origin, such as https://margin.example.com, with no trailing slash. Used for canonical links, og:url, absolute social image URLs, robots.txt and the sitemap.Canonical links are left out and social image URLs are relative, which most link previews ignore. robots.txt and the sitemap fall back to the host of each request.

Set VITE_SITE_URL for every production build: Open Graph and Twitter cards need absolute image URLs to show the preview image.

Server secrets#

SecretUsed forWithout it
CLERK_SECRET_KEYAccounts. Clerk's secret key (sk_test_… or sk_live_…), used by the Worker to verify who is signed in. Needs the matching publishable key in src/config/auth.json too.The Worker cannot verify who is signed in, so accounts do not work and nothing syncs. Set both Clerk keys together.
ANTHROPIC_API_KEYLocal development only, together with MARGIN_DEV_SITE_AI=1 and AI_ENABLED set to true: lets pnpm dev answer AI requests with Claude so you can work on AI features. Production builds ignore it.Nothing changes for users: AI is off.
MARGIN_DEV_SITE_AIDevelopment only. 1 answers AI requests with ANTHROPIC_API_KEY under vite dev, for everyone, signed in or not, when AI_ENABLED is true. It has no effect in a production build.AI requests are refused.
ADMIN_TOKENProtects POST /api/admin/reindex, which rebuilds the semantic search index. Use a long random string.The reindex endpoint refuses every request, so semantic results never appear. Lexical search still works.

Generate a token with:

openssl rand -hex 32

Accounts need both Clerk keys: the Worker only attaches Clerk when it has the secret key and the publishable key together, and the browser only offers sign-in when it has the publishable key.

Accounts with Clerk#

Margin has its own sign-in and sign-up pages (/sign-in, /sign-up, finished on /sso-callback) built on Clerk's hooks, with two ways in and no passwords: Google and an email code. In the Clerk dashboard, for each instance (development and production):

  1. User & authentication → Email: turn on Sign-up with email and Sign-in with email, choose Email verification code as the verification method, and require the email address.
  2. User & authentication → Password: turn off (the pages have no password field).
  3. User & authentication → Email (or Email, phone, username): turn off phone numbers and usernames, which the pages can't collect.
  4. SSO connections: add Google. Development instances can use Clerk's shared credentials; for production, create a Google OAuth client and enter its client id and secret, with the redirect URI Clerk shows.
  5. Paths (under Configure → Paths or Account Portal): set the sign-in URL to /sign-in, the sign-up URL to /sign-up, and the after sign-up fallback to /onboarding. The app also passes these to ClerkProvider.
  6. Restrictions: leave sign-ups open, which email-code sign-ups need.
  7. Bot protection is fine to keep on: the pages render Clerk's challenge in a clerk-captcha element.
  8. For production, add your domain to the Clerk instance and set its DNS records as Clerk shows.

The emails Clerk sends (sign-in codes, new-device alerts and the rest) use Margin's own templates. See Account emails for how to edit them and push them to each instance.

Onboarding and profiles#

After the first sign-in, the workspace sends people to /onboarding until they finish it: role (student or teacher), year or school and subjects, then courses, which finishes it. Answers are stored in the profiles table, created by drizzle/0003_profiles.sql:

pnpm db:migrate:local    # development
pnpm db:migrate:remote   # production

Until that migration runs, signed-in users still get into the app, but their answers are not saved and onboarding comes back.

Cloudflare bindings#

Bindings are declared in wrangler.jsonc. Their types are generated into worker-configuration.d.ts by pnpm cf-typegen.

BindingTypeUsed forWithout it
DBD1 databaseAccount data: every synced table in src/db/schema.ts. Migrations live in drizzle/.Signed-in data cannot be saved. Guests are unaffected.
KVKV namespaceDevelopment only: hourly limits for the site key and local test accountsLocal test accounts are off. Nothing changes in production.
VECTORSVectorize indexSemantic search over note chunks. The index needs 768 dimensions and the cosine metric.Search is lexical only.
AIWorkers AIEmbeddings for semantic search, with @cf/baai/bge-base-en-v1.5Search is lexical only.
UPLOADSR2 bucketReserved for uploads. Nothing reads or writes it yet.Nothing changes.

The resource names in wrangler.jsonc (the Worker, the D1 database, the R2 bucket and the Vectorize index) are still alevel-maths… from before the project became Margin. They are only identifiers; keep them, or rename them consistently in wrangler.jsonc, package.json and your Cloudflare account. See Deploying for creating each resource.

In development, D1, KV and R2 are emulated locally by the Workers runtime, so they work with no Cloudflare account. Vectorize has no local emulation, so semantic search is off in development.

Development switches#

These are read by the dev server and have no effect on a deployed site.

VariableEffect
MARGIN_DEV_AUTH=1In .dev.vars. Turns on local test accounts when no Clerk keys are set (see Try accounts locally). Production builds do not contain them.
NOTES_WATCH=0Freezes notes at startup: edits under content/notes no longer reload the page. Use it when working on the interface while content is being rewritten. Docs always reload.
VITE_CACHE_DIRVite's dependency cache directory. Default node_modules/.vite. Give each dev server its own, such as node_modules/.vite-docs, when several share one checkout.
NOTES_WATCH=0 VITE_CACHE_DIR=node_modules/.vite-ui pnpm dev --port 3101

AI#

Every AI feature is switched off by one flag, AI_ENABLED in src/config/ai.ts, which is false. While it is off, the AI entry points are hidden in the interface, and the AI server functions and /api/ai/* refuse every request. There is no end-user AI provider yet: the provider layer (resolveProvider in src/server/ai/provider.server.ts) and its guard (requireAiAccess in src/server/ai/access.server.ts) are kept so one can plug in later. See Architecture.

For working on AI features locally, set AI_ENABLED to true, then put ANTHROPIC_API_KEY and MARGIN_DEV_SITE_AI=1 in .dev.vars. Under pnpm dev, AI requests are then answered by Claude. Production builds ignore both, so a deployed site has no AI whatever the flag says.

Semantic search is not affected by the flag: it needs only the VECTORS and AI bindings and ADMIN_TOKEN.

Everything at a glance#

NameKindWhereRequired for
Clerk publishable keysPublicsrc/config/auth.jsonAccounts
VITE_SITE_URLPublic.envCanonical links and social images
CLERK_SECRET_KEYSecret.dev.vars, wrangler secretAccounts
ANTHROPIC_API_KEYSecret.dev.varsDevelopment-only AI (with MARGIN_DEV_SITE_AI=1 and AI_ENABLED)
MARGIN_DEV_SITE_AISecret.dev.varsDevelopment only
MARGIN_DEV_AUTHSwitch.dev.varsDevelopment only: local test accounts
ADMIN_TOKENSecret.dev.vars, wrangler secretSemantic search index
DBBindingwrangler.jsoncAccounts, profiles
KVBindingwrangler.jsoncDevelopment only: site-key limits, local test accounts
VECTORSBindingwrangler.jsoncSemantic search
AIBindingwrangler.jsoncSemantic search
UPLOADSBindingwrangler.jsoncNothing yet
NOTES_WATCHShellDev serverDevelopment only
VITE_CACHE_DIRShellDev serverDevelopment only

Search documentation

Search every page of the Margin docs