Files
pic/CLAUDE.md
T
roof dde4d9a53f
Unit Tests / test (push) Successful in 8m54s
Rewrite CLAUDE.md following article best practices
Adds: tech stack, coding conventions, file placement rules, safety rules,
infrastructure topology table, and expands architecture with key-file table
and before-request hook documentation. Removes vague guidance, replaces
with actionable rules Claude can follow automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 07:25:53 -04:00

13 KiB

CLAUDE.md

This file is the primary context source for Claude Code in this repository. Read it fully before touching any code.


Project Overview

Personal Internet Cell (PIC) is a self-hosted digital infrastructure platform for individuals who want full ownership of their core internet services without relying on cloud providers.

A PIC instance runs DNS, DHCP, NTP, WireGuard VPN, email (SMTP/IMAP), calendar/contacts (CalDAV/CardDAV), file storage (WebDAV), HTTPS reverse proxy (Caddy), an internal certificate authority, and optional third-party services — all managed from a single REST API and a React web UI. No manual config-file editing is required for normal operations.

Primary users: technically capable individuals, homelab operators, small families or teams.

What the product optimizes for:

  • One-command install, browser-based first-run wizard, no manual .env editing for identity
  • Everything managed through the API and UI — the user should never need to ssh for day-to-day operations
  • Security by default: session auth, CSRF protection, WireGuard isolation, internal CA, no open API port
  • Reliability and observability: structured logs, health monitoring, automated config backups

Key constraints:

  • Runs on a single Linux host with Docker; no Kubernetes, no swarm
  • Must work on Debian, Ubuntu, Fedora, RHEL, and Alpine
  • The Flask API must never be exposed directly; Caddy always proxies it
  • All secrets live in data/ (git-ignored), never in the repo

Tech Stack

Backend

  • Python 3.11 — Flask REST API (api/app.py)
  • Flask — routing, sessions, before-request hooks (enforce_setup, enforce_auth, check_csrf)
  • bcrypt — password hashing in AuthManager
  • Docker SDK for Python — container lifecycle in ContainerManager
  • PyNaCl / Age — encryption in VaultManager
  • pyotp — TOTP for DDNS registration

Frontend

  • React 18 — SPA
  • Vite — dev server and build (proxies /api:3000)
  • Tailwind CSS — all styling; no custom CSS files
  • Axios — all API calls go through src/services/api.js

Infrastructure

  • Docker Compose — all 12+ service containers
  • Caddy — reverse proxy, TLS termination (Let's Encrypt DNS-01 or HTTP-01 or internal CA)
  • CoreDNS.cell TLD authoritative DNS
  • dnsmasq — DHCP
  • chrony — NTP
  • WireGuard — VPN (kernel module, not userspace)
  • Postfix + Dovecot — email via docker-mailserver
  • Radicale — CalDAV/CardDAV
  • PowerDNS — authoritative DNS on the DDNS VPS (separate repo: pic-ddns)

CI/CD

  • Gitea Actions — unit tests on every push, image builds on tag
  • act_runner — self-hosted runner on pic0 (192.168.31.51)
  • Gitea Container Registry — images pushed to git.pic.ngo

Do not introduce: Redux, styled-components, SQLAlchemy, Celery, or any async framework (asyncio/FastAPI) into the main API unless explicitly requested.


Architecture

Browser / WireGuard peer
  └── Caddy (:80/:443)           TLS termination, reverse proxy
        └── React SPA (:8081)    Vite + Tailwind (Nginx in container)
        └── Flask API (:3000)    REST API, bound to 127.0.0.1 only
              ├── NetworkManager       CoreDNS, dnsmasq, chrony
              ├── WireGuardManager     WireGuard peer lifecycle
              ├── PeerRegistry         peer registration and trust
              ├── EmailManager         Postfix + Dovecot
              ├── CalendarManager      Radicale CalDAV/CardDAV
              ├── FileManager          WebDAV + Filegator
              ├── RoutingManager       iptables NAT and routing
              ├── FirewallManager      iptables INPUT/FORWARD rules
              ├── VaultManager         internal CA, TLS certs, Age encryption
              ├── ContainerManager     Docker SDK
              ├── CellLinkManager      site-to-site WireGuard links
              ├── ConnectivityManager  per-peer exit routing (WG ext, OpenVPN, Tor)
              ├── DDNSManager          dynamic DNS heartbeat
              ├── ServiceStoreManager  optional service install/remove
              ├── CaddyManager         Caddyfile generation and reload
              ├── AuthManager          bcrypt passwords, session auth, RBAC
              └── SetupManager         first-run wizard state

Key files

File Role
api/app.py Flask app, all REST endpoints, before-request hooks, health monitor thread
api/managers.py Singleton instantiation of all service managers
api/base_service_manager.py Abstract base class: get_status, get_config, update_config, validate_config, test_connectivity, get_logs, restart_service
api/config_manager.py Single source of truth for cell_config.json — all read/write goes through here
api/service_bus.py Pub/sub event system between managers
webui/src/services/api.js Axios API client — all UI→API calls
docker-compose.yml Container definitions and network topology
Makefile All operational commands
install.sh Bash installer served via https://install.pic.ngo

Directory layout

api/           Flask API and all service managers
webui/         React SPA (Vite + Tailwind)
tests/         pytest unit tests (no running services required)
tests/integration/  require a running PIC stack
tests/e2e/     Playwright UI and WireGuard e2e tests
config/        Runtime config per service (mostly git-ignored)
data/          Runtime secrets and state (fully git-ignored)
scripts/       Setup and maintenance scripts
install.sh     One-line installer entry point
Makefile       All make targets
docker-compose.yml

Config and secrets

  • Runtime config: config/api/cell_config.json — managed by ConfigManager, never edit directly
  • Secrets and user data: data/ — git-ignored, contains auth_users.json, WireGuard keys, DDNS token, CA key
  • DDNS config lives under the top-level ddns key in cell_config.json, accessed via config_manager.configs.get('ddns', {})
  • Do not read _identity.domain expecting a dict — it is a plain string (the domain mode, e.g. "pic_ngo")

Before-request hooks (app.py)

Three hooks run on every request in this order:

  1. enforce_setup — returns 428 for all /api/* except /api/setup/* and /health until setup is complete. Skipped when app.config['TESTING'] is True.
  2. enforce_auth — returns 401 if no session; returns 503 if users file exists but is empty (misconfiguration). Skipped when app.config['TESTING'] is True.
  3. check_csrf — requires X-CSRF-Token header on all mutating requests except /api/auth/* and /api/setup/*.

Coding Conventions

Python (API)

  • All managers inherit BaseServiceManager — always implement all abstract methods
  • Use self.logger (from BaseServiceManager) — never print() or raw logging
  • Config reads go through self.config_manager — never open cell_config.json directly
  • Use threading.RLock for shared state; managers run in a multi-threaded Flask app
  • Do not use any typing; be explicit
  • Keep Flask route handlers thin — business logic belongs in the manager, not in app.py
  • Error responses must be JSON: jsonify({'error': '...'}), <status_code>
  • Do not catch bare Exception and silently swallow it — log at minimum

JavaScript (webui)

  • All API calls go through src/services/api.js — never use fetch or a new Axios instance directly
  • Use functional components; no class components
  • Tailwind utilities only — no inline styles, no custom CSS files
  • Keep page components in src/pages/, reusable UI in src/components/
  • State: local useState/useEffect is fine; no Redux or global state library

General

  • No comments that describe what the code does — only why if non-obvious
  • No dead code, no commented-out blocks
  • No backwards-compat shims for things being removed
  • Prefer editing existing files over creating new ones
  • Tests that write to disk: mock builtins.open with OSError rather than relying on /nonexistent/path (CI runs as root and can create any path)

Testing and Quality

Before considering any task complete:

  1. Run make test — all 1500+ unit tests must pass
  2. Fix failures before committing — the pre-commit hook will block the commit anyway

Rules

  • Use unittest.mock / pytest-mock for all Docker, filesystem, and subprocess calls
  • Tests must pass in CI (rootless environment where filesystem assumptions don't hold)
  • When testing write-failure paths, mock builtins.open with side_effect=OSError — do not rely on unwritable paths
  • Integration tests (tests/integration/) require a running stack — exclude from CI with --ignore=tests/integration
  • E2e tests (tests/e2e/) require Playwright — exclude from CI with --ignore=tests/e2e
  • Add tests for any new API endpoint, manager method, or utility function
  • Do not add tests for Flask routing boilerplate or trivial getters — test behaviour, not structure

File Placement Rules

New thing Where it goes
New service manager api/<name>_manager.py, registered in api/managers.py and wired into app.py
New API endpoints app.py — grouped with the relevant manager's existing endpoints
New React page webui/src/pages/
Reusable UI component webui/src/components/
New pytest test file tests/test_<module>.py
Operational script scripts/
Documentation Update README.md, QUICKSTART.md, or Personal Internet Cell – Project Wiki.md as appropriate

Do not create a new abstraction for a single use case. Do not create near-duplicate files — edit the existing one.


Safety Rules

  • Never expose the Flask API port (3000) directly — it must always be behind Caddy
  • Never commit secretsdata/, .env, *.key, *.pem are all git-ignored; keep it that way
  • Do not modify enforce_setup or enforce_auth hooks without understanding the full auth flow — these are the security boundary
  • Do not change the cell_config.json schema without updating ConfigManager validation and all manager reads
  • Do not rename API route paths without checking the webui api.js client and any external callers
  • Do not modify WireGuard key generation — losing the server private key means all peers must be re-provisioned
  • Flag any change to auth flow, CSRF logic, or session management as security-sensitive before implementing

Commands

# Stack lifecycle (always use make — never call docker/docker-compose directly)
make start           # build and start all containers
make stop            # stop all containers
make restart         # restart containers
make status          # container status + API health check
make logs            # follow all container logs
make logs-api        # follow API logs only
make logs-caddy      # follow Caddy logs
make shell-api       # shell inside the API container
make build-api       # rebuild API image after code change
make build-webui     # rebuild webui image after code change

# Tests
make test            # pytest tests/ --ignore=tests/e2e --ignore=tests/integration
make test-coverage   # coverage report in htmlcov/
pytest tests/test_<module>.py -v   # single test file

# Local dev (no Docker)
pip install -r api/requirements.txt
python3 api/app.py              # Flask API on :3000

cd webui && npm install && npm run dev   # React UI on :5173 (proxies /api → :3000)

# Peer / WireGuard
make list-peers
make show-routes

# Admin password
make show-admin-password
make reset-admin-password

# Backup / restore
make backup
make restore

# Maintenance
make update          # git pull + rebuild + restart
make uninstall       # stop containers; prompt to delete config/ and data/

Infrastructure Topology

Machine IP Role
pic0 192.168.31.51 Dev machine — you are here. Run all commands directly.
pic1 192.168.31.52 Test/staging PIC instance
Gitea 192.168.31.50 Self-hosted git server (gitea@192.168.31.50:roof/pic.git)
DDNS VPS 192.168.31.101 (LAN) / 178.168.15.65 (public) PowerDNS + FastAPI for *.pic.ngo DDNS

The roof user on pic0 has passwordless sudo and is in the docker group — use both freely.


AI Collaboration Rules

These rules apply to every Claude Code session in this repo:

  • Read memory first — load /home/roof/.claude/projects/-home-roof/memory/MEMORY.md at session start; follow referenced memory files for relevant context.
  • You are on pic0 — execute commands directly here; do not ask the user to run them.
  • make is the only container interface — never call docker or docker-compose directly. All container lifecycle goes through make start, make stop, make build, make logs, etc.
  • Use specialized agents — spawn pic-remote for VPS/pic1 SSH tasks, pic-qa for test writing, pic-architect for design decisions, pic-designer for UI review, pic-devops for docker-compose/Makefile changes, pic-writer for documentation.
  • Test before commit — run make test and fix all failures before staging. The pre-commit hook enforces this, but run it manually first.
  • No skipping hooks — never use --no-verify unless the only change is documentation or a workflow file with no Python/JS.
  • Commits need context — write commit messages that explain why, not just what. Always add the Co-Authored-By trailer.