From ab093f3136b81cc0b5e27b2f4929e00e5e511251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Sat, 3 Oct 2026 10:10:28 +0000 Subject: [PATCH] Use Valkey instead of Redis Redis 7.4+ is RSALv2/SSPL and 8.0+ adds AGPLv3. None of the three works for a closed-source commercial cloud service, and SSPL section 13 specifically targets offering the software's functionality as a service, which is what a control plane does. Valkey is BSD-3-Clause, under the Linux Foundation, forked from Redis 7.2.4. Deciding this now rather than after step 3 of PLAN.md stands up real infrastructure makes it free: Valkey speaks the same RESP protocol, so `ioredis` is untouched and only names change. `REDIS_URL` becomes `VALKEY_URL`, the exported client becomes `valkey`, and `/health` reports a `valkey` field. Two things deliberately do not change, documented in place so they are not "fixed" later: the `redis://` URL scheme, which names the wire protocol and is what every client actually parses (none understands `valkey://`), and `ioredis` itself, which is a RESP client whose name refers to the protocol's origin and not to the server we run. docker-compose.yml is new, pinning valkey/valkey:8-alpine alongside pgvector/pgvector:pg17 - the plain postgres image lacks the extension `frame_embeddings` needs. It makes the Valkey choice concrete rather than a note someone has to remember. --- .env.example | 8 ++++++-- README.md | 14 ++++++++++---- docker-compose.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 6 +++--- src/queue/index.ts | 7 +++++-- 5 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example index 21c027c..732a6b5 100644 --- a/.env.example +++ b/.env.example @@ -4,8 +4,12 @@ PORT=3000 DATABASE_URL=postgres://continuum:continuum@localhost:6432/continuum DATABASE_POOL_MAX=10 -# Redis (state + streams) -REDIS_URL=redis://localhost:6379 +# Valkey (state + streams), NOT Redis - Redis 7.4+ is RSALv2/SSPL and 8.0+ +# adds AGPLv3, none of which we can ship commercially. Valkey is BSD-3-Clause +# and wire-compatible. The URL scheme stays `redis://` because that names the +# RESP wire protocol, which is what the client libraries parse - no client +# understands a `valkey://` scheme. +VALKEY_URL=redis://localhost:6379 # Cloudflare R2 (S3-compatible) R2_ACCOUNT_ID= diff --git a/README.md b/README.md index 058f2be..0b76e71 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # continuum-backend Cloud control plane for Continuum. Bun + ElysiaJS, PostgreSQL (via pgCat/PgBouncer, -with pgvector for frame embeddings), Redis Streams, and Cloudflare R2 storage. +with pgvector for frame embeddings), Valkey Streams, and Cloudflare R2 storage. ## Run ```sh bun install -cp .env.example .env # fill in DATABASE_URL, REDIS_URL, R2_*, SUPABASE_JWT_SECRET +cp .env.example .env # fill in DATABASE_URL, VALKEY_URL, R2_*, SUPABASE_JWT_SECRET bun run db:generate && bun run db:migrate bun run dev ``` @@ -21,8 +21,14 @@ bun run dev instead of two hand-synced ones. - `src/s3/` - R2 presigned URL helpers (`PUT` for client uploads, `GET` for the edge proxy). -- `src/queue/` - Redis client + `XADD` helper pushing onto `stream:camera_frames` - for `continuum-ai-worker`. +- `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`. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f37c356 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +# Local development dependencies for continuum-backend. +# +# Valkey rather than Redis on purpose: Redis 7.4+ is RSALv2/SSPL and 8.0+ +# adds AGPLv3, none of which a closed-source commercial product can ship. +# Valkey is BSD-3-Clause and speaks the same RESP protocol, so both clients +# (`ioredis` here, the `redis` crate in continuum-ai-worker) are unchanged. +# See continuum-common/docs/LICENSES.md. +# +# pgCat/PgBouncer is deliberately not here - DATABASE_URL in .env.example +# points at :6432 for production, but talking straight to Postgres on :5432 +# is one less moving part locally. + +services: + valkey: + image: valkey/valkey:8-alpine + ports: + - "6379:6379" + command: ["valkey-server", "--appendonly", "yes"] + volumes: + - valkey-data:/data + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 5s + retries: 5 + + postgres: + # pgvector/pgvector ships the extension prebuilt; the plain postgres + # image does not, and `frame_embeddings` needs it. + image: pgvector/pgvector:pg17 + ports: + - "5432:5432" + environment: + POSTGRES_USER: continuum + POSTGRES_PASSWORD: continuum + POSTGRES_DB: continuum + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U continuum -d continuum"] + interval: 5s + retries: 5 + +volumes: + valkey-data: + postgres-data: diff --git a/src/index.ts b/src/index.ts index 79f5bfe..f77bb96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { cors } from "@elysiajs/cors"; import { swagger } from "@elysiajs/swagger"; import { sql } from "drizzle-orm"; import { db } from "./db"; -import { redis } from "./queue"; +import { valkey } from "./queue"; import { authRoutes } from "./routes/auth"; import { farmRoutes } from "./routes/farms"; import { printerRoutes } from "./routes/printers"; @@ -15,8 +15,8 @@ const app = new Elysia() .use(swagger({ path: "/docs" })) .get("/health", async () => { const rows = await db.execute<{ ok: number }>(sql`select 1 as ok`); - const redisPing = await redis.ping(); - return { status: "ok", db: rows[0]?.ok === 1, redis: redisPing === "PONG" }; + const valkeyPing = await valkey.ping(); + return { status: "ok", db: rows[0]?.ok === 1, valkey: valkeyPing === "PONG" }; }) .use(authRoutes) .use(farmRoutes) diff --git a/src/queue/index.ts b/src/queue/index.ts index 799da70..a11ad02 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -1,6 +1,9 @@ +// ioredis talks RESP, so it drives Valkey unchanged - see docs/LICENSES.md in +// continuum-common for why the server is Valkey and not Redis. The `redis://` +// scheme names the wire protocol, not the product. import Redis from "ioredis"; -export const redis = new Redis(process.env.REDIS_URL ?? "redis://localhost:6379", { +export const valkey = new Redis(process.env.VALKEY_URL ?? "redis://localhost:6379", { maxRetriesPerRequest: 3, }); @@ -16,7 +19,7 @@ export interface FrameIngestionTask { /** Pushes a frame-ingestion task for continuum-ai-worker to pick up via XREADGROUP. */ export async function enqueueFrameIngestion(task: FrameIngestionTask): Promise { - return redis.xadd( + return valkey.xadd( CAMERA_FRAMES_STREAM, "*", "farmId",