Express/TypeScript server that converts Notion pages and uploaded files (HTML, markdown, xlsx, zip) into Anki .apkg decks. Frontend is the sibling workspace under web/.
Mission: give people the simplest, fastest way to turn what they're studying into beautiful Anki flashcards. Drop something in, get a clean deck back. Scale: grow 2anki.net past 300K users. Every PR is checked against both — does it make the experience simpler/faster/more beautiful, and does it move us toward scale?
- Node 22.20.0 (
.nvmrc), pnpm workspace, TypeScript ~6.0 - Express 5, Knex + Postgres (with
better-sqlite3for local), multer for upload - Jest + ts-jest,
*.test.tscolocated next to source - Notion (
@notionhq/client), Anthropic, Stripe, SendGrid, AWS S3 - SonarCloud quality gate; Biome lint runs in the
web/workspace
src/server.ts— boots Express, wires routers, runs migrations on startup, marks interrupted Claude jobs.src/routes/→src/controllers/→src/usecases/→src/services/→src/data_layer/(DB). Each layer has its own CLAUDE.md — read it before editing.- Hot path docs: @src/lib/parser/FEATURE.md, @src/services/NotionService/FEATURE.md, @src/services/observability/FEATURE.md, @src/lib/ankify/FEATURE.md
- Deeper context:
Documentation/,ROADMAP.md,RULES.md.
- Install:
pnpm install(nevernpm/yarn). - Ask before starting the server. Dev:
pnpm dev(server + web). Server only:pnpm dev:server. - Tests:
pnpm test <path>to scope to one file. If output is truncated, rerun without coverage. - All-green gate:
/check(parallel server tsc + web typecheck + web vitest + web lint). - Migrations: regenerate types after a new migration with
pnpm kanel. - Production deploys via the
deploy.2anki.net.ymlworkflow; verify with/deploy-statusafter.
@.claude/rules/security.md @.claude/rules/testing.md @.claude/rules/code-quality.md @.claude/rules/dependencies.md
- Conventional commit prefixes:
feat:,fix:,chore:,refactor:,test:,docs:,perf:,ci:,build:,style:,revert:. - Suggest a branch name before starting code changes; format
<type>/<short-slug>. - Always rebase on
origin/mainbefore opening a PR. - One PR per feature. Never stack PRs — the deploy pipeline pulls a single branch.
- Push pattern:
git push -u origin <branch>— never baregit push, never tomain. Thesafety.pyhook blocks both. - When a unit of work is done, ship it: commit, push, open a draft PR with
gh pr create --draft. - Before
gh pr merge: everystatusCheckRollupentry must be non-FAILURE (not just required ones). Thecheck-merge-status.pyhook enforces this. - Touching auth, payments, or external-API integration? Run
/security-reviewbefore merge. - Document scope/follow-ups in commit bodies, not new GitHub issues.
- For research spanning 3+ queries (where is X defined, what touches Y), spawn
Agent(subagent_type=Explore). If the result isn't immediately needed, run it withrun_in_background: trueand keep editing. - For risky changes (auth, payments, migrations, deploy pipeline), use
EnterWorktree— reverting a worktree is free. - For "wait until X" (long builds, CI, deploys baking on prod), use
ScheduleWakeup(270s if cache-warm matters, 1200s+ for genuine waits). Never busy-poll with sleep. - After deploys to 2anki.net, run
/deploy-statusto confirm the box is healthy. - If you keep approving the same read-only Bash commands, suggest
/fewer-permission-prompts.
- TDD by default: failing test → verify it fails for the right reason → simplest pass → refactor. If asked to skip tests, confirm first.
- Outside-in testing. Mock only external dependencies (HTTP, third-party APIs, email) — never internal services.
- A passing suite is not proof of correctness — review affected user flows for regressions before committing.
- Before declaring a task done, strip scaffolding, debug logs, and temporary code added during implementation.
- Stripe sync is manual only — never put
updateStripeSubscriptionson a cron orsetInterval. - Never edit
src/data_layer/public/— Kanel-generated; rerunpnpm kanelinstead. - Lead with the positive in
if/else(if (ready)notif (!ready)) — Sonar S7735. - Use
value == nullto test "is the ID present?" —!valuerejects falsy IDs like0. - The Ankify feature is gated to users with
users.patreon = true(lifetime). UsehasAnkifyAccessfromsrc/lib/ankify/access.ts; don't reintroduce hard-coded emails. - Notion webhook receiver in
routes/AnkifyWebhookRouter.tsis intentionally inactive; polling at 5 min carries the story today. - The prod box checks out this repo at
/home/alemayhu/src/github.com/2anki/2anki.net(legacy name).
Three sub-agents in .claude/agents/:
- engineer — implements specs, reviews PRs, writes tests, ships.
- designer — UI/UX decisions, copy, visual consistency.
- pm — feedback synthesis, prioritization, spec writing, metrics.
Default: pm produces a spec → designer validates UX (only if user-facing) → engineer implements and ships. For tiny fixes, skip to engineer. Read-only audits go through dead-code-auditor (haiku); isolated feature work through test-writer (sonnet, worktree).
Trio conventions: be opinionated (one recommendation, not five options); specs fit on one page; say what not to build; reply to support email as a draft for Alexander to send.
/triage-feedback,/spec,/implement,/review-pr,/changelog,/weekly-retro,/support-reply— trio workflow./check,/pr-checks,/deploy-status— local + remote status./tdd,/add-tests,/security-audit,/verify-completion,/simplify,/systematic-debugging,/revise-claude-md— engineering aids.