# continuum-backend Cloud control plane for Continuum. Bun + ElysiaJS, PostgreSQL (with pgvector for frame embeddings), Valkey Streams, and Cloudflare R2 storage. ## Run ```sh 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 ```sh 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.ts` derives Elysia/TypeBox request validators straight from the Drizzle tables via `drizzle-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 (`PUT` for client uploads, `GET` for the edge proxy). - `src/queue/` - Valkey client (`ioredis`) + `XADD` helper pushing onto `stream:camera_frames` for `continuum-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 is `schemas/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_id` is 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.