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#
| File | Holds | Read |
|---|---|---|
.env.local (development) or .env (production builds) | Public VITE_ variables | At build time. They are baked into the client bundle, so never put a secret here. |
.dev.vars | Server secrets in development, plus any VITE_ variable you want there too | By 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 production | By the deployed Worker at request time |
wrangler.jsonc | Cloudflare bindings: D1, KV, R2, Vectorize, Workers AI | By wrangler and the Vite Cloudflare plugin |
| Your shell | Development-only switches | By the dev server when it starts |
Start from the examples in the repository:
cp .env.example .env.local
cp .dev.vars.example .dev.varsBoth .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:
-
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. -
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.
-
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 ashttp://localhost:3000. -
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
.envfile or insrc/config. -
Run
pnpm db:migrate:localonce, then restartpnpm devand 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#
| Variable | Used for | Without it |
|---|---|---|
src/config/auth.json publishable keys | Accounts. 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_URL | The 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#
| Secret | Used for | Without it |
|---|---|---|
CLERK_SECRET_KEY | Accounts. 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_KEY | Local 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_AI | Development 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_TOKEN | Protects 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 32Accounts 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):
- 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.
- User & authentication → Password: turn off (the pages have no password field).
- User & authentication → Email (or Email, phone, username): turn off phone numbers and usernames, which the pages can't collect.
- 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.
- 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 toClerkProvider. - Restrictions: leave sign-ups open, which email-code sign-ups need.
- Bot protection is fine to keep on: the pages render Clerk's challenge in a
clerk-captchaelement. - 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 # productionUntil 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.
| Binding | Type | Used for | Without it |
|---|---|---|---|
DB | D1 database | Account data: every synced table in src/db/schema.ts. Migrations live in drizzle/. | Signed-in data cannot be saved. Guests are unaffected. |
KV | KV namespace | Development only: hourly limits for the site key and local test accounts | Local test accounts are off. Nothing changes in production. |
VECTORS | Vectorize index | Semantic search over note chunks. The index needs 768 dimensions and the cosine metric. | Search is lexical only. |
AI | Workers AI | Embeddings for semantic search, with @cf/baai/bge-base-en-v1.5 | Search is lexical only. |
UPLOADS | R2 bucket | Reserved 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.
| Variable | Effect |
|---|---|
MARGIN_DEV_AUTH=1 | In .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=0 | Freezes 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_DIR | Vite'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 3101AI#
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#
| Name | Kind | Where | Required for |
|---|---|---|---|
| Clerk publishable keys | Public | src/config/auth.json | Accounts |
VITE_SITE_URL | Public | .env | Canonical links and social images |
CLERK_SECRET_KEY | Secret | .dev.vars, wrangler secret | Accounts |
ANTHROPIC_API_KEY | Secret | .dev.vars | Development-only AI (with MARGIN_DEV_SITE_AI=1 and AI_ENABLED) |
MARGIN_DEV_SITE_AI | Secret | .dev.vars | Development only |
MARGIN_DEV_AUTH | Switch | .dev.vars | Development only: local test accounts |
ADMIN_TOKEN | Secret | .dev.vars, wrangler secret | Semantic search index |
DB | Binding | wrangler.jsonc | Accounts, profiles |
KV | Binding | wrangler.jsonc | Development only: site-key limits, local test accounts |
VECTORS | Binding | wrangler.jsonc | Semantic search |
AI | Binding | wrangler.jsonc | Semantic search |
UPLOADS | Binding | wrangler.jsonc | Nothing yet |
NOTES_WATCH | Shell | Dev server | Development only |
VITE_CACHE_DIR | Shell | Dev server | Development only |