Guidelines for agents and developers working in this repository.
Bun + Turbo monorepo with:
- Apps:
apps/web- Main web application (app.superset.sh)apps/marketing- Marketing site (superset.sh)apps/admin- Admin dashboardapps/api- API backendapps/desktop- Electron desktop application (see Desktop App Guide below)apps/docs- Documentation site
- Packages:
packages/ui- Shared UI components (shadcn/ui + TailwindCSS v4).- Add components:
npx shadcn@latest add <component>(run inpackages/ui/)
- Add components:
packages/db- Drizzle ORM database schemapackages/constants- Shared constantspackages/scripts- CLI toolingpackages/typescript-config- TypeScript configs
- Package Manager: Bun (no npm/yarn/pnpm)
- Build System: Turborepo
- Database: Drizzle ORM + Neon PostgreSQL
- UI: React + TailwindCSS v4 + shadcn/ui
- Code Quality: Biome (formatting + linting at root)
- Next.js: Version 16 - NEVER create
middleware.ts. Next.js 16 renamed middleware toproxy.ts. Always useproxy.tsfor request interception.
# Development
bun dev # Start all dev servers
bun test # Run tests
bun build # Build all packages
# Code Quality
bun run lint # Check for lint issues (no changes)
bun run lint:fix # Fix auto-fixable lint issues
bun run format # Format code only
bun run format:check # Check formatting only (CI)
bun run typecheck # Type check all packages
# Database
bun run db:push # Apply schema changes
bun run db:seed # Seed database
bun run db:migrate # Run migrations
bun run db:studio # Open Drizzle Studio
# Maintenance
bun run clean # Clean root node_modules
bun run clean:workspaces # Clean all workspace node_modulesBiome runs at root level (not per-package) for speed:
biome check --write= format + lint + organize imports + fix safe issuesbiome check= check only (no changes)biome format= format only- Use
bun run lint:fixto fix all issues automatically
- Keep diffs minimal - targeted edits only
- Follow existing patterns - match the codebase style
- Type safety - avoid
anyunless necessary - Search narrowly - avoid reading large files/assets
These are default heuristics for making design decisions across the monorepo. When in doubt, prefer consistency with existing patterns over novel abstractions.
- Separate by ownership + lifecycle: Keep transport (routes, API handlers), orchestration (tRPC procedures), and domain rules in distinct layers when complexity warrants it.
- Co-locate by lifecycle: Feature-specific code lives together, not split by "type" (e.g., all task-related code in
router/task/). - Boundary layers own error handling: Domain utilities return data or throw specific errors; only boundary code (tRPC procedures, API routes) should catch and transform to
TRPCErroror HTTP responses.
- Keep modules self-contained with narrow public APIs; avoid importing "app state" into lower layers.
- Apply the Law of Demeter: depend on direct collaborators (passed dependencies), not transitive globals.
- When a module grows complex, prefer injecting dependencies (logger, db, external clients) rather than importing singletons so tests can substitute fakes.
- Prefer existing primitives before writing new ones: check
packages/ui,packages/constants, existing utilities. - Use lookup objects/maps over
if (type === ...)conditionals scattered across call sites when handling multiple cases. - Match persistence + complexity to requirements: keep constants as code when static; use Drizzle for multi-tenant data.
- Validate at boundaries (Zod schemas for tRPC inputs, API route bodies) and handle invalid input with clear, user-visible errors.
- External API data is untrusted: handle missing fields, unknown enums, and unexpected shapes; prefer tolerant parsing + explicit fallbacks.
- Never swallow errors silently—at minimum log them with context.
- Start with the simplest correct solution; add complexity only when requirements demand it.
- Use the "three instances" heuristic for new helpers: don't abstract until you've seen the pattern three times.
- Don't introduce frameworks/DSLs for one-off cases.
- tRPC procedures and API route handlers should validate + delegate; complex domain rules live in utilities or service functions.
- A function should operate at one level of abstraction (orchestrate steps or perform low-level work, not both).
Use case-by-case judgment. Extract business logic from tRPC procedures when:
- The procedure exceeds ~50 lines of non-trivial logic
- The same logic is needed by multiple procedures or entry points
- Complex error handling with multiple failure modes
- You need to mock the logic independently for testing
Otherwise, inline logic in procedures is fine for straightforward CRUD.
Functions with 2+ parameters should accept a single params object instead of positional arguments:
// ✅ Good
const createTask = ({ title, userId, priority }: {
title: string;
userId: string;
priority?: number
}) => { ... };
// ❌ Bad - positional arguments
const createTask = (title: string, userId: string, priority?: number) => { ... };Why? Named parameters are self-documenting, order-independent, and easier to extend.
Use appropriate error codes consistently:
// NOT_FOUND - Resource doesn't exist
throw new TRPCError({ code: "NOT_FOUND", message: "Task not found" });
// UNAUTHORIZED - Not logged in
throw new TRPCError({ code: "UNAUTHORIZED", message: "Must be logged in" });
// FORBIDDEN - Logged in but no permission
throw new TRPCError({ code: "FORBIDDEN", message: "Not authorized to access this task" });
// BAD_REQUEST - Invalid input that passed Zod validation
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid task state transition" });
// INTERNAL_SERVER_ERROR - Unexpected failures (use sparingly, prefer specific codes)
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to process task" });
// NOT_IMPLEMENTED - Feature exists but isn't ready yet
throw new TRPCError({ code: "NOT_IMPLEMENTED", message: "Feature not yet implemented" });Pattern for external service failures:
try {
const result = await externalService.call(params);
if (!result.ok) {
console.error("[context] External service error:", result.error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "External service unavailable"
});
}
return result.data;
} catch (error) {
console.error("[context] Unexpected error:", error);
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Operation failed" });
}Use prefixed console logging with consistent context:
// Pattern: [domain/operation] message
console.log("[auth/refresh] Refreshing token for user:", userId);
console.error("[sync/linear] Failed to sync:", error);
console.warn("[task/archive] Task already archived:", taskId);What to log:
- ✅ Entry/exit of significant operations
- ✅ External API calls (without sensitive data)
- ✅ Error conditions with context (IDs, relevant state)
- ❌ Sensitive data (tokens, passwords, PII)
- ❌ High-frequency operations in loops (batch the log)
| Smell | Symptom | Preferred Fix |
|---|---|---|
| Magic numbers | Hardcoded 100, 3, "linear" in logic |
Extract to named constants at module top |
| Provider conditionals | Repeated if (provider === ...) |
Use a lookup object/map pattern |
| God procedures | tRPC procedure does validation + business rules + I/O + error handling | Extract a utility function; keep procedure thin |
| Cross-layer imports | UI importing from packages/db internals |
Go through proper package exports |
| Opacity | Reader can't understand intent within 30 seconds | Rename variables, extract named functions |
| Primitive obsession | Passing raw string for IDs everywhere |
Consider branded types or wrapper objects for critical IDs |
| Shotgun surgery | One logical change requires edits in 5+ files | Co-locate related code; reconsider boundaries |
| Silent error swallowing | catch(() => {}) or catch(e) { return null } |
At minimum log the error; prefer re-throwing or explicit handling |
| Optional deps without reason | logger?: Logger in interface |
Make required unless truly optional; document why if optional |
| Barrel file abuse | export * from "./module" creating circular deps |
Import from concrete files directly when possible |
| Deep nesting | 4+ levels of if/for/try nesting | Early returns, extract functions, invert conditions |
| Boolean blindness | doThing(true, false, true) |
Use options object with named properties |
All projects in this repo should be structured like this:
app/
├── page.tsx
├── dashboard/
│ ├── page.tsx
│ ├── components/
│ │ └── MetricsChart/
│ │ ├── MetricsChart.tsx
│ │ ├── MetricsChart.test.tsx # Tests co-located
│ │ ├── index.ts
│ │ └── constants.ts
│ ├── hooks/ # Hooks used only in dashboard
│ │ └── useMetrics/
│ │ ├── useMetrics.ts
│ │ ├── useMetrics.test.ts
│ │ └── index.ts
│ ├── utils/ # Utils used only in dashboard
│ │ └── formatData/
│ │ ├── formatData.ts
│ │ ├── formatData.test.ts
│ │ └── index.ts
│ ├── stores/ # Stores used only in dashboard
│ │ └── dashboardStore/
│ │ ├── dashboardStore.ts
│ │ └── index.ts
│ └── providers/ # Providers for dashboard context
│ └── DashboardProvider/
│ ├── DashboardProvider.tsx
│ └── index.ts
└── components/
├── Sidebar/
│ ├── Sidebar.tsx
│ ├── Sidebar.test.tsx # Tests co-located
│ ├── index.ts
│ ├── components/ # Used 2+ times IN Sidebar
│ │ └── SidebarButton/ # Shared by SidebarNav + SidebarFooter
│ │ ├── SidebarButton.tsx
│ │ ├── SidebarButton.test.tsx
│ │ └── index.ts
│ ├── SidebarNav/
│ │ ├── SidebarNav.tsx
│ │ └── index.ts
│ └── SidebarFooter/
│ ├── SidebarFooter.tsx
│ └── index.ts
└── HeroSection/
├── HeroSection.tsx
├── HeroSection.test.tsx # Tests co-located
├── index.ts
└── components/ # Used ONLY by HeroSection
└── HeroCanvas/
├── HeroCanvas.tsx
├── HeroCanvas.test.tsx
├── HeroCanvas.stories.tsx
├── index.ts
└── config.ts
components/ # Used in 2+ pages (last resort)
└── Header/
- One folder per component:
ComponentName/ComponentName.tsx+index.tsfor barrel export - Co-locate by usage: If used once, nest under parent's
components/. If used 2+ times, promote to highest shared parent'scomponents/(orcomponents/as last resort) - One component per file: No multi-component files
- Co-locate dependencies: Utils, hooks, constants, config, tests, stories live next to the file using them
The src/components/ui/, src/components/ai-elements, and src/components/react-flow/ directories contain shadcn/ui components. These use kebab-case single files (e.g., button.tsx, base-node.tsx) instead of the folder structure above. This is intentional—shadcn CLI expects this format for updates via bunx shadcn@latest add.
** IMPORTANT ** - Never touch the production database unless explicitly asked to. Even then, confirm with the user first.
- Schema in
packages/db/src/ - Use Drizzle ORM for all database operations
- Always spin up a new neon branch to create migrations. Update our root .env files to point at the neon branch locally.
- Use drizzle to manage the migration. You can see the schema at packages/db/src/schema. Never run a migration yourself.
- Create migrations by changing drizzle schema then running
pnpm drizzle-kit generate --name="<sample_name_snake_case>" NEON_ORG_IDandNEON_PROJECT_IDenv vars are set in .env- list_projects tool requires org_id passed in
- NEVER manually edit files in
packages/db/drizzle/- this includes.sqlmigration files,meta/_journal.json, and snapshot files. These are auto-generated by Drizzle. If you need to create a migration, only modify the schema files inpackages/db/src/schema/and ask the user to rundrizzle-kit generate.
The desktop app uses:
- Electron - Main process, renderer process, preload scripts
- IPC Communication - Type-safe IPC system (see below)
- Terminal Management - node-pty for terminal sessions
- Workspace/Worktree System - Git worktree-based workspace management
- Main process (
src/main/): Can use Node.js modules (fs, path, os, net, etc.) - Renderer process (
src/renderer/): Cannot use Node.js modules - browser environment only - Shared code (
src/lib/electron-router-dom.tsand similar): Cannot use Node.js modules
Why? Vite externalizes Node.js modules for browser compatibility. Importing them in renderer code causes:
Uncaught Error: Module "node:fs" has been externalized for browser compatibility
How to check: Run bun run lint:check-node-imports to detect violations automatically.
This check runs as part of bun run typecheck.
If you need Node.js functionality in renderer:
- Move the code to
src/main/lib/ - Use IPC to communicate between renderer and main process
- Pass data through preload script or environment variables
All IPC communication is fully type-safe. See apps/desktop/docs/TYPE_SAFE_IPC.md for complete documentation.
1. Define channel types in apps/desktop/src/shared/ipc-channels.ts:
export interface IpcChannels {
"my-channel": {
request: { param1: string; param2: number };
response: { success: boolean; data?: any };
};
}2. Implement handler in apps/desktop/src/main/lib/*.ts:
// ✅ CORRECT: Accept object parameter
ipcMain.handle("my-channel", async (_event, input: { param1: string; param2: number }) => {
return { success: true, data: someResult };
});
// ❌ WRONG: Don't use positional parameters
ipcMain.handle("my-channel", async (_event, param1, param2) => {
// This won't match the typed renderer calls!
});3. Call from renderer in apps/desktop/src/renderer/**/*.tsx:
// Type-safe - no manual type assertions needed!
const result = await window.ipcRenderer.invoke("my-channel", {
param1: "value",
param2: 123,
});
// TypeScript knows the exact response type- Always use object parameters - Handlers must accept a single object, not positional params
- Define types first - Add to
ipc-channels.tsbefore implementing - No manual type assertions - Let TypeScript infer types from the definitions
- Test after adding channels - Verify parameters are received correctly
src/main/- Main process (Node.js environment)lib/workspace-ipcs.ts- Workspace/worktree IPC handlerslib/terminal-ipcs.ts- Terminal IPC handlerslib/workspace-manager.ts- Workspace business logiclib/worktree-manager.ts- Git worktree operations
src/renderer/- Renderer process (Browser environment)src/preload/- Preload scripts (Context bridge, type-safe IPC wrapper)src/shared/- Shared types and constantstypes.ts- Data modelsipc-channels.ts- IPC type definitions
The desktop app loads environment variables from the monorepo root .env file:
Loading sequence:
src/main/index.ts- Loads.envwithoverride: truebefore any imports (main process)electron.vite.config.ts- Loads.envwithoverride: truefor Vite configuration (build time)
Important notes:
override: trueis critical - ensures.envvalues override inherited environment variablessrc/lib/electron-router-dom.tsmust NOT import Node.js modules (node:path,dotenv) as it's shared between main and renderer processes- Port configuration flows:
.env→ main process →electron-router-domsettings → Vite dev server