7.8 KiB
CLAUDE.md
Project Overview
Officer is an AI-assisted personal backend and frontend for life management.
Monorepo Structure
src/
├── apps/
│ ├── dashboard/ # Main UI (React 19)
│ └── editor/ # Visual editor
├── servers/
│ ├── api/ # REST API (Hono, port 5000)
│ └── tracking/ # Event collection (port 5001)
├── databases/
│ ├── officer_db/ # Main app data
│ └── ephemeral_db/ # Cache & temporary data
└── workspaces/ # Shared: components, hooks, helpers, types, config, etc.
Path aliases: @/ → dashboard, @@/ → servers
Tech Stack
- Runtime: Bun
- Language: TypeScript (strict), some raw SQL
- Frontend: React 19, React Router, React Query + Context
- UI: shadcn/ui base + custom components, Tailwind CSS
- Backend: Hono framework
- Database: PostgreSQL, Drizzle ORM + raw SQL
Code Style
- Paradigm: Functional - pure functions, immutability, composition
- TypeScript: Strict - no
any, proper types everywhere - Comments: Minimal - code should be self-documenting
- Async: Always async/await
- Errors: Try/catch (detailed patterns in section-specific docs)
TypeScript Patterns
Imports
Order imports as: types → external → workspace → relative
import type { Experiment, User } from 'types';
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';
import { formatDate } from 'helpers/formatters';
import { useWebsites } from '../useWebsites';
Type-only imports: With verbatimModuleSyntax enabled, types must use type-only imports:
// ✅ Good - inline type import
import { ActionModals, type ActionModalsTypes } from './ActionModals';
// ✅ Good - separate type import
import type { FormEvent } from 'react';
import { useState } from 'react';
// ❌ Bad - will error with verbatimModuleSyntax
import { ActionModals, ActionModalsTypes } from './ActionModals';
Types
- Prefer
typeoverinterface - Shared types go in
workspaces/types- re-exports from database types - Database types live in their respective
db/types.tsfiles - Component props: colocated as named type
type ExperimentCardProps = {
experiment: Experiment;
onSelect: (id: number) => void;
};
Database Types
Use Drizzle-inferred types, never manual type definitions in schema files:
// In officerdb/types.ts
import * as Schema from './schema';
// Simple table - just Select/Insert
export type Screenshot = typeof Schema.Screenshots.$inferSelect;
export type ScreenshotInsert = typeof Schema.Screenshots.$inferInsert;
// Table with relations - Select + extended type for API responses
export type UserSelect = typeof Schema.Users.$inferSelect;
export type UserInsert = typeof Schema.Users.$inferInsert;
export type User = UserSelect & {
company: Company;
passkeys: Passkey[];
};
In app code, import types from 'types' (the workspace re-exports everything):
// ✅ Good - in apps/dashboard, apps/editor, etc.
import type { User, Experiment, VariantStat } from 'types';
// ❌ Bad - don't import directly from database packages in app code
import type { User } from 'officerdb/types';
import type { VariantStat } from 'statisticsdb/types';
In database/server code, import from the specific package:
// ✅ Good - in servers/, databases/
import type { User } from 'officerdb/types';
Functions
- Arrow functions for simple/one-liners
- Regular functions for complex logic
- Named exports only (no default exports)
- No multiline parameter definitions - extract params type when signature gets long
// Simple utility - arrow
export const formatDate = (value: number) => new Date(value).toLocaleDateString();
// Complex logic - regular function
export function calculateStatistics(data: DataPoint[]) {
// multi-line logic
}
// ❌ Bad - multiline params
export function processData(
body: Record<string, unknown> | undefined,
query: Record<string, string | undefined>,
): Result { ... }
// ✅ Good - extract params type
type ProcessDataParams = {
body: Record<string, unknown> | undefined;
query: Record<string, string | undefined>;
};
export function processData({ body, query }: ProcessDataParams): Result { ... }
Null Handling
- Early returns for guards
- Optional chaining for property access
- Use
?? undefinedto convertnulltoundefinedfor props expectingstring | undefined - Use
?? 0or?? ''for fallback values with state setters
function processExperiment(exp: Experiment | null) {
if (!exp) return null; // guard
const websiteName = exp.website?.name ?? 'Unknown'; // access
// ...
}
// Converting null to undefined for props
<AvatarImage src={user.avatar ?? undefined} /> // avatar is string | null, src expects string | undefined
// Fallback for state setters expecting non-null
setWebsite(website?.id ?? 0); // id might be undefined, setter expects number
// Array access with known valid index - use non-null assertion
const items = ['a', 'b', 'c'];
if (items.length > 0) {
const first = items[0]!; // We know index 0 exists after length check
}
React Patterns
See CONVENTIONS.md for detailed patterns (feature folders, manager pattern, state management, React 19 rules).
Components
Arrow function with named props type:
type ExperimentCardProps = {
experiment: Experiment;
onSelect: (id: number) => void;
};
export const ExperimentCard = ({ experiment, onSelect }: ExperimentCardProps) => {
return (/* ... */);
};
Hooks
Return objects for complex hooks, tuples for simple state:
// Complex - return object
export const useExperiment = (id: number) => {
return { experiment, isLoading, update, delete: deleteExp };
};
// Simple state - return tuple (like useState)
export const useGlobal = <T>(key: string, initial: T) => {
return [value, setValue, refresh, reset] as const;
};
Directory Structure
Naming Conventions
- Components:
PascalCase.tsx - Everything else:
kebab-case.ts - Hooks:
use-*.tsoruseFeatureName.ts
Formatting
Prettier config in .prettierrc:
- Semicolons: always
- Quotes: single (JS), double (JSX)
- Trailing commas: all
- Indent: 2 spaces
- Line width: 120
bun format # Format all files
bun format:check # Check without writing
Development
bun dev # Runs dashboard + API server
bun dev:tracking # Runs tracking server separately if needed
bunx tsgo # TypeScript check (not npx tsc)
New features: Usually database-first (schema → API → UI)
Environment variables:
- Backend:
process.env.Xdirectly - Frontend: Use existing config patterns
Logging: console.log for debugging (Signoz integration coming)
Git
Simple lowercase commit messages, no prefixes.
Working With Me
- Ask first: Confirm approach before significant changes
- Explore thoroughly: Read related files for full context
- Keep it simple: No over-engineering or premature abstractions
- Be explicit: No magic or implicit behavior
- Stay focused: Note unrelated issues but don't fix them
- Detailed output: Provide full breakdown of changes when completing tasks
Section-Specific Docs
Detailed patterns for each area live in their respective directories:
Frontend:
src/apps/CLAUDE.md- Shared frontend patterns (components, hooks, state)src/apps/dashboard/CLAUDE.md- Dashboard specificssrc/apps/editor/CLAUDE.md- Editor specifics Backend:src/servers/CLAUDE.md- API and tracking serverssrc/databases/CLAUDE.md- Database schemas and patterns
Project-wide:
CONVENTIONS.md- Detailed code patterns with rationale (component organization, state management, React patterns)