rewrite CLAUDE.md against the current codebase

The old version described a repo that no longer exists: apps/dashboard and
apps/editor, a tracking server on port 5001, an ephemeral_db, and a dashboard
and API running as separate processes. None of that is true.

Replaces it with what the code actually does — one Bun process serving the SPA,
the API and eight WebSocket providers; sidecars for the privileged work; the
split between Postgres and the file-backed items store; and the single-user
invariant stated as an invariant rather than a migration in progress.

Also records two things that are easy to get wrong from reading alone: the
database is maintained with db:push and has never had a migration applied, and
agents run unsandboxed with permissions bypassed on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
brunorezio
2026-07-25 23:30:20 +01:00
co-authored by Claude Opus 5
parent 0d67e2af26
commit 0e71d87f75
+142 -220
View File
@@ -2,267 +2,189 @@
## Project Overview
Officer is an AI-assisted personal backend and frontend for life management.
Officer is a self-hosted personal platform for one person: the server owner. It bundles an AI agent,
a terminal, a file browser, a code editor, email, chat channels, a remote desktop and customisable
dashboards behind a single web app.
## Monorepo Structure
**Single-user is a hard invariant, not a stage.** There is exactly one account, created once by
`POST /auth/bootstrap` while the user table is empty. There are no roles, no invitations, no
sandboxing of one user from another, and no per-user isolation anywhere in the codebase. If a change
seems to need "which user is this", the answer is always the owner.
## Architecture
One Bun process (`src/server.tsx`) serves everything:
- the React SPA, via Bun's HTML import of `src/apps/officer-web/index.html` (HMR in dev)
- the REST API, a Hono app mounted at `/api` (`src/servers/hono.ts`)
- eight WebSocket providers — terminal, chat, task-runner, pipeline, dev-server proxy, cliamp,
cliamp-audio, desktop — plus a sidecar registration socket
- a browser relay on its own port (`BROWSER_RELAY_PORT`, default 18792)
Long-running and privileged work lives in **sidecars**: separate processes that dial back in over
`/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them
(`ecosystem.config.cjs`): `officer` (the server), `officer-claude`, `officer-opencode`,
`officer-email`, `officer-pty`, `officer-vnc`.
Agents run **unsandboxed as the server owner**, with `--dangerously-skip-permissions`. This is
deliberate — it is the owner's own machine. Do not add a jail without being asked.
## Repository Layout
```
src/
├── server.tsx # the single entrypoint: routes, WS upgrades, static files
├── apps/
│ ├── dashboard/ # Main UI (React 19)
│ └── editor/ # Visual editor
│ ├── officer-web/ # the SPA shell — Screens/Authentication + Screens/Dashboard
│ └── landing/ # marketing landing page
├── 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.
│ ├── hono.ts # router composition; everything under /api
── _middlewares/ # auth, body parsing, origin validation, rate limiting
│ ├── api/<feature>/ # one folder per feature, each exporting a router
│ ├── channels/ # Telegram / WhatsApp / Discord bridges
── queue/ # background job engine
│ └── sidecar/ # sidecar implementations + the wire protocol
├── databases/officer_db/ # the only database (Postgres + Drizzle)
├── extensions/browser-relay/
└── workspaces/ # shared packages, each a bun workspace
```
**Path aliases**: `@/` → dashboard, `@@/` → servers
`src/workspaces/officerdev` is the biggest of these: the windowed "apps" (FileBrowser, Chat,
Terminal, CodeEditor, Desktop, Projects, Dashboards…) that the shell hosts, behind an `AppRegistry`.
`src/apps/officer-web` is only the shell — screens, routing and settings.
**Path aliases** (`tsconfig.json`): `@/*``src/apps/officer-web`, `@/components/*`
`src/workspaces/components`, `@@/*``src/servers`, `@/public/*``public`. Workspace packages are
imported by their package name (`officerdev`, `hooks`, `state`, `types`, `helpers`, …).
## 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
- **Runtime**: Bun (Node 22 is enforced by a `preinstall` check)
- **Language**: TypeScript, strict. `bunx tsgo` is clean — keep it that way.
- **Frontend**: React 19, React Router 7, React Query, Tailwind 4, shadcn/ui + custom components
- **Backend**: Hono
- **Database**: one Postgres database via Drizzle
- **Agents**: Claude Code and opencode, driven through sidecars; tools exposed over MCP
## Data
Two stores, and the split matters:
**Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards,
projects, email accounts, queue and pipeline jobs. Schema in `src/schema/`, hand-written queries in
`src/queries/`, types inferred from the schema in `src/types.ts`.
**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` contains one directory
per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no database rows, no
scope tiers. `DATA_PATH/<email>/` holds the managed home, attachments and per-account email SQLite
stores. Path helpers live in `src/servers/data-path.ts`; note `getHomeDir` (the managed home under
`DATA_PATH`) versus `getOwnerHomeDir` (the owner's real login home when `HOME_DIR` is set, which is
where terminals, chats and task runs actually execute).
### Schema changes use `push`, not migrations
This database is kept in sync with `bun db:push`. **`drizzle-kit migrate` has never been run here**
— there is no `__drizzle_migrations` table, and the files in `officer_db/migrations/` are historical
residue that does not describe the live database. Change the schema, run `bun db:push`, done.
## Security Model
The perimeter is one credential, so the guards matter:
- `IS_DEV_BUILD` (`src/servers/build-env.ts`) is true **only** when `PUBLIC_BUILD_ENV` is explicitly
`dev`/`development`. Everything else, including unset, is hardened. Origin validation, rate
limiting and password rules all key off it — they fail closed.
- Allowed origins come from `PUBLIC_URL`. Officer always sits behind an HTTPS reverse proxy, so the
forwarded `Host` must equal `PUBLIC_URL`'s authority exactly.
- JWTs are 30-day, blacklisted on signout, and invalidated by a password change (`passwordChangedAt`).
- A panic lockdown (`src/servers/api/auth/panic.ts`) is in-memory only and refuses every
authenticated request until the server restarts.
## Commands
```bash
bun dev # the whole app — SPA + API + WebSockets, watched
bun start # production
bunx tsgo # typecheck (not tsc)
bun test # tests
bun format # prettier over changed files only
bun db:push # apply the schema to Postgres
bun setup # guided install (writes .env, incl. PUBLIC_BUILD_ENV=production)
```
Sidecar control: `bun run start:sidecar` / `stop:sidecar` / `restart:sidecar` / `logs:sidecar`.
Note: `build:editor*` in `package.json` points at `scripts/build/editor.ts`, which does not exist.
## 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
- **Paradigm**: functional pure functions, immutability, composition
- **TypeScript**: strict, no `any`. Type-only imports are required (`verbatimModuleSyntax`).
- **Comments**: minimal, and about *why*. Don't narrate what the code already says.
- **Async**: always async/await
- **Exports**: named only, no defaults
- **Files**: `PascalCase.tsx` for components, `kebab-case.ts` for everything else
- **Prettier**: single quotes in JS, double in JSX, semicolons, trailing commas, 120 cols
### Imports
Order imports as: types → external → workspace → relative
Order: types → external → workspace → relative.
```ts
import type { Experiment, User } from 'types';
import { useState, useEffect } from 'react';
import type { User } from 'types';
import { useState } 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:
```ts
// ✅ 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 `type` over `interface`
- Shared types go in `workspaces/types` - re-exports from database types
- Database types live in their respective `db/types.ts` files
- Component props: colocated as named type
```ts
type ExperimentCardProps = {
experiment: Experiment;
onSelect: (id: number) => void;
};
```
### Database Types
Use Drizzle-inferred types, never manual type definitions in schema files:
```ts
// 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):
```ts
// ✅ 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:
```ts
// ✅ Good - in servers/, databases/
import type { User } from 'officerdb/types';
```
In app code import types from `'types'`, which re-exports the database types. Only server and
database code imports from `officerdb/types` directly.
### 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
Arrow functions for one-liners, regular functions for anything with real logic. No multiline
parameter lists — extract a params type instead.
```ts
// 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>;
};
type ProcessDataParams = { body: Record<string, unknown> | undefined; query: Record<string, string> };
export function processData({ body, query }: ProcessDataParams): Result { ... }
```
### Null Handling
- Early returns for guards
- Optional chaining for property access
- Use `?? undefined` to convert `null` to `undefined` for props expecting `string | undefined`
- Use `?? 0` or `?? ''` for fallback values with state setters
### Components and hooks
```ts
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:
```tsx
type ExperimentCardProps = {
experiment: Experiment;
onSelect: (id: number) => void;
};
type ExperimentCardProps = { experiment: Experiment; onSelect: (id: number) => void };
export const ExperimentCard = ({ experiment, onSelect }: ExperimentCardProps) => {
return (/* ... */);
};
export const ExperimentCard = ({ experiment, onSelect }: ExperimentCardProps) => { ... };
```
### Hooks
Return objects for complex hooks, tuples for simple state:
```tsx
// Complex - return object
export const useExperiment = (id: number) => {
return { experiment, isLoading, update, delete: deleteExp };
};
Complex hooks return objects; simple state hooks return `as const` tuples. Event parameters are
always named `ev`, never `e`.
// Simple state - return tuple (like useState)
export const useGlobal = <T>(key: string, initial: T) => {
return [value, setValue, refresh, reset] as const;
};
```
### API calls
## Directory Structure
### Naming Conventions
- **Components**: `PascalCase.tsx`
- **Everything else**: `kebab-case.ts`
- **Hooks**: `use-*.ts` or `useFeatureName.ts`
## Formatting
Prettier config in `.prettierrc`:
- Semicolons: always
- Quotes: single (JS), double (JSX)
- Trailing commas: all
- Indent: 2 spaces
- Line width: 120
```bash
bun format # Format all files
bun format:check # Check without writing
```
## Development
```bash
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.X` directly
- Frontend: Use existing config patterns
**Logging**: `console.log` for debugging (Signoz integration coming)
## Git
Simple lowercase commit messages, no prefixes.
`useClient()` returns typed verbs — `client.get<Foo>('/foo')` really does return `Foo`, so pass the
type parameter and let inference flow from it.
## 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
- **Ask first** — confirm the approach before a significant change
- **Explore thoroughly** — read the related files before editing
- **Keep it simple** — no over-engineering, no premature abstraction
- **Be explicit** — no magic, no implicit behaviour
- **Stay focused** — note unrelated problems, don't fix them uninvited
- **Report honestly** — say what you verified and what you didn't
## Section-Specific Docs
Commit messages: simple lowercase, no prefixes.
Detailed patterns for each area live in their respective directories:
## Further Reading
**Frontend:**
- `src/apps/CLAUDE.md` - Shared frontend patterns (components, hooks, state)
- `src/apps/officer-web/Screens/Dashboard/Automation/CLAUDE.md` - Automation screen specifics
- `CONVENTIONS.md` — component organisation, state management, React patterns, with rationale
- `TODO.md` — current direction and deferred work; **takes precedence over this file where they disagree**
- `src/apps/CLAUDE.md` — shared frontend patterns
- `src/databases/CLAUDE.md` — database patterns
**Backend:**
- `src/databases/CLAUDE.md` - Database schemas and patterns
**Project-wide:**
- `CONVENTIONS.md` - Detailed code patterns with rationale (component organization, state management, React patterns)
- `TODO.md` - Current direction and deferred work. Takes precedence over this file where they disagree.
The other markdown files in the repo root (`OFFICERDEV_*.md`, `MARKETING_WEBSITE.md`, `PHONE_APP.md`,
`SECURITY_AUDIT.md`, `SETUP_*.md`, …) are older design notes. Treat the code as the source of truth.