Local development
Run Margin on your own machine, with or without accounts, and the commands you will use day to day.
Margin runs locally with one command and no accounts or keys. Accounts switch on as you add configuration.
Requirements#
- A current Node.js LTS release
- pnpm
- Git
The Cloudflare tooling (wrangler and the Workers runtime) is installed as a development dependency, so you do not need a Cloudflare account to run Margin locally.
Set up#
git clone https://github.com/jstEagle/A-Level-Notes-Website.git margin
cd margin
pnpm install
cp .env.example .env.local # public keys, optional
cp .dev.vars.example .dev.vars # server secrets, optional
pnpm db:migrate:local # creates the local D1 database
pnpm devOpen http://localhost:3000. If port 3000 is taken, the dev server picks the next free port and prints it.
What works without keys#
Everything works with empty configuration files:
| Without | Behaviour |
|---|---|
VITE_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY | Accounts are off. The sidebar shows Guest · saved on this device, all data stays in localStorage, and /onboarding saves its answers in the browser. |
| Vectorize | Has no local emulation, so semantic search is off in development and search is lexical only. |
To try accounts locally, either create a free Clerk development application and put its two keys in .dev.vars, or turn on local test accounts with MARGIN_DEV_AUTH=1 in .dev.vars, which need no external account. Then run pnpm db:migrate:local so profiles can be stored. See Try accounts locally.
The AI features are switched off by one flag, AI_ENABLED in src/config/ai.ts. To work on them locally, set it to true and put ANTHROPIC_API_KEY and MARGIN_DEV_SITE_AI=1 in .dev.vars. Under pnpm dev every AI request is then answered by Claude, signed in or not. Production builds ignore both. See Configuration.
See Configuration for every variable.
Commands#
| Command | What it does |
|---|---|
pnpm dev | Start the dev server on port 3000 with hot reload |
pnpm check | Type-check everything with tsc --noEmit. Run it after every change. |
pnpm notes:check [course ...] | Compile notes exactly as the build does and report every problem. Run it after every content edit. |
pnpm build | Production build. Fails if server-only code leaks into a client bundle. |
pnpm preview | Serve the production build locally |
pnpm db:generate | Generate a migration after editing src/db/schema.ts |
pnpm db:migrate:local | Apply migrations to the local D1 database |
pnpm db:migrate:remote | Apply migrations to the production D1 database |
pnpm dev:reset-auth | Forget every local test account and profile in the local database |
pnpm deploy | Build and deploy to Cloudflare |
pnpm cf-typegen | Regenerate worker-configuration.d.ts from wrangler.jsonc |
There is no test suite yet: pnpm check, pnpm notes:check and pnpm build are the gates.
Editing content#
Notes and docs reload in the browser as you save them.
NOTES_WATCH=0 pnpm devfreezes notes at startup. Use it when working on the interface while content is being rewritten, so the page does not reload on every note edit.- Docs under
content/docsalways reload, whateverNOTES_WATCHis set to.
See Writing notes for the content workflow.
Running several dev servers#
Several dev servers can share one checkout, for example when you run a second copy on another port. Give each its own dependency cache so they do not fight over it:
VITE_CACHE_DIR=node_modules/.vite-docs pnpm dev --port 3103Changing the database schema#
User data types are defined twice, on purpose: as Zod schemas in src/lib/data/schemas.ts (used by the client and validated on the server) and as Drizzle tables in src/db/schema.ts. Edit both together, then:
pnpm db:generate
pnpm db:migrate:localCommit the generated migration in drizzle/ with your change.
Conventions#
- Server-only code (the Anthropic SDK,
cloudflare:workers, Clerk's server helpers, request headers) lives in*.server.tsfiles or is loaded with a dynamicimport()inside acreateServerFnhandler. Never import it from anything a component imports. - Data is read and written through the collections from
useData(), never through server functions directly. Inserts supply every field, usinguid()andDate.now(). - Messages between panes go through
sendandsubscribeinsrc/lib/inbox.ts. - New tab kinds need an entry in the
Tabtype insrc/lib/workspace.ts, a title and icon for the tab bar, and a case in the workspace's tab content. - Design: build from
src/components/uiand lucide icons, and use only the semantic colour tokens. No accent colours. - Writing: British spelling and sentence-case headings, in the interface and in content.
See Architecture for how the pieces fit together.