Private leadership intelligence for technical leaders entering a new organization. A structured notebook and relationship map: capture observations, link people and systems, and surface patterns over time. Single-user, local-first. Not a CRM.
Design and backlog: See .ai/context/design_decisions.md. Implementation follows the execution plan in .ai/context/execution_plan.md. Shared technical references: docs/REACT_PATTERNS.md, docs/DATA_FORMAT_REFERENCE.md.
Command Atlas is aimed at one person using it on their own machine (or an equivalent local setup such as Docker on your workstation). Data stays under your control; the stack assumes a low-trust network boundary—for example localhost or a private container network—not the public internet.
If you grow beyond that—shared hosting, remote access, multiple users, importing data from untrusted parties, or anything that widens who can reach the API—treat security as a first-class requirement: authentication, transport protection (TLS), careful validation of uploads and backups, secrets handling, and operational hardening appropriate to the threat model. The current codebase optimizes for simplicity and local trust; do not reuse that implicit model when you expand scope.
- Node 18+
- npm or pnpm
- Docker-compatible runtime (Docker Desktop or OrbStack) for containerized local install
Database: SQLite by default (no separate server). The DB file is created at api/prisma/dev.db when you run migrations. To use PostgreSQL instead, switch the Prisma provider and set DATABASE_URL in api/.env.
People may have an optional site location (a label shown in the UI; the stored value is a code referencing SiteLocation). Allowed codes and labels live in the SiteLocation table (code, label, sort_order), not in application TypeScript. Use the app’s Locations page (/locations) to add, edit labels/sort order, or delete site locations without opening the database. The repository ships neutral placeholder rows in the seed migration (e.g. LOC01 … Location 1); you can replace or extend those rows via the UI, or with direct SQL / a new migration if you prefer bulk or scripted changes. Do not commit real site names or codes if you treat them as sensitive.
How locations get into the database (normal path)
-
Run Prisma migrations against your SQLite file (creates tables and applies SQL seeds):
cd api && npx prisma migrate deploy
For local development you can use
npx prisma migrate devinstead; both apply pending migrations. -
The seed rows for five placeholder sites are defined in the migration
api/prisma/migrations/20260410224810_seed_site_locations/migration.sql(INSERT OR IGNORE …). You do not need a separate seed script unless you want different initial data. -
If
prisma migrate deployreports a checksum error on20260410224810_seed_site_locations(e.g. after the seed migration file was updated), treat it as a local dev database issue: back updev.dbif needed, remove it or recreate it, then run migrations again. Production databases should follow your normal migration playbook. -
Docker: Use the same migration flow inside the API container / image so
SiteLocationexists before serving traffic.
Changing or adding locations later
- In-app (usual): Open Locations in the nav, then add a row or open one to edit code, label, and sort order (renaming code updates people assigned to that site via the database FK). Deleting a location clears that assignment on any people who had it.
- Migrations / SQL (bulk or automation): Prefer a new migration with additional
INSERT OR IGNORE/UPDATEstatements, or edit rows directly in SQLite (e.g.sqlite3 api/prisma/dev.db) if you accept manual ops. The People UI loads options fromGET /api/site-locationsat runtime.
Backups and older database files
- A full export is a snapshot of the whole SQLite file (all tables, including
SiteLocationwhen present). - Import rebuilds core app tables from the uploaded file’s schema. Restoring a backup taken before
SiteLocation/Person.locationexisted could leave the live file out of sync with the current app until migrations or post-import repair run. The API runsrepairSchemaAfterSqliteImportafter a successful in-app import (seeapi/src/lib/postImportSchemaRepair.ts) to createSiteLocation, seed default rows, and addPerson.locationif missing. If you still see API errors after import, re-import once on the upgraded app or runnpx prisma migrate deployagainst that database file.
Process notes: docs/POSTMORTEM_SITE_LOCATION_AND_LEGACY_BACKUP.md.
This path works with Docker Desktop and OrbStack.
docker compose up --buildOpen http://localhost:5173.
Note: the Docker setup does not publish API port 4000 to the host by default (to avoid port conflicts). The app reaches the API over the internal compose network.
# If 5173 is in use on your host, choose another host port:
# APP_PORT=5174 docker compose up --build
# Stop services
docker compose down
# Rebuild after dependency or Dockerfile changes
docker compose up --build
# View logs
docker compose logs -f- SQLite data persists in named volume
api_sqlite_datamounted at/app/sqlitein the API container (DATABASE_URLpoints atsqlite/dev.db). Theprisma/directory is not overlaid by a volume soschema.prismaand migrations from the image stay visible andprisma generatestays in sync with the schema. - Obsidian direct-write export path is mounted from
./obsidian-vaultto/obsidian-vaultin the API container. - To remove containers and persisted SQLite data:
docker compose down -vcd api
npm install
cp .env.example .env # already set for SQLite (file:./dev.db)
npx prisma migrate dev --name init # creates SQLite DB and tables
# Optional: npm run db:seed # seed sample data
npm run dev # starts API on http://localhost:4000In another terminal:
cd app
npm install
npm run dev # starts Vite on http://localhost:5173, proxies /api to APIOpen http://localhost:5173. Use Capture to add observations; add People and Systems first to link from observations.
The People table is sorted and filtered in the app using the full list from GET /api/people (client-side only; no extra query parameters). Column headers cycle ascending → descending → original order and show a sort indicator. Use the filter row to narrow by name, title, team, location, and manager (including empty values where applicable). Click a manager name to filter to people who report to that manager; use the small open icon beside the name to open that manager’s person page. Icons use lucide-react.
Process notes: docs/POSTMORTEM_PEOPLE_LIST_UX.md.
Observations hold the main text plus optional metadata (title, observation type, links to people/systems). You can also record operational toil so repetitive or scaling manual work is easier to spot later:
| Concept | In the UI | Notes |
|---|---|---|
| What happened / the work | The Manual Action | Required body text in quick capture. |
| Why it matters | The Toil Cost (Impact) | Optional; maps to whyItMatters. |
| Where it came from | The Trigger/Catalyst | Optional; maps to context. |
| Category of toil | Toil type | Optional dropdown (e.g. Manual Deployment/Release, Incident Firefighting, Handoff/Ticket Friction). |
| Subjective severity | Friction score | Optional 1–5; clearable in the editor. |
Set these in Quick Capture or when editing an observation on its detail page. Obsidian export includes toil type and friction score in generated observation notes when present.
- Scope: manual, one-way export from Command Atlas -> Obsidian.
- Delivery:
- Download zip from Dashboard (primary path).
- Optional direct write to
OBSIDIAN_VAULT_ROOTvia API.
- Link mode: Obsidian wikilinks (
[[...]]) in generated note bodies. - Overwrite mode: full regenerate/replace of generated folders on each export.
Set OBSIDIAN_VAULT_ROOT in api/.env to your vault directory:
OBSIDIAN_VAULT_ROOT="/absolute/path/to/your/vault"- Start API (
cd api && npm run dev) and app (cd app && npm run dev). - In the app Dashboard, click Download Obsidian Zip.
- Unzip the downloaded file into your Obsidian vault.
- Set
OBSIDIAN_VAULT_ROOTinapi/.env. - Trigger:
curl -X POST http://localhost:4000/api/export/obsidian- The API writes generated files to:
People/Systems/Observations/inside your configured vault root.
curl -L "http://localhost:4000/api/export/obsidian.zip" -o command-atlas-obsidian.zip- No import from Obsidian back into Command Atlas.
- No bidirectional sync, scheduling, or on-save automation.
- No
.canvas, Dataview/Bases generation, or merge/conflict handling.
| Location | Command | Description |
|---|---|---|
| root | npm run test |
Run API tests then E2E (E2E needs API + app running) |
| root | npm run backup:db -- "<codename>" |
Canonical Docker backup: creates one portable .db file and .md5 checksum in backups/ |
| api | npm run test |
API integration tests (Vitest) |
| api | npm run dev |
Start API (tsx watch) |
| api | npm run db:migrate |
Run Prisma migrations |
| api | npm run db:seed |
Seed sample data |
| api | curl -X POST http://localhost:4000/api/export/obsidian |
Trigger direct-write Obsidian export via API |
| api | curl -L "http://localhost:4000/api/export/obsidian.zip" -o command-atlas-obsidian.zip |
Download Obsidian export zip via API |
| app | npm run test:e2e |
E2E tests (Playwright; reuses existing API/app if running) |
| app | npm run dev |
Start Vite dev server |
| app | npm run build |
Build for production |
cd api
npx prisma migrate reset # drops DB, reapplies migrations, optionally runs seedWith the API and app running, open Backup in the header (/backup). Download saves a full SQLite .db snapshot; import replaces all application data from a compatible backup file (export first if you need to keep the current database). For deep recovery steps and WAL sidecar handling when replacing files on disk, see below.
Canonical mode: use Docker as the source of truth for runtime + backup. Sync model is single active machine at a time, with on-demand manual backup + upload.
Run exactly one backup command from repo root:
npm run backup:db -- "spring-harbor"What it does:
- Runs SQLite
.backupinside the API container against/app/sqlite/dev.dbso the snapshot is consistent even if the app is writing (better than copying the raw file while SQLite is open). - Writes backup file to
backups/command-atlas-<UTC timestamp>-<codename>.db - Writes checksum to
backups/command-atlas-<UTC timestamp>-<codename>.db.md5 - Prints the MD5 hash in terminal for quick validation
If the script says sqlite3 is not available in the API container, rebuild the API image once: docker compose build --no-cache api (the image installs the sqlite3 CLI for backups).
If you omit the codename, the script auto-generates a human-readable codename.
Move/upload the single .db backup file to the other machine:
backups/command-atlas-<timestamp>-<codename>.db
Optional but recommended: move the matching .md5 file too and verify after transfer.
cd backups
md5sum -c "command-atlas-<timestamp>-<codename>.db.md5"SQLite may create sidecar files next to the DB (dev.db-wal, dev.db-shm) when using WAL mode. If you replace only dev.db but leave old WAL/SHM files behind, SQLite can show wrong or partial data (for example missing people or systems) because the journal no longer matches the new main file. Always remove those sidecars after copying a restored dev.db.
- Stop the API so nothing holds the DB open:
docker compose stop api- Copy your backup over the live DB file:
docker cp "backups/command-atlas-<timestamp>-<codename>.db" "$(docker compose ps -q api):/app/sqlite/dev.db"- Remove stale WAL/SHM files on the same volume (safe if missing):
docker compose run --rm --no-deps api sh -lc 'rm -f /app/sqlite/dev.db-wal /app/sqlite/dev.db-shm'- Start API again:
docker compose up -d api- Verify data: open People and Systems as well as observations — all live in the same SQLite file, so they should match the backup.
If Docker is unavailable, run API + app locally with Node and point API DATABASE_URL to the transferred .db file path. Keep the same single-file handoff model (.db + optional .md5). After placing the file, delete any dev.db-wal and dev.db-shm next to it before starting the API, for the same WAL reason as Docker.
Usually stale SQLite WAL sidecar files (dev.db-wal, dev.db-shm) left next to a newly restored dev.db. Follow the restore steps above (stop API → copy → rm WAL/SHM → start API). All entities share one database file; there is no separate “people” restore step.
Often schema mismatch: the imported file predates a migration (for example SiteLocation or Person.location). Re-import the backup using the Backup page after upgrading to a build that includes post-import repair, or run cd api && npx prisma migrate deploy against the same DATABASE_URL file so migrations and seed SQL apply. See Site locations (reference data) above.
- API running? The app proxies
/apitohttp://localhost:4000. Start the API withcd api && npm run dev. If your API runs on another port, set the proxy inapp/vite.config.ts(e.g.target: "http://localhost:5001"). - Database: Default is SQLite (
DATABASE_URL="file:./dev.db"inapi/.env). Runcd api && npx prisma migrate dev --name initto create the DB and tables. For PostgreSQL, change the provider inapi/prisma/schema.prismaand setDATABASE_URLto your connection string. - Prisma client: Run
cd api && npx prisma generateafter changing the schema. - Dev error details: With
NODE_ENVunset or notproduction, the API includes the underlying error message in the 500 response. Check the API terminal for full logs. - Docker startup race: On first
docker compose up --build, the app may load before API migrations finish. Wait a few seconds and refresh, or checkdocker compose logs -f api. - Docker:
Unknown argument toilType(or similar) on save: The API container must seeprisma/schema.prismafrom the image. If an older compose file mounted a volume over all of/app/prisma, the volume could hide the schema andprisma generatewould produce a stale client. Current compose mounts onlysqlite/for the DB file. Rebuild and recreate:docker compose down && docker compose up --build.
If 5173 is already in use:
APP_PORT=5174 docker compose up --buildThen open http://localhost:5174.
If the API cannot write the SQLite database in Docker, recreate containers and volume:
docker compose down -v
docker compose up --buildIf API logs show Prisma libssl/openssl detection errors, rebuild the API image after pulling latest changes:
docker compose down
docker compose build --no-cache api
docker compose up --buildIf API logs show @prisma/client did not initialize yet, regenerate Prisma client by rebuilding the API image:
docker compose down
docker compose build --no-cache api
docker compose up --buildIf you see "Cannot convert undefined or null to object" after a failed request, ensure the backend returns JSON error bodies (e.g. { "error": "message" }) and that the API URL/port matches the app proxy in vite.config.ts.
If you added a new nav link/page (for example Backup) but cannot see it in the browser:
- Hard refresh the page (
Ctrl+Shift+R/Cmd+Shift+R). - Restart the app dev server (
cd app && npm run dev). - If using Docker, rebuild and restart (
docker compose up --build).
Vite/Docker can occasionally serve stale assets after branch switches or dependency changes.
- Frontend: React, TypeScript, Vite, Tailwind CSS, TanStack Query, React Router, react-markdown
- Backend: Node, Express, TypeScript, Prisma
- Database: SQLite (default; file at
api/prisma/dev.db). PostgreSQL supported by changing the Prisma provider.
Private use. See repo for details.
Before publishing this repository:
- Verify no secrets are committed (
.env, API keys, tokens, private keys). - Keep local machine paths out of tracked files (for example, absolute vault paths).
- Prefer
.env.exampletemplates for required configuration. - Ensure
node_modules, build artifacts, and local SQLite files are ignored. - Rotate any credential that was ever stored in a local
.envfile before making a repo public.