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.
This commit is contained in:
+6
-2
@@ -4,8 +4,12 @@ PORT=3000
|
|||||||
DATABASE_URL=postgres://continuum:continuum@localhost:6432/continuum
|
DATABASE_URL=postgres://continuum:continuum@localhost:6432/continuum
|
||||||
DATABASE_POOL_MAX=10
|
DATABASE_POOL_MAX=10
|
||||||
|
|
||||||
# Redis (state + streams)
|
# Valkey (state + streams), NOT Redis - Redis 7.4+ is RSALv2/SSPL and 8.0+
|
||||||
REDIS_URL=redis://localhost:6379
|
# 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)
|
# Cloudflare R2 (S3-compatible)
|
||||||
R2_ACCOUNT_ID=
|
R2_ACCOUNT_ID=
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# continuum-backend
|
# continuum-backend
|
||||||
|
|
||||||
Cloud control plane for Continuum. Bun + ElysiaJS, PostgreSQL (via pgCat/PgBouncer,
|
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
|
## Run
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bun install
|
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 db:generate && bun run db:migrate
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
@@ -21,8 +21,14 @@ bun run dev
|
|||||||
instead of two hand-synced ones.
|
instead of two hand-synced ones.
|
||||||
- `src/s3/` - R2 presigned URL helpers (`PUT` for client uploads, `GET` for the
|
- `src/s3/` - R2 presigned URL helpers (`PUT` for client uploads, `GET` for the
|
||||||
edge proxy).
|
edge proxy).
|
||||||
- `src/queue/` - Redis client + `XADD` helper pushing onto `stream:camera_frames`
|
- `src/queue/` - Valkey client (`ioredis`) + `XADD` helper pushing onto
|
||||||
for `continuum-ai-worker`.
|
`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
|
- `src/routes/` - `/auth`, `/farms`, `/printers`, `/jobs`, and the edge-gateway
|
||||||
WebSocket at `/ws/edge/v1`.
|
WebSocket at `/ws/edge/v1`.
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
+3
-3
@@ -3,7 +3,7 @@ import { cors } from "@elysiajs/cors";
|
|||||||
import { swagger } from "@elysiajs/swagger";
|
import { swagger } from "@elysiajs/swagger";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { redis } from "./queue";
|
import { valkey } from "./queue";
|
||||||
import { authRoutes } from "./routes/auth";
|
import { authRoutes } from "./routes/auth";
|
||||||
import { farmRoutes } from "./routes/farms";
|
import { farmRoutes } from "./routes/farms";
|
||||||
import { printerRoutes } from "./routes/printers";
|
import { printerRoutes } from "./routes/printers";
|
||||||
@@ -15,8 +15,8 @@ const app = new Elysia()
|
|||||||
.use(swagger({ path: "/docs" }))
|
.use(swagger({ path: "/docs" }))
|
||||||
.get("/health", async () => {
|
.get("/health", async () => {
|
||||||
const rows = await db.execute<{ ok: number }>(sql`select 1 as ok`);
|
const rows = await db.execute<{ ok: number }>(sql`select 1 as ok`);
|
||||||
const redisPing = await redis.ping();
|
const valkeyPing = await valkey.ping();
|
||||||
return { status: "ok", db: rows[0]?.ok === 1, redis: redisPing === "PONG" };
|
return { status: "ok", db: rows[0]?.ok === 1, valkey: valkeyPing === "PONG" };
|
||||||
})
|
})
|
||||||
.use(authRoutes)
|
.use(authRoutes)
|
||||||
.use(farmRoutes)
|
.use(farmRoutes)
|
||||||
|
|||||||
+5
-2
@@ -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";
|
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,
|
maxRetriesPerRequest: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -16,7 +19,7 @@ export interface FrameIngestionTask {
|
|||||||
|
|
||||||
/** Pushes a frame-ingestion task for continuum-ai-worker to pick up via XREADGROUP. */
|
/** Pushes a frame-ingestion task for continuum-ai-worker to pick up via XREADGROUP. */
|
||||||
export async function enqueueFrameIngestion(task: FrameIngestionTask): Promise<string> {
|
export async function enqueueFrameIngestion(task: FrameIngestionTask): Promise<string> {
|
||||||
return redis.xadd(
|
return valkey.xadd(
|
||||||
CAMERA_FRAMES_STREAM,
|
CAMERA_FRAMES_STREAM,
|
||||||
"*",
|
"*",
|
||||||
"farmId",
|
"farmId",
|
||||||
|
|||||||
Reference in New Issue
Block a user