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>
191 lines
8.7 KiB
Markdown
191 lines
8.7 KiB
Markdown
# CLAUDE.md
|
|
|
|
## Project Overview
|
|
|
|
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.
|
|
|
|
**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/
|
|
│ ├── officer-web/ # the SPA shell — Screens/Authentication + Screens/Dashboard
|
|
│ └── landing/ # marketing landing page
|
|
├── servers/
|
|
│ ├── 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
|
|
```
|
|
|
|
`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 (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`. 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: types → external → workspace → relative.
|
|
|
|
```ts
|
|
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';
|
|
```
|
|
|
|
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 one-liners, regular functions for anything with real logic. No multiline
|
|
parameter lists — extract a params type instead.
|
|
|
|
```ts
|
|
type ProcessDataParams = { body: Record<string, unknown> | undefined; query: Record<string, string> };
|
|
export function processData({ body, query }: ProcessDataParams): Result { ... }
|
|
```
|
|
|
|
### Components and hooks
|
|
|
|
```tsx
|
|
type ExperimentCardProps = { experiment: Experiment; onSelect: (id: number) => void };
|
|
|
|
export const ExperimentCard = ({ experiment, onSelect }: ExperimentCardProps) => { ... };
|
|
```
|
|
|
|
Complex hooks return objects; simple state hooks return `as const` tuples. Event parameters are
|
|
always named `ev`, never `e`.
|
|
|
|
### API calls
|
|
|
|
`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 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
|
|
|
|
Commit messages: simple lowercase, no prefixes.
|
|
|
|
## Further Reading
|
|
|
|
- `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
|
|
|
|
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.
|