Building a dashboard needs the backend, the proxy with its go2rtc, the web console with hot reload and a seeded farm to stay up while it is being edited, and e2e-camera.sh already boots exactly that. SERVE=1 skips every check, seeds the user as owner (the dashboard has to command and manage the org) with every printer in printers.toml, named as that file names them, and holds until SIGTERM or Ctrl-C, when the usual cleanup removes the farm, the processes and the sign-in file. The password is written to a 0600 file under umask 077 and only the path is printed. The web console binds loopback unless APP_HOST says otherwise, because it serves a password sign-in. Verified: signing in through the real form against the live stack shows Live and both printers (the H2C live, the P1P honestly "offline, Never reported" because it was switched off); SIGTERM leaves no process, port, login file or seeded farm behind.
continuum-backend
Cloud control plane for Continuum. Bun + ElysiaJS, PostgreSQL (with pgvector for frame embeddings), Valkey Streams, and Cloudflare R2 storage.
Run
bun install
scripts/dev-env.sh up # Valkey + Postgres/pgvector, and print the URLs
scripts/dev-env.sh schema # apply drizzle/ - same as bun run db:migrate
cp .env.example .env # fill in the URLs it printed, plus R2_*, SUPABASE_JWT_SECRET
bun run dev
scripts/dev-env.sh uses docker-compose.yml when a Docker daemon is
answering, and falls back to the same two services from apt when there is no
usable container runtime - some sandboxes cannot run Docker at all, because
dockerd needs CAP_NET_ADMIN for its bridge and user namespaces to unpack an
image, and neither can be granted from inside. scripts/dev-env.sh status
says which backend is in use.
Schema changes go through migrations: edit src/db/schema/, run
bun run db:generate, and commit the file it writes to drizzle/ along with
the schema change. scripts/dev-env.sh push reshapes a local database straight
from the schema with no migration file, which is faster while a table is still
moving but records nothing and will drop a column to make things match - so
generate before committing. drizzle/0000_*.sql creates the vector extension
itself; that line is hand-added, since drizzle-kit does not emit extension
statements and the migration would otherwise only apply to a database somebody
had already prepared.
Test
bun test # unit + protocol contract tests, no services needed
scripts/e2e-edge.sh # the whole edge path: real database, real printers
scripts/e2e-edge.sh seeds a farm, claims its printers, mints an edge token,
runs the real continuum-proxy against this backend, and asserts that live
telemetry reached Postgres and that a command reached a printer. It only ever
sends a fan speed, and only to a printer reporting IDLE.
scripts/capture-gateway-frames.js is the smaller tool: it stands in for this
backend and records what a gateway sends, for refreshing the fixtures in
tests/fixtures/.
Layout
src/db/- Drizzle schema (printers,farms,print_jobs,frame_embeddings) and client, pointed straight at Postgres.src/db/validators.tsderives Elysia/TypeBox request validators straight from the Drizzle tables viadrizzle-typebox, so the Postgres schema and the API validators come from one definition instead of two hand-synced ones.src/s3/- R2 presigned URL helpers (PUTfor client uploads,GETfor the edge proxy).src/queue/- Valkey client (ioredis) +XADDhelper pushing ontostream:camera_framesforcontinuum-ai-worker.
Valkey, not Redis: Redis 7.4+ is RSALv2/SSPL and 8.0+ adds AGPLv3, none of
which a commercial product can ship. Valkey is BSD-3-Clause and speaks the
same RESP protocol, so ioredis is unchanged - only the server differs. See
continuum-common/docs/LICENSES.md. docker-compose.yml here brings up
Valkey and Postgres+pgvector for local development.
src/routes/-/auth,/farms,/printers,/jobs, and the edge-gateway WebSocket at/ws/edge/v1. That route's wire format is not defined here: it isschemas/continuum/v1/edge/in continuum-schemas, validated through its TypeBox mirror in@continuum/ts-types, because both ends of it are separate programs in different languages and they had already drifted apart once.printers.gateway_printer_idis what maps a gateway's own printer ids ("p1p") onto rows here; telemetry for an unclaimed id is logged and skipped, never inserted.
frame_embeddings.embedding requires the vector extension. The first
migration creates it, and scripts/dev-env.sh up also does so for the push
path, which does not run migrations.
Dependency pins
elysia, @sinclair/typebox and the drizzle-* packages are pinned tightly
on purpose, not out of caution. Caret ranges here resolved into a combination
that could not boot: elysia: ^1.1.26 installed 1.4.30, which calls
t.Module and so needs TypeBox >=0.34, while drizzle-typebox: ^0.2.1
installed a version whose peers demand drizzle-orm >=0.36 against the
^0.33.0 pinned here. Typechecking passed throughout - only starting the
server failed. tests/dependency_identity.test.ts guards the part of this that
can silently come back.