Build checklist
Every feature, big and small, with its verified/unverified build status. Checkboxes drift — verify claims against code.
Exposes build status
This is the internal feature-by-feature build tracker. It reveals what's shipped, what's only code-complete, and where the known gaps are. Items are checked only when the code is written, verified (lint + build + manual run), and pushed — but checkboxes drift, so verify against code before relying on them.
Derived from BRD. Every feature, big and small. Items get checked
([x]) only when the code is written, verified (lint + build + manual run), and
pushed. Partial work stays unchecked with a note.
Legend: (M#) = BRD MVP feature · (v1.x) = BRD roadmap phase
0. Foundation
- Git repo + GitHub (
anchit2000/dhaga), AGPL-3.0 LICENSE, README - CLAUDE.md project rules (stack, SOLID, security, file organization)
- Next.js app scaffold (
apps/web) — App Router, TS strict, Tailwind v4 - Design tokens centralised (
globals.css@theme: ink/panel/seam/paper/fog/amber) - Landing page (hero, scrollytelling feature story, pricing, FAQ, OSS section)
- npm workspaces (
apps/*,packages/*) sopackages/coreis shareable -
apps/web/.env.exampledocumenting every env var - CI (typecheck + lint + tests + build on push)
- Test suite (vitest, in-memory PGlite): heuristic parser, export formats, receipts cascade
- Fix Dependabot alert (postcss
<8.5.10via next — npm override to ^8.5.16) - Deploy to Vercel (landing + app)
1. Shared core — packages/core
- Zod contact schema (name, title, company, emails[], phones[], links, location)
- Zod extraction schema (facts, relationships, follow_ups, tags) per BRD §6.3
-
LLMClientinterface (Dependency-Inversion contract) -
AnthropicLLMClient— structured outputs via Zod-derived JSON schema - Prompt builders (pure functions): contact parse, note extraction
- Prompt builders: search answer, follow-up draft
- Prompt builder: search query understanding (structured filters stage)
-
getLLMClient()factory (env-driven; Ollama/BYO-key = future implementations) - Heuristic (no-LLM) contact parser fallback — email/phone/URL regex + name lines
- Shared API types (request/response contracts used by web now, mobile later) —
packages/core/src/api/{capture,contacts,export,card-image,access-requests,jobs}.ts; each routesatisfiesits contract (2026-07-07);auth/[...all],stripe/webhook, andtelegramintentionally left untyped (better-auth's own catch-all, a Stripe-shaped webhook receiver, and an internal bot webhook — none are a shared web/mobile contract surface), not pushed
2. Web app shell (v1.1 surface, built first)
- Auth: real accounts (better-auth email/password), signed httpOnly session cookie — typecheck/lint/build/test all pass; manual browser click-through + push still pending
- Every
/apppage + server action validates the session (guard helpers) - App layout: nav (People / Events / Search / Quick-add), mobile-first at 375px
- Empty states + error states; submit buttons have loading spinners
- Loading skeletons on data-heavy screens (route
loading.tsxfiles) - Dark warm theme reused from landing tokens
- Home dashboard: due reach-outs + open follow-ups across the graph
- Fix render-blocking font/animation on first load (BRD §7.6) — Geist Pixel self-hosted via
next/font/local(was adisplay=blockGoogle Fonts<link>, invisible text until it loaded); landing WebGL cursor + GSAP scroll thread deferred vianext/dynamic({ ssr: false })so they no longer block first paint — 2026-07-12, build verification + push still pending - Cache authenticated
/app/*navigation so switching pages doesn't re-run the full Postgres query set on every click (BRD §7.6) — per-user scoped, invalidated on mutation, not a raw TTL
3. Data layer (BRD §7.4 — boring storage)
- PGlite (embedded Postgres) + Drizzle; hosted Postgres = driver swap
- Hosted Postgres support:
DATABASE_URL→ node-postgres (unlocks Vercel) -
contactstable -
companiestable -
events+event_contactstables (M2) -
notestable (kind: voice|text|capture_source, body) -
factstable (type, text, confidence,source_note_id,deleted_at) -
edgestable (src/dst typed, predicate,source_note_id) -
embeddingstable (pgvector, 384-dim, receipts via owner_type/owner_id) -
follow_upstable -
ai_actionsmetering table (day one requirement) - Deletion cascade: contact → notes → facts → edges → embeddings ("forget this person") —
forgetContactalready did this correctly (repo/contacts/mutations.ts) - Note deletion tombstones derived facts/edges (receipts invariant) — embeddings cleanup moved into
deleteNoteitself (2026-07-07, was only in the action layer, so any other caller skipped it);graph-receipts.test.tsnow asserts embeddings are gone, not pushed
4. Capture — web quick-add (v1.1, M1-equivalent for web)
- Paste email signature / free text → extracted contact (LLM, heuristic fallback)
- Card photo scan (M1 web path): phone camera/upload → vision parse → review → receipt
- Card photos stored as visual receipts (user's own DB — local or hosted), shown on the contact page
- Settings page: per-user "store card photos" toggle + purge-all button
- Photo deletion cascades: gone with its receipt note, gone with "forget this person"
- Edit-before-save review form (M1 acceptance: user confirms fields)
- Assign capture to an event (create/pick "Web Summit 2026")
- Attach source text as first note (receipt for extracted fields)
- Manual add-contact form (no extraction path)
- People list with filter + contact detail page
- Company auto-link: extracted company name → find-or-create
companiesrow - User-triggered enrichment: web search → cited enrichment note → receipted facts
- LinkedIn Connections CSV import — user's own LinkedIn data export → bulk contacts, ToS-safe (BRD §6.7) (v1.1);
lib/import/linkedin.ts, wired into/app/import, LinkedIn header format covered bycsv-import.test.ts/import-repo.test.ts - Waitlist signups get a confirmation email (Resend)
5. Events / auto-grouping (M2)
- Events list + event page (contacts met there)
- Create events (from Events page and inline in quick-add)
- Rename/merge events
- Web: active-event default (an event started today is preselected in quick-add)
- Mobile: time + geohash clustering per BRD §6.2 —
packages/core/src/geo/geohash.ts+apps/web/src/lib/repo/event-clustering.ts(geohash-6, 4h window, "Name this event?" prompt on new cluster); typecheck/lint/build/tests all pass (a346516), not pushed; needs on-device verification (no EAS/device access here)
6. Notes & entity extraction (M3, M4)
- Text notes on a contact (add, list, delete)
- Extraction: note → facts/relationships/follow-ups/tags (Haiku, structured output)
- Facts render on contact page with receipt ("from note, {date}")
- User can edit/delete a fact inline (M4 acceptance)
- Deleting a note tombstones its derived facts/edges
- Voice notes on web (browser SpeechRecognition → transcript → extraction)
- Voice notes on mobile (whisper.cpp / Apple Speech — mobile milestone) — built via
expo-speech-recognition(wraps iOSSFSpeechRecognizer/AndroidSpeechRecognizer, forced on-device, no audio/transcript leaves the phone); tap mic → live interim transcript fills the same text-review box typed input uses (e64b336). Typecheck/lint pass; package has no published SDK-57 tag yet (installed cleanly, types check, but native linking in an EAS/dev-client build is unverified) — needs a real device build before this can be checked off, not pushed
7. Knowledge graph v0 (M5)
- Relationship edges written from extraction (
works_at,knows,used_to_work_at, …) - Contact page: same-company + same-event + edge connections render
- Graph browser page (React Flow: people/companies, typed edges, ring layout)
- Tag ontology v0 (tags from extraction, filter chips on People)
8. Natural-language search (M6)
- Keyword + structured search (SQL ILIKE over contacts/notes/facts) — free path
- Query understanding: LLM → structured filters + semantic residual (Ask AI stage 1)
- Embeddings + pgvector similarity (bge-small via transformers.js — local, $0)
- "Ask AI" answer over retrieved candidates with receipts (Sonnet, explicit click)
- Acceptance: seeded test set — correct contact in top 3 (vitest, keyword path; semantic covered by the standalone E2E check)
9. AI follow-up drafts (M7)
- One-tap draft using notes + event context + facts (Sonnet, cache-friendly prompt)
- Draft references ≥1 note-derived fact (acceptance — verify with a live API key)
- Edit + copy-to-clipboard flow
10. Metering, cost control (BRD §8.2–8.3)
- Every AI call logged to
ai_actions(feature, model, tokens in/out) - Free-tier cap enforced (25/month,
DHAGA_AI_MONTHLY_CAPoverride) with clear UI message - Prompt caching markers on stable system prompts (engages once prompts exceed the model's cacheable minimum)
- Batch API for nightly jobs —
lib/jobs/detect-signals/(§14) is now a two-phase job through Anthropic's Message Batches API (BatchLLMClient,packages/core/src/llm): each run applies the previous run's finished batch (dedup + signal insert + metering, unchanged) then submits one fresh batch for whatever's newly due, persisting the pending batch id viasettings(getPendingSignalBatchId/setPendingSignalBatchId). Vercel Hobby's once-daily cron means a contact's signal now lands with a ~1-day lag instead of immediately — accepted tradeoff for a job that was already nightly and latency-insensitive. Typecheck/lint/tests pass (signal-detection-batch.test.ts); no live-API manual run
11. Privacy & export (M8, BRD §7.5)
- "Forget this person" — full cascade delete with confirmation
- Export: contacts CSV
- Export: vCard
- Export: full JSON dump (contacts+events+notes+facts+edges)
- No contact PII / transcripts / extraction output in server logs
- Enrichment & cloud AI strictly user-triggered (no background calls)
12. Mobile app — apps/mobile (BRD MVP platform; separate milestone)
-
PWA: installable web app (manifest, standalone display, brand icons) — the interim mobile surface
-
Per-user API keys (better-auth
apiKeyplugin) for non-browser clients — replacesDHAGA_API_TOKEN; code confirmed fully wired end-to-end (2026-07-07 audit):apiKeyplugin, settings UI,/api/captureauth fallback all connected,DHAGA_API_TOKENno longer referenced anywhere in code. Only remaining bar is a human clicking through the settings UI in a browser -
/api/captureaccepts card images (base64) — ready for the Expo app to call -
M0 spike: Expo app, camera → Vision/ML Kit OCR → Haiku parse → contact saved — built 2026-07-04 (
apps/mobile: Expo SDK 57, dev-client,expo-text-extractoron-device OCR primary + server photo-scan fallback,x-api-keyauth); typecheck green; needs on-device verification on Android + iPhone (seeapps/mobile/README.md), not pushed -
M1 card scan: edit-before-save, ≥90% accuracy on clean cards, under 5s
-
M2 auto event grouping (time + geohash clustering, name-once prompt)
-
M3 voice notes: whisper.cpp / Apple Speech on-device transcription — built via
expo-speech-recognition(§6 has details); typecheck/lint pass, needs a real device build to confirm native linking on SDK 57, not pushed -
M8 local-first SQLite (op-sqlite) + sqlite-vec; full offline
-
Sync engine (field-level LWW or PowerSync/ElectricSQL — decide per BRD §11)
-
E2E-encrypted backup/sync
-
EAS builds: .aab (Play) + .ipa (App Store); store listings
13. Browser extension (v1.1)
- Chrome/Edge extension (MV3): popup captures page selection →
/api/capture - One-click capture from any page (activeTab on explicit click only, page URL as receipt)
-
/api/captureREST endpoint (session-gated; shared by extension + future mobile share) - "Save this article to {contact}": attach mode in the popup + contact search API
- Web Store packaging/listing (privacy policy page now live at
/privacy) — further along than it looks: build/zip pipeline exists (apps/extension/build.mjs), a pre-built.zipandSTORE_LISTING.mdwith listing copy are ready; only remaining blockers are a reviewer demo account + the $5 Chrome dev registration fee (both manual/external, not code)
14. Proactive intelligence (v1.2)
- Job-change detection — simplified to web-search-only per product decision (2026-07-05), superseding the CSV-diff mechanism in BRD §6.7: shares the signal-detection sweep with the news watchlist below,
kind: "job_change"when the search results show a different title/employer than what's on file. Typecheck/lint/build/tests pass; manual click-through needs a realFIRECRAWL_API_KEY+ANTHROPIC_API_KEY, not done - Opt-in news watchlist: per-contact "Watch for job changes & news" toggle (contact page) → nightly cron (
/api/jobs/detect-signals,apps/web/vercel.json) → provider-agnostic web search (packages/core/src/search, Firecrawl by default — cheaper per-search than an LLM's own web-search tool) → Haiku classifies hits →signalstable → Home "Signals" feed + contact page, "Add as note" (receipted) or dismiss. Capped per plan (PRO_TIER_WATCHLIST_CAP/FREE_TIER_WATCHLIST_CAP). Typecheck/lint/build/tests pass (signals.test.ts,search-client.test.ts); manual click-through not done (needs live API keys) - Keep-in-touch cadence reminders + Home reach-out feed (ideas.md #2)
- Automatic relationship-decay detection — built read-time (no nightly job needed): "Going quiet" feed on Home surfaces contacts with no touch in ~8 months and no cadence set (
repo/strength.ts); typecheck/lint/build/tests pass, manual click-through not done - Relationship-strength score from own-graph data (interaction recency/frequency, notes, events — no external data) — built: 0–100 recency×frequency score (
scoreStrength), ranks the Going-quiet feed strongest-first; tests instrength.test.ts, manual click-through not done - Post-event digest email (user-triggered from the event page, template-based)
- Pre-meeting brief on demand ("Brief me ✦" on the contact page, graph-only)
- Calendar integration → brief pushed 30 min before the meeting
15. Graph power (v1.3)
- Warm-path finding (BFS over edges/companies/events — no AI cost)
- Second-degree suggestions ("Nearby in your network" — ideas.md #1, local-only traversal)
- Relationship timeline view (captures, events, notes, touches on the contact page)
- Watch app / widgets
16. Ecosystem (v1.4)
- Salesforce/HubSpot/Notion export-sync
- Outbound webhooks (contact.created, followup.created →
DHAGA_WEBHOOK_URL) - Telegram bot: capture + ?questions from chat (ideas.md #6; owner-only, secret-verified)
- WhatsApp capture (needs Meta business API)
- LinkedIn QR format support — pure matcher
packages/core/src/capture/linkedin-qr.ts(unit-tested,apps/web/src/lib/__tests__/linkedin-qr.test.ts); web:QuickAddForm/PhotoCaptureInput.tsxdetects via BarcodeDetector and routes to/app/people/new?linkedin=(prefills the existing manual-add form, no auto-create); mobile:camera-capture-view.tsxscans live via expo-camera,LinkedInQrPrompthands off to the web form viaLinking.openURL(mobile has no manual-add screen of its own to route to — see docs/notes, this is the closest honest equivalent). Typecheck/lint/test all pass; manual browser + device click-through still pending - Email/calendar interaction sync (Gmail/Outlook OAuth, explicit opt-in) — the one ToS-clean ambient-capture channel (BRD §6.7)
17. Teams (v2.0 — revenue engine)
- Org workspace, contact-level sharing controls
- "Who knows whom" across team
- SSO; per-seat billing
18. Monetization & launch
- Access-request API (
/api/access-requests,packages/ee) wired to real storage; landing form posts to it — replaces the old public waitlist, gated behindDHAGA_HOSTED_MODE; code confirmed backed by a real Drizzle/Postgres table (2026-07-07 audit), no stub — only remaining bar is a human clicking through the flow in a browser - Free tier caps live; Pro (lifetime $79 first-500 / $8 mo) checkout — Stripe Checkout + webhook built (§19); real Stripe Product/Price setup + a live test-mode purchase still needed
- Self-host docs — Self-hosting (no
packages/eeneeded) -
docker compose up— multi-stageDockerfile(node:22-slim, standalone output viaDHAGA_STANDALONE=1, non-root, 494MB) +compose.yml(pgvector/pgvector:pg16 db with healthcheck) verified end-to-end 2026-07-16: Postgres-backed and zero-config PGlite boots both serve with clean first-boot DDL self-heal (the earlier single-stage image was never actually runnable — missing workspace manifest) - Public roadmap + good-first-issues —
docs/ROADMAP.mdwritten 2026-07-07, now linked fromREADME.md's Status section (2026-07-12); good-first-issue candidates drafted but intentionally not posted as real GitHub issues yet (that's a public/outward action for the owner to approve) - Replace randomuser.me landing portraits with licensed photos before paid marketing
19. SaaS platform — accounts, multi-tenancy, billing, admin (Dhaga Cloud)
Everything in this section is typecheck/lint/build/vitest-clean (including a
build with packages/ee and its exclusive route folders physically removed,
confirming the AGPL core doesn't need it — as of 2026-07-07 this is enforced
on every push by the verify-without-ee CI job, not just a one-off manual
check) but not yet manually click-tested in a browser or pushed — the
checklist bar for [x] needs both, so these stay unchecked until that
happens. A 2026-07-07 code audit confirmed every item below is backed by
real, substantive implementation (RLS policies, Stripe integration matching
the "4 event types" claim exactly, real notFound() admin gating) — none of
this is a stub, the only gap is the manual click-through + push.
- Real accounts (better-auth email/password) replacing the single shared
DHAGA_PASSWORD;/login+/signup— flagged 2026-07-07: this is the same code/checkbox as §2's "Auth: real accounts", not a distinct multi-tenant implementation (DHAGA_PASSWORDdoesn't exist anywhere in code, only in this checklist); consider deleting one of the two entries once confirmed there's no separate hosted-mode acceptance bar - Per-user personal access tokens (better-auth
apiKeyplugin) replacingDHAGA_API_TOKEN, settings UI to create/revoke — flagged 2026-07-07: duplicate of §12's "Per-user API keys", sameapiKeyplugin/settings UI, not SaaS-specific - Multi-tenant data isolation via Postgres Row-Level Security, entirely in
packages/ee— zero query-logic changes inapps/web/src/lib/repo/* - Open-core boundary:
packages/ee(source-available, PolyForm Shield 1.0.0 — seepackages/ee/LICENSE), self-host runs fully without it (SELF_HOSTING.md) - Early access: public request form → admin approve/reject → gated signup (
DHAGA_HOSTED_MODEonly; inert and 404s otherwise) - Admin panel (
/app/admin): dashboard, access requests, users, subscriptions — 404s for non-admins - First-admin bootstrap via
DHAGA_ADMIN_EMAILS(seeSELF_HOSTING.md) - Stripe billing: Checkout (Pro/Lifetime), billing portal, webhook (4 event types), AI-cap bypass for paid users
- Billing UI cleanly absent (not broken) when
STRIPE_SECRET_KEYis unset, even in hosted mode