Development Environment
Local development runs in DDEV (≥ 1.24): containers for Node 22 (corepack/pnpm enabled) and Postgres 16 — no host-side toolchain required.
ddev start # boot web (Node 22) + db (Postgres 16)
ddev exec pnpm install # install the workspace INSIDE the container (Linux binaries)
ddev dev # start the full dev stack (API + web); Ctrl-C to stop
ddev dev runs pnpm dev (Turborepo) in the web container: the API server
(tsx watch, applies migrations on boot) and the Vite dev server with HMR, both
served through the DDEV router over TLS.
| Service | URL | Notes |
|---|---|---|
| App (Vite) | https://basalt.ddev.site:5173 |
open this — the browser origin; proxies /api to the server |
| API (direct) | https://basalt.ddev.site:3443 |
for curl/health; /api/v1/health |
| Postgres | via ddev psql |
host db, db/user/pass db |
The dev environment is wired in .ddev/config.yaml (web_environment:
DATABASE_URL, BASE_URL, PORT, NODE_ENV) and .ddev/.env.web
(AUTH_SECRET, gitignored — regenerate with openssl rand -hex 32).
BASE_URL is the Vite origin so Better-Auth cookies and the CSRF origin check
line up; vite.config.ts binds all interfaces and points HMR at the DDEV host
when IS_DDEV_PROJECT is set (a plain host pnpm dev is unaffected).
WebSockets (collab plane): .ddev/traefik/config/basalt-alpn-h1.yaml
restricts the DDEV router to HTTP/1.1 (ALPN). Over HTTP/2 the collab
WebSocket's upgrade completes but Traefik does not bridge its data frames to
the HTTP/1.1 Node backend, so the editor would sit on "Connecting…". The file
is not #ddev-generated; delete it to restore HTTP/2 (and lose live collab in
this dev setup).
Conventions:
- Run all toolchain commands (
pnpm install,pnpm dev,pnpm test) inside the container (ddev sshorddev exec …) so every machine shares the same Node and Postgres.node_modulesholds Linux binaries once installed in the container — do not mix with a hostpnpm install(they overwrite each other; pick one and stay there). - DDEV is the development environment only. The tested self-host
artifact is the Docker compose under
infra/docker/(ADR-0008); CI runs against compose, not DDEV. .ddev/config.yamlis committed. Local overrides belong in.ddev/config.local.yaml(ignored by DDEV's own.gitignore).
Toolchain: pnpm + Turborepo
pnpm is provided by corepack — no global install:
corepack enable pnpm # once per machine/container
pnpm install # install the whole workspace
The exact pnpm version is pinned in root package.json#packageManager;
corepack picks it up automatically. Node version: .nvmrc (24, engines allow
= 22).
Root scripts (run from the repo root):
| Command | What it does |
|---|---|
pnpm dev |
turbo run dev — app dev servers |
pnpm typecheck |
turbo run typecheck — tsc --noEmit per package/app |
pnpm lint |
ESLint (one flat config at the root) |
pnpm test |
turbo run test — Vitest per package/app |
pnpm build |
turbo run build — apps only; packages are consumed as TS source |
pnpm format |
Prettier over the repo |
Workspace layout: apps/*, packages/*, and e2e (see
pnpm-workspace.yaml). Internal packages export src/index.ts directly — no
package build step, no watch mode needed; editing a package is immediately
visible to apps.
E2E tests
e2e/ (@basalt/e2e) holds the Playwright end-to-end suite. It tests the
self-host artifact: the run builds the Docker image, boots the compose
stack from infra/docker/compose.selfhost.yml under an isolated compose
project (basalt-e2e — own containers, network, and volume; throwaway
credentials, nothing written to disk), waits for /api/v1/health, runs the
tests against http://localhost:8080, and tears everything down again with
down -v. Nothing survives between runs.
pnpm --filter @basalt/e2e exec playwright install chromium # once per machine
pnpm --filter @basalt/e2e e2e # build + boot stack, test, tear down
E2E_PORT=8090 pnpm --filter @basalt/e2e e2e # if host port 8080 is taken
Requirements: Docker with compose v2.24+ on the host — run this from the
host, not inside DDEV (it drives the host's Docker daemon and publishes a host
port). Because it boots real containers, e2e is deliberately not part of
pnpm test; CI runs it as its own job (.github/workflows/ci.yml, job
e2e).