This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
All commands run from the repo root via mise. Run mise tasks to list all available tasks with descriptions.
# Verify changes (run both before marking work done)
bun run typecheck # tsc -b across all packages
bun run check # biome lint + format (auto-fixes)
mise run verify # Both above
# Development
mise run dev # Both worker + web in parallel
mise run dev:worker # Wrangler on :8787
mise run dev:web # Vite on :5173
# Database
mise run db:generate # Generate Drizzle migration from schema
mise run db:migrate # Apply migrations to local D1 (default)
mise run db:migrate staging # Apply migrations to staging D1
mise run db:migrate prod # Apply migrations to production D1
# Pull remote D1 data into local D1 (wipes local D1 first)
mise run db:pull # Pull from staging (default)
mise run db:pull prod # Pull from production
# Backfill historical Slack data (requires SLACK_BOT_TOKEN env var)
mise run backfill # Generate SQL + apply to local D1 (default)
mise run backfill staging # Generate SQL + apply to staging D1
mise run backfill prod # Generate SQL + apply to production D1
# Deploy (defaults to staging; requires clean git state)
mise run deploy:site # Verify → migrate → deploy worker + Pages (staging)
mise run deploy:site prod # Verify → migrate → deploy worker + Pages (production)
mise run deploy:site --no-pages # Skip Pages deploy
mise run deploy:worker # Deploy worker only to staging
mise run deploy:worker prod # Deploy worker only to production- Local is the default for all D1 commands (
db:migrate,backfill).db:pullis the exception — it has nolocalsource and defaults tostaging. - Staging is the default for all deploy commands (
deploy:worker,deploy:web). - Production always requires explicit
prodargument and an interactive confirmation. - Remote targets (staging/prod) require a clean git working directory.
Each package has its own CLAUDE.md with detailed patterns and conventions:
packages/worker/CLAUDE.md— Adding routes, Drizzle patterns, schema changespackages/web/CLAUDE.md— Routing (TanStack Router), data fetching (TanStack Query), components, styling
Bun monorepo with two packages:
packages/worker— Cloudflare Worker: Hono API + Slack event handler, backed by D1 (SQLite).packages/web— React 19 + Vite SPA. TanStack Router (file-based routing) + TanStack Query. Uses Hono's typed RPC client for end-to-end type safety with the worker API.
The worker exports AppType from app.ts. The web package references the worker via TypeScript project references (tsconfig.json → references), so hono/client infers request/response types across packages with zero codegen.
src/index.ts splits traffic by path:
/api/slack/events→slack-cloudflare-workersSDK (signature verification, challenge, event dispatch)- Everything else → Hono app (
src/app.ts)
Reactions use delete-on-remove (not event sourcing). Two pre-aggregated tables (reaction_totals, user_emoji_counts) are kept in sync by SQLite triggers on reactions and rebuilt from scratch during backfill. Schema lives in src/db/schema.ts, migrations in packages/worker/migrations/.
All routes are defined in packages/worker/src/app.ts. Each .route() call mounts a route file from src/routes/. Read app.ts to see the full list — it's the single source of truth. Don't duplicate the route list elsewhere.
- Always use
bun/bunx, nevernpm/npx. - Biome handles formatting and linting. A PostToolUse hook runs
bun run check && bun run typecheckafter every file edit. - No one-line if statements. Always use braces on a new line, even for single-statement bodies.
- Caret ranges with full semver in package.json (e.g.
"^4.11.9", not"^4"). import typefor type-only imports. Enforced by biome (useImportType). Useimport type { Foo }when importing only types.- No unused imports. Enforced by biome (
noUnusedImports). Remove imports that are no longer used after refactoring. @/alias for cross-directory imports in the web package. Only use relative imports for same-directory siblings. Seepackages/web/CLAUDE.mdfor details.- Prefer Drizzle query builder over raw SQL. Use Drizzle's typed API (
db.select(),db.insert().select(),count(), etc.) instead ofsql`...`template strings for full queries. Rawsqlexpressions are fine within Drizzle operations (e.g.sql`MAX(0, ...)`).