This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Databuddy is a comprehensive analytics platform — a Turborepo monorepo using Bun as the package manager and runtime. It consists of multiple apps (dashboard, API, data collectors) and shared packages, backed by PostgreSQL, ClickHouse, and Redis.
# Start dashboard + API only (most common)
bun run dev:dashboard
# Start all apps
bun run dev
# Lint
bun run lint
# Format
bun run format
# Type check
bun run check-types
# Run tests
bun run test
bun run test:watch
# Database
bun run db:push # Apply schema changes (no migration files)
bun run db:migrate # Run migration files
bun run db:studio # Open Drizzle Studio GUI
bun run db:seed <WEBSITE_ID> [EVENT_COUNT] # Seed sample analytics data
# SDK (must build before dev if SDK changed)
bun run sdk:build
# Build everything
bun run buildNote: All commands use dotenv -- prefix internally to load .env — just run them from root.
# From root
cd apps/api && bun test path/to/test.ts
# Or with filter
cd apps/api && bun test --test-name-pattern "test name"bun install
cp .env.example .env
docker-compose up -d # PostgreSQL, ClickHouse, Redis
bun run db:push
bun run clickhouse:init # from packages/db
bun run sdk:build
bun run dev:dashboardapps/
dashboard/ # Next.js 16 frontend (React 19, TailwindCSS 4)
api/ # Elysia.js backend (Bun, port 3001)
basket/ # Analytics event ingestion service
uptime/ # Uptime monitoring
cron/ # Scheduled jobs
links/ # Short link service
docs/ # Documentation site
status/ # Status page app
slack/ # Slack integration
packages/
db/ # Drizzle ORM schemas + clients (PostgreSQL + ClickHouse)
rpc/ # ORPC router — type-safe API layer between dashboard and api
auth/ # Better-Auth integration + permission system
sdk/ # Public analytics SDK (React, Vue, Node.js)
cache/ # Redis-backed Drizzle caching layer
redis/ # Redis client, pub/sub, BullMQ job queues
shared/ # Shared types, utilities, constants
validation/ # Zod schemas
services/ # Business logic services
email/ # Email via Resend
notifications/ # Notification system
tracker/ # Lightweight client-side tracking scripts
mapper/ # Data transformation utilities
env/ # Environment configuration (type-safe env vars)
devtools/ # Browser devtools extension
encryption/ # Encryption utilities
evals/ # AI eval framework
api-keys/ # API key management and scopes
Browser (SDK/tracker) → basket (ingestion) → ClickHouse (analytics warehouse)
→ PostgreSQL (relational data)
Dashboard (Next.js) ←→ ORPC (rpc package) ←→ API (Elysia) → PostgreSQL + ClickHouse + Redis
RPC Layer (packages/rpc): The central type-safe API contract between dashboard and api. Dashboard uses ORPC client with TanStack Query; API implements the procedures. Adding a new endpoint means defining it in rpc, implementing it in api, and calling it from dashboard.
Database Layer (packages/db): Single source of truth for all schemas. Uses Drizzle ORM for PostgreSQL (relational data: users, websites, settings) and a ClickHouse client for analytics data (events, sessions, pageviews). Schema changes use db:push for development; db:migrate for production migrations.
Caching (packages/cache): Redis cache sits in front of Drizzle queries. Cache keys and TTLs are defined alongside queries.
Auth (packages/auth): Better-Auth handles sessions. The package also contains the permission system used across all apps.
State management in Dashboard: Jotai for local UI state, TanStack Query for server state.
- Runtime: Bun 1.3.14+
- Frontend: Next.js 16, React 19, TailwindCSS 4, Radix UI, Recharts
- Backend: Elysia.js (Bun-native HTTP framework)
- API layer: ORPC (type-safe RPC with OpenAPI generation)
- Auth: Better-Auth
- ORM: Drizzle ORM
- Databases: PostgreSQL 17, ClickHouse 25.5, Redis 7
- Validation: Zod 4
- Linting/Formatting: Biome via Ultracite
- Build: Turborepo + Bun
- Linter/Formatter: Ultracite (Biome-based). Run
bun run lint/bun run format. - TypeScript: Strict mode. Always use proper types — avoid
any. - Commit format:
<type>(<scope>): <description>(e.g.,feat(dashboard): add export button,fix(api): handle null session) - Commit slicing rule: Prefer one commit per coherent product or technical slice, not one giant snapshot and not ultra-fragmented file-by-file commits.
- Split commits by intent: feature, bug fix, refactor, style/copy pass, or migration slice.
- Use the dominant surface as scope:
dashboard,api,rpc,basket,docs,db,sdk,tracker,deps,ci. - Group closely related UI files into one commit when they ship one visible change.
- Keep unrelated surfaces in separate commits even if they were edited in the same session.
- For broad migrations, follow the repo’s existing pattern: one commit per meaningful area, e.g.
feat(dashboard): migrate home, events, insights, and links pages to DS primitives. - Before committing, check
git diff --statandgit status --short; if the diff mixes unrelated intents, split it. - Only make a single snapshot commit for the whole worktree when the user explicitly asks to include everything as-is.
- PRs: Open against
stagingbranch (notmain).
Test infra lives in packages/test. Integration tests live in apps/api/src/integration/.
# Run integration tests (requires Docker: postgres + redis)
cd apps/api && bun test src/integration/
# One-time setup for test DB
cd packages/db && DATABASE_URL="postgres://databuddy:databuddy_dev_password@localhost:5432/databuddy_test" bunx drizzle-kit pushKey helpers from @databuddy/test:
signUp()— creates a real user via better-auth with session cookieaddToOrganization(userId, orgId, role)— inserts member rowuserContext(user, orgId)/apiKeyContext(orgId, scopes)— builds RPC ContextinsertOrganization()/insertWebsite()/insertApiKey()— DB factoriesexpectCode(promise, "FORBIDDEN")— asserts ORPCError codereset()/cleanup()— truncate tables / close connectionsimport "@databuddy/test/env"— sets test env vars (must be first import)
Rules for integration tests:
- Always
import "@databuddy/test/env"as the first line - Use
const iit = hasTestDb ? it : it.skipfor graceful skip when Docker is down - Use
userContext/apiKeyContext/expectCodefrom the test package — don't redefine locally - Type API key objects against
Context["apiKey"]to catch schema drift beforeEach(() => reset())andafterAll(() => cleanup())in every file
- Type test objects against their source type. Fake API keys must be typed as
Context["apiKey"], fake users asUser, etc. If the schema adds a required field, the test must fail to compile — not silently pass with a partial object. - Never hand-write dependency versions. Use
bun add <pkg>to add dependencies. Hand-written version ranges drift from lockfile reality and cause phantom resolution bugs. - Shared test helpers over local copies.
expectCode,userContext,apiKeyContext, env setup — these live in@databuddy/test. If you're about to define a helper that already exists there, import it instead. - Scope maps must match. If
RESOURCE_SCOPE_OVERRIDESchanges inpackages/api-keys/src/scopes.ts, the integration tests inlink-handlers.test.tsandwith-workspace.test.tsmust be updated to match. The link resource is mapped there (read:linksfor read,write:linksfor create/update/delete), as is the flag resource (manage:flagsfor create/update/delete). Both are enforced solely bywithWorkspace; there are no separate pre-check layers. New resources also need role grants inpackages/auth/src/permissions.ts(statement plus each role). - Identity columns are governed by
packages/db/src/clickhouse/identity.ts.PROFILE_ID_TABLESlists every ClickHouse table carryingprofile_id;identity.test.tsenforces that each entry has a CREATE column, an idempotent migration, andprofile_id+anonymous_idin the agent SQL allowlist. Addingprofile_idto a table means adding it there and following the failing test. Query-side identity stitching must useEVENTS_VISITOR_KEY/CUSTOM_EVENTS_VISITOR_KEY/visitorMatch()from that module — never inline the expression. Profile write semantics (trait splitting, upserts) live in@databuddy/services/identity;profile_idvalues are customer-supplied and are never salted, unlikeanonymous_id.
The project has a formal AI usage policy (AI_POLICY.md). For contributions: all AI usage must be disclosed, PRs must reference an accepted issue, and all AI-generated code must be fully human-verified. Maintainers are exempt and may use AI at their discretion.