Databuddy Internal
Databuddy is a Bun + Turborepo TypeScript monorepo. Start by locating the user request in one product surface, then trace its shared dependencies before editing.
For external integrations (SDK, CDN, public APIs), use the databuddy skill; this skill is for this repository.
Skill maintenance (required)
When a mistake could have been avoided with better repo context (wrong app, package, port, or pattern), or when the user corrects you or asks you to fix something you got wrong, update this skill (SKILL.md or references/codebase-map.md) in the same turn when practical.
Keep additions minimal: one bullet, a new rg hint, or a routing note—enough that the next session does not repeat it. If the lesson is for SDK/API customers, add it under .agents/skills/databuddy/ instead.
- Repo-local
.agents/skillsis versioned project guidance. Put new personal or experimental agent skills under/Users/iza/.agents/skillsunless the user explicitly wants the skill committed with Databuddy.
Quick Map
- Prod infrastructure repo is local at
/Users/iza/Documents/GitHub/databuddy-infra(databuddy-analytics/infra); ClickHouse cluster inventory isclickhouse/ansible/inventory.yml, not/Users/iza/Dev/Databuddy/infraorDatabuddyOPS. - Never use production/customer data as tests, fixtures, snapshots, examples, or copied output. Tests must use placeholders/mocks only (example.com, example IDs). If production ClickHouse is queried for investigation, summarize anonymized aggregates and do not paste customer domains, client IDs, emails, or other identifiers into code or responses.
@databuddy/test/envtargets localdatabuddy_testunlessCI=true, so a normaldb:pushmay update a different database; sync that test database explicitly before debugging removed-column failures.apps/dashboard: Next.js app on port3000(per-website agent chat:@ai-sdk/reactuseChatviacontexts/chat-context.tsx— not the separatechat-sdkpackage; overlapping sends while streaming are queued client-side to mirror a “queue latest” strategy.)- Dashboard Playwright webServer commands run under CI PATH from setup-bun; avoid
bash -lcbecause login shells can drop Bun from PATH. Build dist-only workspace packages such as@databuddy/sdkand@databuddy/devtoolsbefore starting the API/dashboard. ClientNEXT_PUBLIC_*flags must use direct env access so Next can inline them.readBooleanEnvonly treats the literal string"true"as enabled, so CI E2E booleans must use"true"/"false", not"1"/"0". - Dashboard pages that call
useSearchParams()must keep that call inside a Suspense-rendered child; run the production dashboard build after adding URL-driven onboarding state. - The local
.envsetsNODE_ENV=development; override it withNODE_ENV=productionwhen verifyingnext build, or root-error prerender behavior can produce a misleading local failure. - Local E2E dashboard smokes that need
/api/test/e2e/*should start the API/dashboard directly (or through Playwright's webServer command), not viabun run dev:dashboard; Turbo runs in strict env mode and dropsDATABUDDY_E2E_MODE/DATABUDDY_E2E_TEST_KEYunless they are added toturbo.jsonglobalEnv. - Dashboard Playwright public/demo analytics specs call API
/v1/queryanonymously from the browser; keepDATABUDDY_E2E_MODEquery behavior isolated from production rate limits so CI retries do not exhaustanon:unknown. apps/api: Elysia API on port3001- Public REST docs live in
apps/api/src/rpc/openapi.ts:/spec.jsonis the generated spec,/is the reference UI, and hiding a router there also makes its top-level REST paths return 404 because/*uses the same filtered docs router. apps/slack: Slack agent adapter; Slack installs resolve through org-scoped DB integration records, not a single env bot token/default website. Agent calls use the org-scoped internal principal synthesized from the active integration inslack/installations.ts, never a global internal secret.- Slack OAuth lives in
apps/api, but slash commands/events requireapps/slackto be running too; localbun run dev:dashboardruns dashboard + API only, so usebun run dev:slackwhen working on Slack. The Slack package scripts read the root.env. - Run Slack tests through
bun run testinsideapps/slack; the package script supplies only the inert Redis URL needed by eager shared imports. - Slack routing is organization-scoped: OAuth binds a Slack workspace to a Databuddy organization, app mentions from the installed workspace auto-bind channels including Slack Connect, and
/bindis now a manual fallback for unknown/unapproved channels. DMs/assistant threads work after workspace install. Analytics questions should go through app mentions/DMs using MCP-style website discovery inside the installed organization, never by fanning out across the message sender's user memberships. Slack emits evlog events underapps/slack/.evlog/logsin development/SLACK_EVLOG_FS=1; Axiom usesAXIOM_TOKENand theslackdataset; reactions need thereactions:writebot scope. - Slack scope changes require reinstalling/reauthorizing the workspace; updating the local/remote manifest alone does not grant newly-added bot scopes to an existing installation.
- Slack agent billing flows through an org-scoped automation API key; existing keys may have
userId: null, so the agent billing resolver must fall back to the organization owner when an API key hasorganizationId. - Slack memory is separate from billing/auth: pass a Slack-scoped
memoryUserIdsuch asslack-{team}-{user}plus current-speaker context so one Slack user's saved name/preferences do not bleed into another user's replies. - Slack agent tools use the scopes on the internal principal in
slack/installations.ts; changing those Databuddy scopes applies without Slack reauthorization. Only changes to Slack OAuth scopes require reconnecting the workspace. - Shared agent integrations should call
@databuddy/ai/agent(askDatabuddyAgent/streamDatabuddyAgent) instead of importing internal MCP run/history helpers directly. - First-party ads attribution work should start by preserving UTMs into registration and signup events only; do not add RPC plumbing, conversion destinations, env hooks, tables, workers, or UI until explicitly needed.
- Insights generation logic belongs in
apps/insightsand should reuse@databuddy/ai;apps/apishould only read insight data or queue runs, not own prompts, model calls, tool loops, validation, or persistence orchestration. SPEC.mdis the intelligence product contract.insight_observationsis the readable Insights history;analytics_insightsis the durable investigation projection. The agent outcome owns brief publication andact/askpromotion; do not replace either with frontend heuristics or collapse the feed into cases. Do not add a parallel agent, evidence API, fixed query choreography, or action-specific lifecycle.- Insights quality reviews must compare fresh baseline/candidate outputs and lead with the product verdict and concrete examples. Score usefulness, noise, reading effort, and retained useful findings separately from code tests and contract passes; preserve interrupted attempts instead of reporting retries as an uninterrupted pass rate.
- Insights RPC helpers that take
{ context, ...input }must stripcontextbefore parsing a.strict()Zod input schema (same pattern asappendInvestigationReply/applyInsightGoalAction); otherwise CI fails withUnrecognized key: "context". insights.history/ MCPlist_investigationshide cases while a reply isqueued/running(action-inbox verification); tests must list before reply or expect an empty list while verifying.- When reporting what an organization can see in Insights, follow the
insights.brief/historyvisibility rules instead of countinganalytics_insights; the projection can contain legacy rows without a readable or publishedinsight_observationsturn. - Production insight shadows must freeze
--reference-time, retain a tool-name trace, and pass available GitHub context before supporting quality claims. Postgres and ClickHouse are read-only, but connector token refreshes or cache writes can still occur; never describe the whole run as zero-write. - Automatic investigations have one organization-wide schedule (
off,daily, orweekly) and one organization-wide delivery set; website selection is only for manual runs. Do not reintroduce per-website overrides, hourly/custom cadence, or cron input. - A manual insight run is a deliberate recheck: it bypasses automatic cooldown only for currently detected signals, while retaining detector thresholds and normal signal ranking. Otherwise “Run now” can complete without producing an evaluable result.
- Insight run items are execution metadata, not rendered insight content; previews should use run status/counts or query real insights, never infer titles or bodies from run items.
- Insight Slack delivery must resolve each channel binding to its active same-organization integration; never choose an arbitrary organization bot token.
- Replies beneath delivered Slack investigations must resolve the delivery and enter the existing durable reply/resume path; never route them through generic Slack chat or relevance scoring.
- One-off insight previews must preserve the real signal entity and use customer-facing product output. Never hand-write Slack copy from eval metadata or expose evaluation and suppression mechanics.
- Agent ClickHouse SQL must use the canonical analytics.events schema:
client_id,time,path,event_name, and pageviews asevent_name = 'screen_view'; neverwebsite_id,created_at,page_path,event_type, orpageview. - Agent
get_datafilters select rows; do not expose SQL CTEtargetorhavingas generic event/error scopes. Query discovery supplies accepted selectors, and result row counts describe the query output rather than a complete population. - Slack agent expected stops such as exhausted Databunny credits should throw
DatabuddyAgentUserErrorfrom@databuddy/ai/agent/errors; Slack surfaces those messages directly and reserves the generic reconnect copy for real infrastructure failures. - Slack Docker builds use
bun build --compile --bytecode; keepapps/slack/src/index.tsbootstrapping inside an asyncmain()instead of top-levelawait, which can fail during compile even when typecheck passes. - Insights Docker builds also use
bun build --compile --bytecode; keepapps/insights/src/index.tsstartup work inside async functions instead of top-levelawait. - After Slack Docker changes, verify the full pruned image with
docker build --progress=plain -f slack.Dockerfile -t databuddy-slack:test .; the inner Bun compile is not enough because prune can miss dependency build outputs and package exports. - Slack-reachable shared packages (
@databuddy/ai,@databuddy/rpc) must not importevlog/elysia; use host-injected request logger providers from the API and plain evlog fallbacks elsewhere. - AI link tools must assign link folders by existing folder
idorslugonly; folder names are display text and must not be used for routing or dedupe. apps/basket: ingest and LLM tracking service, Elysia app on port4000- Basket tests use Vitest; run them through
bun run testinsideapps/basket, notbun testdirectly. apps/docs: Next.js + Fumadocs docs app on port3005- When a user drops a prototype, remove only prototype-specific wiring and preserve the existing product surfaces it temporarily reused.
apps/links: redirect/link serviceapps/uptime: uptime monitoring serviceapps/uptimeBullMQ worker concurrency defaults high for Bun async I/O; do not lower it just because10_000looks large. Verify downstream saturation or lock/timeout evidence first.- Public status pages render from
apps/status;apps/dashboardowns status-page management/config UI only. When cleaning public status UX, update shared@databuddy/ui/uptimepieces orapps/statuswrappers instead of redesigning dashboard-only route remnants. packages/db: Drizzle Postgres schema, client, and ClickHouse helpers- Keep Bun-only DQL provisioning code off the
@databuddy/db/clickhousebarrel; dashboard Next routes run under Node and import the shared ClickHouse surface. packages/rpc: shared oRPC router, procedures, auth-aware server contextpackages/rpcmust declaredrizzle-orm: "catalog:"before importingdrizzle-orm/*helpers such asdrizzle-orm/zod; otherwise TypeScript can resolve a different Drizzle instance than@databuddy/dband reject table-derived schemas.packages/auth: Better Auth setup, permissions, organization accesspackages/env: shared URL, public, and boolean environment helperspackages/shared: shared types, flags, analytics schemas, utilities- Analytics query builders live in
packages/ai/src/query; there is no standalonepackages/querydirectory. Tests are excluded from root Biome checks, so format changed test blocks explicitly. apps/insightsuses the ES2022 TypeScript library; Bun supportingfindLastor newer Set methods does not make those APIs typecheck here. Check the package TS library before choosing newer built-ins.packages/sdk: published analytics SDK for React, Vue, and Nodepackages/tracker: internal tracker script build and release packagepackages/encryption,packages/notifications,packages/cache,packages/redis,packages/services,packages/validation,packages/api-keys: shared infra and domain packages- Knip is configured in root
knip.json(runbun run knip); per-workspace test globs are required because the roottest:watch(bun test --watch ./apps) script shadows the Bun plugin's per-workspace script parsing;apps/cronis ignored (standalone scripts, no package.json)
Read codebase-map.md when you need deeper routing guidance.
Workflow
- Identify the runtime surface first: dashboard UI, API, ingest pipeline, docs site, tracker, or shared package.
- Read the owning package's
package.json, entrypoint, and direct dependencies before changing code. - If the change crosses app boundaries, trace the contract:
dashboard -> apps/dashboard/lib/orpc.ts -> packages/rpc -> apps/api - If the change touches analytics ingestion or LLM observability, trace:
packages/sdkorpackages/tracker->apps/basket->packages/db/ ClickHouse - If the change touches auth, org permissions, or session-aware server behavior, inspect
packages/authandpackages/rpctogether. - Validate with the smallest relevant command instead of running the whole monorepo by default.
Repo Conventions
- Package manager:
bun - For ad-hoc read-only data scripts, run from an owning package and import the client through
@databuddy/db; a root-level Bun eval cannot resolve workspace or transitive dependencies directly. - When running
bun install --lockfile-only, preserve lockfile sync for pre-existingpackage.jsonchanges instead of reverting them as unrelated. - Task runner:
turbo - Run filtered Turbo commands such as
bun run check-types --filter=…from the workspace root; inside a package, its local script invokestscdirectly and treats those flags as TypeScript options. - Formatting/linting:
bun run format,bun run lint - Use neutral branch names, commit messages, and PR copy; do not include tool-attribution prefixes or generated-by language.
- When the user names an integration branch such as
staging, verifyHEAD, the intended commit set, and upstream divergence on that branch before declaring the work complete. - Lefthook's
no-secretsguard intentionally ignores the exact.env.exampletemplate; real.env,.env.*, key, and credential files should still be blocked. - Root dev orchestration:
bun run dev - Dashboard + API together:
bun run dev:dashboard - Tests at root currently target
./apps:bun run test - Package test mocks can leak across files in one Bun process; when
packages/aibroadly mocks@databuddy/redis, keep the mock export surface in sync with runtime imports from shared RPC/tool code. - Database scripts are routed from root into
packages/db - Runtime environment reads stay in the owning service; shared URL/public helpers live in
packages/env - BullMQ queues use
BULLMQ_REDIS_URL; generic Redis cache/pubsub code usesREDIS_URL.
Code Standards
- Keep one source of truth. If output is AI-generated copy, semantic labels, summaries, or recommendations, fix the upstream prompt/schema/validation contract; do not patch it later with frontend regex/string heuristics.
- When an undescribed goal or funnel combines unrelated workflows, explain its broad scope and end a published insight with one concrete evidence-backed edit, rename, create, use, or cleanup recommendation even when resolving it. Recommend deletion only when evidence shows duplication or no distinct use. An explicit purpose/config mismatch is material decision harm and becomes an action; ambiguity alone does not.
- Start small backend slices with one implementation file per owning package; split only when a concrete independent consumer or lifecycle appears.
- Use deterministic transforms only for deterministic data: stable enums, IDs, namespaces, routes, schema fields, and typed status values. Do not guess meaning from free-form model/user text with regexes.
- Prefer structured contracts over text parsing. If the UI needs a label, action, link, severity, or metric category, add it to the schema/tool output and validate it at the boundary.
- Keep domain concerns at the owning seam. Routers/UI should call domain/service helpers, not know cache keys, raw Redis patterns, billing internals, or provider-specific lifecycle details.
- Prefer direct, boring code. Use typed registries and small local helpers when they delete duplication; avoid generic job/facade abstractions, labeled pipelines, or framework-y wrappers unless they clearly reduce code and concepts.
- Test invariants and contracts, not implementation trivia. Add guard tests for architectural rules only when they prevent repeat classes of bugs.
- During cleanup, do not add per-consumer tests or test-only modules for schemas already covered at their shared boundary; keep one real end-to-end boundary regression when wiring itself changed.
Change Routing
Dashboard work
- Start in
apps/dashboard - Organization settings should match
organizations/components/general-settings.tsx: the samemax-w-2xlcolumn, sharedCardheaders/content, compact fields, andTopBar.Actionsfor Save. Compare actual neighboring pages visually before claiming design consistency; shared inputs alone are not enough. - The Feature Flags list is a dense data table; keep flag identity metadata clean and place activity telemetry in a dedicated labeled column with its accuracy caveat in the Activity header tooltip.
- Keep large page-owned settings sheets and dialogs in adjacent feature files; route components should own page data and layout rather than embedding unrelated form lifecycles.
- For dashboard navigation audits, check all route surfaces:
components/layout/navigation/navigation-config.tsx,components/ui/command-search.tsx, and localPageNavigationlayouts underapp/**/layout.tsxbefore calling a page orphaned. - When fixing broken dashboard links to moved sections, update the real docs/search/navigation links and section anchors directly; do not add compatibility redirect pages unless explicitly requested.
- Custom events UI is shared in
apps/dashboard/components/events/custom-events; keep many-series legends outside the Recharts plot, use compact controls for property-summary event selection, and avoid separate event-count chip/list sections. - Goals and Funnels are sibling conversion surfaces; keep Goals list-first and visually aligned with
app/(main)/websites/[id]/funnelsinstead of adding separate summary-card chrome. - Funnel rows keep the action menu outside the main toggle button; put row padding on the sibling
Button, not only onList.Row, so the visible row surface is clickable without nesting buttons. - Demo website navigation must be public-safe and route-backed; hide sensitive, configuration-heavy, or unavailable website features such as Agent, Feature Flags, Revenue, Users, Realtime, and website Settings instead of inheriting the full website nav. Goals and Funnels may be public demo surfaces, but keep them read-only.
- Dashboard definitions for feature flags and target groups are admin surfaces; do not expose even sanitized rows to demo-tier/public website access.
- Insights history is grouped by its backend-owned subject key in the RPC layer so every client sees one current row per investigation; reads must not invoke AI generation.
- An executable Insight action must apply its stored mutation and enqueue verification transactionally; never ask a teammate to mark it done manually. Show a verified result only after measured evidence supports it.
- Insights case pages should not explain their own structure (for example, "context, investigation, and verification in one thread"). Keep only evidence, current state, and actionable controls visible.
- Insight visual cleanup must preserve scan hierarchy: retain the compact visual/status anchors and clearly separated proof on dense feed rows; remove redundant explanatory copy before flattening the information structure.
- Theme:
apps/dashboard/app/globals.css.--borderis intentionally subtle; do not crank it darker for “contrast” unless iza asks—prefer text tokens or layout for readability. - Website analytics filters are two-way synced between Jotai and the
filtersURL param inapp/(main)/websites/[id]/layout.tsx; guard URL-driven atom writes from echoing stale atom state back intonuqs, or adding a filter can lock the page during form submit. - Do not centralize, relocate, or otherwise refactor dashboard E2E API route access gates during cleanup; keep test-only access checks local to each route unless iza explicitly asks for that change.
- Integration catalog logos: use filled Simple Icons SVG path data (or equivalent filled brand SVG), store the path on each item as
iconPath, render it through a shared logo tile withbg-secondary/60,border-border/70,text-foreground, andfill="currentColor", then use brand color only as a small accent bar (accentoraccentClassName: "bg-foreground/70"for black/near-black brands). Avoid raw brand-black icons or mixed line/filled icon sets that disappear in dark mode. - Organization integrations settings should stay list-first and operational: coming-soon integrations are static rows, Slack is the only expandable row for now, and connected integrations need obvious lifecycle controls such as uninstall/disconnect in the row details.
- MCP setup UI should mirror the governed write metadata in
packages/ai/src/ai/mcp/tools.ts: default toread:data, then expose explicit action bundles for workspace actions, feature flags, and short links with their required scopes and confirmation behavior. - Dashboard primitives now live in
packages/ui/src/componentsand are consumed through@databuddy/ui(@databuddy/ui/clientfor client-only components); the oldapps/dashboard/components/ds/README.mdpath no longer exists. Read the corresponding shared component implementation before extending UI. Dashboard UI must use these shared primitives exactly; feature code must not use raw form/control elements (button,input,select,textarea, native dialogs), Base UI/Radix primitives, or ad hoc styled controls directly. If a variant is missing, add or extend the DS component first. For menu-style folder/status/filter/sort/action pickers, usecomponents/ds/dropdown-menu.tsx; useSelectonly when the established pattern is explicitly a select/combobox. Inspect the shared component APIs inpackages/ui/src/componentsbefore creating new dashboard UI. DropdownMenu.GroupLabelmust be rendered insideDropdownMenu.Group; Base UI throwsMenuGroupRootContext is missingwhen labels are placed directly underDropdownMenu.Content.- Traffic Trends chart annotations should use a chart-adjacent annotation rail for dense data; avoid in-plot labels, tall lines, or floating dots that compete with the chart tooltip/data layer.
- Flags list rows (
app/(main)/websites/[id]/flags/_components/flags-list.tsx) are clickable containers with nested controls; mark nested controls withdata-row-interactive="true"and have the row ignore those targets instead of relying on broad cell-levelstopPropagation. - Never put interactive controls inside another
<button>on dashboard rows. If a row has actions/menus, make the main row content a siblingButtonand keep action buttons as separate siblings; do not use adivwith click/key handlers as a fake button. - For data loading and mutations, inspect
apps/dashboard/lib/orpc.tsand the corresponding hooks/components - Public/demo analytics data still flows through
apps/api/src/routes/query.ts; public website access is controlled by per-query-builderpublicAccess, not only oRPC metadata. - Many changes require matching edits in
packages/rpc
API and RPC work
- Start in
apps/api/src - Shared API contracts and procedure logic live in
packages/rpc - Prefer changing shared router logic in
packages/rpcrather than duplicating validation in the dashboard - Investigations run in
apps/insights; RPC only reads cases and accepts durable replies. Case identity iswebsiteId|subjectKey, where the backend owns the subject key. Persist a new observation for each turn while updating the existing insight row. The storedchangePercentis already signed.
Ingestion and analytics pipeline
- Start in
apps/basket/src - Request validation, billing checks, geo/IP parsing, producer logic, and structured errors are important here
- For analytics delivery, success means a durable, replayable handoff with a stable event identity. Do not treat an in-memory buffer, a pre-persistence Redis dedupe key, or a logged producer error as lossless delivery.
- When a durability task excludes PostgreSQL and migrations, use acknowledged Redpanda handoff plus a durable Redis/BullMQ checkpoint where needed; never introduce SQL outbox tables.
Billing (Autumn)
- Retried insight jobs must persist immutable external delivery effects (currently Slack) before calling providers and reuse the effect ID as the provider idempotency key. An insight observation is product memory, not a delivery checkpoint.
- Intelligence pricing should use the existing token-cost-backed
agent_creditsand top-up flow; do not invent per-site or "monitored product" billing without explicit product selection and runtime enforcement. - Transactional billing email identity has three separate concepts: Autumn customer/billing owner, organization, and actual
torecipient. Only personalize from the actual recipient record; if it is unavailable, omit the greeting rather than using the owner name. Keepagent_creditsas an internal feature ID, but describe it to customers as investigation credits and explain that deeper investigations, replies, and rechecks can use more credits. autumn-jsv1.2.2+ — importautumnHandlerfromautumn-js/fetch(NOTautumn-js/elysia, that export was removed in v1.0)- For Elysia, mount with
.mount(autumnHandler(...))— NOT.use() identifycallback receives(request: Request)directly, not({ request })- Webhook event types:
balances.limit_reached(replaces oldcustomer.threshold_reached),customer.products.updated,balances.usage_alert_triggered balances.limit_reachedpayload is flat:{ customer_id, feature_id, entity_id?, limit_type }— no full customer object- SDK
Customertype uses camelCase (balances,subscriptions,overageAllowed), but webhook payloads are snake_case and use old field names (features,products,included_usage,overage_allowed) — do NOT use the SDKCustomertype for webhooks - SDK class is
new Autumn()(readsAUTUMN_SECRET_KEYfrom env); methods use camelCase:customerId,featureId,sendEvent autumn-jscatalog version is in rootpackage.json— update it when bumping- Storage and schema concerns usually continue into
packages/db - evlog → Axiom: never use top-level
erroras a string onlog.error({ ... })(e.g. process handlers); it overwrites structurederror.messageon the wide event. Useerror_messageinstead. Basket/API drains runnormalizeWideEventForAxiombefore ingest; 4xxEvlogErrorrows are emitted aslevel: "warn"withclient_http_error: trueso Axiom “errors” are not inflated by expected client failures.
Database work
- Postgres schema:
packages/db/src/drizzle/schema.ts - Relations:
packages/db/src/drizzle/relations.ts - Drizzle client:
packages/db/src/client.ts - Production
DATABASE_URLmay already target PgBouncer; inspect both the process pool and PgBouncer queues before attributing API timeouts to PostgreSQL. pg.Poolalready grows lazily from zero to its configuredmax; do not replace it with oneClientto address acquisition timeouts, because that serializes queries. Keep a bounded pool, tune its acquisition timeout, and monitorwaitingCount.- ClickHouse helpers and schema:
packages/db/src/clickhouse/* ch:checkis package-scoped; runcd packages/db && bun run ch:check, not the root script runner.- After schema changes, use the repo db scripts rather than ad hoc commands
- A shipped ClickHouse table change needs a tracked forward migration alongside
its reference DDL: bootstrap
CREATE ... IF NOT EXISTSdoes not migrate deployed tables, and Keeper-path or sort-key changes need a shadow-table exchange. - Do not add ClickHouse migration files for delivery hardening; keep relay identity in the worker/queue unless iza explicitly requests persistent warehouse identity.
- For data-cleanup investigations, inspect and describe the existing mutation path first; do not add a persistent purge script unless iza explicitly requests one.
Auth and permissions
- Core auth setup:
packages/auth/src/auth.ts - Client auth entrypoint:
packages/auth/src/client/auth-client.ts - Permission helpers often flow through
packages/rpc
SDK and tracker work
- Published SDK logic:
packages/sdk/src - Browser tracker bundle:
packages/tracker/src - When a retryable batch failure restores events to an in-memory queue, it must also restore an automatic retry timer with capped backoff; requeueing alone silently stalls delivery.
- Public SDK/tracker visitor ID privacy is only
anonymizeVisitorIds(true/omitted = anonymized,false= raw IDs,"auto"= raw only in Databuddy's conservative country allowlist). - Keep visitor ID privacy internals small and direct; avoid exported helper stacks or storage/hashing vocabulary for this option.
- If the user reports missing analytics events, inspect both the producer side and
apps/basket
Verification
- Use targeted package commands when available, for example:
bun run dev:dashboardcd apps/api && bun run testcd packages/sdk && bun testcd packages/tracker && bun run test:unit
- If verification depends on services like Postgres, Redis, ClickHouse, or Redpanda, say so explicitly.
Pitfalls
- The
:onlinemodel suffix is a Perplexity-only convention (e.g.perplexity/sonar-pro). Never add:onlineto non-Perplexity models. - Vercel AI Gateway model IDs in
apps/api/src/ai/config/models.tsuse gateway-style names (e.g.anthropic/claude-sonnet-4.5), not OpenRouter catalog strings. - Bun HTTP default
idleTimeoutis 10 seconds; agent streams can look idle during slow tools.apps/api/src/index.tsexportsidleTimeouton the server (Bun caps at 255 seconds). - AI SDK UI (
useChat) does not document automatic HTTP retries onDefaultChatTransport—retry UX isregenerate()+error(chatbot error state, error handling).maxRetriesonstreamText/generateTextis server-side model calls, not the browser chatfetch. Mid-stream disconnect:resumeStream()(useChat). - AI SDK UI stream custom chunks must use
type: "data-*"(for exampledata-usageordata-aiComponent); injecting arbitrary chunk types such asusagemakesDefaultChatTransportreject the stream. - Dashboard agent prompt references to new tool names must be backed by registered tools in
packages/ai/src/ai/agents/analytics.ts; otherwise the model may call an unavailable tool and then apologize instead of rendering the intended UI. - Dashboard agent navigation affordances should stay dashboard-local and generative where possible; do not move target-label maps into
@databuddy/sharedjust to sync the AI tool with dashboard routes. @elysiajs/corswithorigin: truesetsVary: *, killing CDN caching. Override withset.headers.vary = "Origin"on cacheable public endpoints.applyAuthWideEventinapps/api/src/index.tsruns a session DB lookup on every request including anonymous/public/routes. Skip it for public endpoints via URL check inonBeforeHandle.- Agent SQL security: Tenant isolation (
client_id) is enforced programmatically invalidateAgentSQL+requiresTenantFilterfrom@databuddy/db. Never rely solely on system-prompt instructions for data isolation. Every SQL tool entry point (API, RPC, etc.) must use the shared validation frompackages/db/src/clickhouse/sql-validation.ts. - ClickHouse table allowlist: Agent SQL is restricted to
analytics.*tables only.system.*,information_schema.*are blocked. Add new allowed prefixes insql-validation.tsif new databases are added. - Flags API local dev requires
dotenv -e .envfrom repo root to pick upREDIS_URL,DATABASE_URL, etc. - Node SDK flags: The export is
createServerFlagsManager(notcreateFlagsManager). CallwaitForInit()before use. - User-scoped flags: The public flags API loads user-scoped flags (where
flags.userIdis set) viagetCachedFlagsForUserand merges them with client/org-scoped flags. Client-scoped cache is shared; user-scoped cache is keyed peruserId. - Detail page stats: Use compact inline
flexbars atmin-h-10/py-2.5(40px) — not<dl>grids with large padding. Heights must be multiples of 10px to align with sidebar item sizing. Status uses a colored dot + text, notBadge. - User profile detail: show web vitals as profile/sidebar context, not inside expanded session event rows.
- Referrer rows: query builders with
parseReferrersshould return canonicalname,referrer,source,domain, andreferrer_type; dashboard tables should render/filter from those fields instead of reparsing source labels. - Referrer cell fallback:
ReferrerSourceCellmust also parse URL/domain-lookingsource,referrer, ornamevalues, because cached/legacy query rows may reach the table before all builders return canonical fields. apps/docsmarketing copy: Do not explain pages as “keyword-focused,” “programmatic,” “intent,” or “meta” in UI—users care about tasks (compare tools, replace X, migrate). Keep internal SEO rationale out of hero and body copy.
Search Hints
- Use
rg "createRPCContext|appRouter|sessionProcedure" packages/rpc apps/api - Use
rg "NEXT_PUBLIC_API_URL|createConfig|publicConfig|readBooleanEnv" packages/env apps/dashboard - Use
rg "clickHouse|ClickHouse|TABLE_NAMES" packages/db apps/basket apps/api - Use
rg "betterAuth|drizzleAdapter|organization" packages/auth packages/rpc apps/dashboard - Use
rg "trackRoute|basketRouter|llmRouter|structured-errors" apps/basket - Use
rg "signalKey|subjectKey|insightDedupeKey" apps/insights packages/rpc packages/shared