Contributing
The distilled version of how this codebase actually gets built — licensing, the branch/PR workflow, the code standards CI enforces, and the architecture principles.
Dhaga (धागा — "thread") is an AI-native personal CRM: capture contacts anywhere, turn every note into a private knowledge graph, query it in natural language. Thanks for wanting to help build it. This guide is the distilled version of how this codebase actually gets built — the rules here are enforced by CI habit, review, and in some cases by tooling, not just convention.
Licensing — read before contributing
The repo is open-core:
- Everything except
packages/eeis AGPL — the self-hostable core. packages/ee(multi-tenant RLS, billing, admin, early access) is source-available under PolyForm Shield 1.0.0, not AGPL.
Self-hosting needs nothing from packages/ee — see
Self-hosting. By contributing to packages/ee
you agree your contribution is licensed under its license, not the AGPL.
Getting started
npm install # workspace root — installs every package
npm run dev --workspace web # zero-config: embedded PGlite, no DATABASE_URLThat's a fully working single-user instance — schema self-heals on first run (there are no migration files, see "Schema changes" below).
For the full multi-tenant stack (Postgres + RLS, what production runs):
docker compose up -d db # pgvector/pg16 on localhost:54329
# point DATABASE_URL at it (see compose.yml), set DHAGA_HOSTED_MODE=trueDemo data — a realistic 20k-contact "life network" (families, employers, events, friend circles, power-law hubs):
cd apps/web
node --env-file=<your env> scripts/seed-dummy-graph.mjs recreate --contacts=20000
# flags: --seed=N (deterministic), --stress (worst-case shapes), --with-notesNext.js caveat
This repo runs a Next version whose APIs may differ from what you (or your AI
assistant) remember. Read the guides in
apps/web/node_modules/next/dist/docs/ before writing app-router code —
apps/web/AGENTS.md exists precisely to make agents do this.
Branch & PR workflow
- One concern, one fresh branch off
main, one PR. Alwaysgit checkout main && git pullfirst. Never push additional commits to a branch whose PR might already be merging — we once lost a privacy fix to that exact race and had to ship a follow-up PR minutes after a merge. - PR descriptions state what was verified, not just what changed: which
tests, whether
tsc/next buildran, and — for anything with a runtime surface — what you actually drove in the app. "Tests pass" with skipped suites, or "done" with silently-skipped steps, is treated as a bug in the PR itself. Surface uncertainty; don't average it away. - If two existing patterns contradict, pick one (more recent / better tested), say why in the PR, and flag the loser for cleanup. Don't blend them.
Code standards
The non-negotiables (a .claude/settings.json hook enforces the first two on
every AI-assisted edit; humans get review):
- TypeScript strict. No
any, no@ts-ignore. Explicit prop and return types. Shared types live inpackages/coreor the app'stypes/— never redefined locally. - 150-line rule. Any
.ts/.tsxfile over 150 lines becomes a directory with anindex.tsbarrel re-exporting, so import paths never change. - Constants live in
apps/web/src/utils/constants/— no inline magic arrays or numbers in components/libs. Enums (rare) inutils/enums.ts. - Imports: all at the top, no wildcards, ordered React/Next → third-party
→
@/internal → types. - No duplicate code. Before writing a utility, check
packages/core,@/utils,@/lib/*,@/components/ui/*. Logic used by 2+ places gets extracted immediately; logic used by 2+ apps belongs inpackages/core. - UI: shadcn/ui primitives only (no raw
<button>/<input>outsidecomponents/ui), loading states on every in-flight interactive element, generous whitespace, one accent color (amber). Mobile-first is mandatory — every feature works at 375px before desktop polish. Grids always carry a breakpoint (grid-cols-1 sm:grid-cols-2), touch targets ≥ 44px, sidebars collapse to aSheet, tables wrap inoverflow-x-auto. - Provider-agnostic gateways. LLM and web-search access go through the
LLMClient/SearchClientinterfaces inpackages/core— adding a provider means a new implementation plus a factory case, zero caller changes. Don't import a concrete provider anywhere else.
Architecture principles
- Local-first. The client works without the cloud; cloud is sync + heavy compute, never a dependency. (This is why the graph caches payloads in IndexedDB and layouts in the DB — returning users paint instantly.)
- Tiered inference. Free on-device primitive → smallest capable model → Batch API for latency-insensitive jobs. Never call a bigger model where a smaller one passes evals.
- Every AI-derived fact keeps a receipt —
source_note_idon facts and edges. Deleting a note tombstones everything derived from it. - Structured outputs always. Extraction uses Zod-derived JSON schemas —
never parse free-text model output. (Convention:
.nullable()rather than.optional()— strict structured-output mode rejects optional fields.) - Boring storage. The knowledge graph is relational tables — no graph DB.
Schema changes
There are no migration files. Schema = Drizzle definitions
(apps/web/src/lib/db/schema/) in lockstep with idempotent self-heal DDL
(apps/web/src/lib/db/ddl/ — CREATE TABLE IF NOT EXISTS,
ALTER ... ADD COLUMN IF NOT EXISTS) that runs on first DB connection. Rules:
- Statement order is load-bearing; append, don't reorder.
- New tenant-scoped tables must be added to
TENANT_TABLESinpackages/ee/src/db/rls-ddl.ts— a deliberate one-line decision, or the table silently ships without row-level security (we've been bitten). - Adding
user_idto an already-populated table needs an ownership backfill (see thepositionsbackfill inrls-ddl.tsfor the pattern), or existing rows become invisible to their own tenants. - Data-normalizing UPDATEs inside DDL need the transaction-local
app.bypass_rlsDO-block pattern (seeddl/kg.ts), or FORCE RLS silently no-ops them on hosted installs.
Security & privacy
Privacy is the product's moat — violations are bugs, not style issues.
ANTHROPIC_API_KEYand all LLM code are server-only. Clients call our/api/*; never a third-party API directly. If you add"use client", audit the file's imports immediately.- Never log contact PII, note transcripts, or extraction output. Telemetry is counts and timings only.
- No silent data collection; enrichment and cloud AI are user-triggered or explicitly opted in.
- Deletion cascades fully (contact → notes → facts → edges → embeddings), and export must always work.
- Client-side caches of user data must be scoped to the signed-in account and purged on mismatch (the graph's IndexedDB cache is the reference).
- Hosted mode:
DATABASE_URLmust use a session-scoped connection — Supabase's session pooler (port 5432) or direct, never the transaction pooler (6543). Tenant scoping rides on session-levelset_config; transaction pooling silently breaks it and can leak the setting across backends. Boot guards fail loud on both this and BYPASSRLS roles — if your deploy refuses to start, that's them doing their job.
Testing & verification
npx vitest runinapps/web; on Windows add--no-file-parallelism— worker-fork crashes without a named failing test are infra noise, and only a serial run gives a trustworthy signal.- Typecheck all three packages:
apps/web,packages/core,packages/ee. - Tests encode WHY. Every non-obvious assertion carries the business
reason (grep
WHY:for examples). A test that can't fail when the business logic changes is wrong. - Unit tests are necessary, not sufficient: features with a runtime surface
get driven end-to-end before shipping.
tests/scripts/holds Playwright verification scripts; heavy loops run against the localdocker composeDB, not Supabase (egress throttling is real and will eat your afternoon). - The graph has an explicit perf envelope (20k nodes at interactive frame rates on integrated GPUs) with budget constants and console tripwires — if you touch the payload or reducers, re-verify against the seeded 20k dataset, and remember sigma reducers must return total objects.
AI-call rules
- Extraction/parsing: the small model tier; search reasoning/drafts: the mid tier; nightly jobs: Batch API. Every AI feature is metered per user.
- System prompts: stable cacheable prefix, volatile content (user data, type
registries,
todayLine()) last — never in the cached prefix. - Every prompt that reasons over user notes includes the anti-fabrication line: if it's not in the notes or graph, say so — never invent.
Where to learn more
- Product & vision — product requirements, roadmap, cost model
- Build checklist — feature-by-feature build status (verify claims against code; checkboxes drift)
- Self-hosting · Deploying · Testing
CLAUDE.md— the AI-assistant contract for this repo; it's also a fair summary of how maintainers think, and the hooks in.claude/settings.json(150-line check, post-edit typecheck) apply to anyone using Claude Code here.
Testing
A manual, step-by-step guide for taking Dhaga from zero to a fully clicked-through app — first deploy, first account, first admin, and every shipped product feature.
Business Requirements
The full BRD — problem, competitive landscape, MVP vs full-product scope, technical architecture, the open-source strategy, and the cost model.