- TypeScript 90.7%
- CSS 8.5%
- Dockerfile 0.5%
- Nix 0.2%
| apps | ||
| packages | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.example | ||
| .gitignore | ||
| compose.dev.yaml | ||
| compose.yaml | ||
| Dockerfile | ||
| package.json | ||
| PLAN.md | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| shell.nix | ||
| tsconfig.base.json | ||
footprint
A CAD-lite tool for interior design. Define rooms by their exact footprint — built from squares and circles combined additively or subtractively, so shapes aren't limited to four corners — then click any wall and type its exact length. Save rooms and items to a library and compose them into a plan of a whole flat.
See PLAN.md for the architecture and build order.
Working today: draw rooms from rectangles and circles combined with union and subtract — and every one of them stays a live layer you can move, resize or switch from add to subtract later —
type exact wall lengths, place doors, windows and passages, and save rooms and items to
searchable libraries at /rooms and /items — then compose them into a whole flat at
/plans, where rooms snap wall-to-wall and the plan warns when furniture overlaps or blocks
a door — and export the result as an SVG or a to-scale PDF you can print at 1:50.
Run it
The app runs in Docker:
docker compose up --build # → http://localhost:8080
Migrations run automatically on startup.
Develop
Dependencies are installed on the host and shared with the containers, so install first:
nix-shell # node 22 + pnpm + psql
pnpm install
cp .env.example .env # your uid/gid, so containers don't write as root
docker compose -f compose.yaml -f compose.dev.yaml up --build
- http://localhost:5173 — Vite with hot reload (proxies
/apiand/healthzto the API) - http://localhost:3000 — the API directly, restarting on change
localhost:5432— Postgres, for a local client
Layout
| Path | What |
|---|---|
packages/geometry |
The geometry kernel. Pure TypeScript, no DOM, no I/O. Runs in the browser and on the server — written once so boolean and constraint bugs can't diverge between them. Booleans keep curves as real arcs rather than decaying them into polylines. |
packages/schema |
zod schemas. One definition drives API validation, client forms and TypeScript types. |
apps/api |
Fastify + Drizzle + Postgres. Revalidates every submitted shape with the kernel. |
apps/web |
React + Vite. Renders to SVG, so the editor view and the export are the same thing. |
All stored geometry is integer millimetres. Floats make snapping unreliable and make "is this wall exactly 3000 mm" unanswerable; conversion to m/cm happens in the UI only.
The container image is Debian slim, not Alpine, so its glibc matches the host's nix-provided
Node and one node_modules tree works in both places.
Commands
Node and pnpm come from shell.nix — run these on the host, not in a container:
nix-shell # node 22 + pnpm + psql
pnpm install
pnpm test # all packages
pnpm --filter @footprint/geometry test # the kernel alone (no DB, no browser)
pnpm typecheck
pnpm --filter @footprint/api db:generate # new migration from the Drizzle schema
Install from the host, never from inside a container: the container is Alpine, and node_modules built against musl makes pnpm wipe and rebuild the whole tree next time you run it locally.