Runtime Abstraction Layer (@lumibase/runtime)
LumiBase có thể chạy trên hai runtime hoàn toàn khác nhau:
- Cloudflare Workers — KV, R2, Hyperdrive, Durable Objects, Queues, CF Image Resizing.
- Docker / Node.js — Redis, MinIO/S3, PostgreSQL pool, MeiliSearch self-host, BullMQ, Imgproxy.
Toàn bộ business logic (routes, services, middleware) không gọi trực tiếp vào API của Cloudflare hay Node — mà đi qua các interface trong @lumibase/runtime.
Vị trí code
packages/runtime/
├── src/
│ ├── interfaces/ # Định nghĩa interface (cache/storage/database/search/queue/media)
│ ├── adapters/
│ │ ├── cloudflare/ # Implementation cho Cloudflare Workers
│ │ └── docker/ # Implementation cho Docker/Node
│ ├── factory.ts # createRuntime(env) chọn theo LUMIBASE_RUNTIME
│ ├── index.ts # entry "." — chỉ phần an toàn cho Worker
│ ├── docker.ts # entry "./docker" — adapter Docker
│ └── node.ts # entry "./node" — createRuntime + leader lock
└── package.json # deps: ioredis, @aws-sdk/client-s3, postgres, meilisearch, bullmq, prom-client
Ba entry point, và vì sao điều đó quan trọng
| Import | Chứa gì | An toàn trong Worker? |
|---|---|---|
@lumibase/runtime | Interface, cache helper, memory provider, xử lý key dùng chung, adapter Cloudflare | Có |
@lumibase/runtime/docker | Adapter Docker — Redis, BullMQ, S3, MeiliSearch, Imgproxy | Không |
@lumibase/runtime/node | createRuntime (rẽ nhánh theo LUMIBASE_RUNTIME) và leader lock dùng Redis | Không |
Việc tách này không phải chuyện hình thức. Trước đây root của package re-export
mọi thứ, nên bundle Cloudflare Worker chứa cả bullmq, ioredis và
@aws-sdk/client-s3 dù Worker không bao giờ dùng tới. Rồi BullMQ 6 thêm backend
Postgres, và sql-loader của nó resolve đường dẫn thư mục của chính nó ở top
level module, throw khi không có __dirname lẫn frame file:/// — đúng môi
trường một Worker đã bundle. Worker ngừng khởi tạo được, và Cloudflare từ chối
mọi deploy với validation error 10021.
Các quy tắc suy ra từ đó:
- Business logic chỉ import từ root. Cần
RuntimeContexthay một provider interface thì import từ@lumibase/runtime— đó là type và bị xoá lúc build. - Entry point Node (
apps/cms/src/serve.ts, script CLI trongapps/cms/scripts/) được phép import/nodevà/docker. - Đừng bao giờ import
/dockertừ bất cứ thứ gì màapps/cms/src/index.tschạm tới. Module đó là app của Worker.await import()động cũng không phải cửa thoát: bundler vẫn inline target, nênwrangler.tomlalias subpath này sang một stub cho các bản build Worker. - Muốn thêm export vào root? Nếu module của nó cần một built-in của Node hoặc
một package chỉ dành cho Node dưới dạng value import, nó thuộc về subpath.
import typethì luôn ổn.
Hai gate CI canh việc này, và chúng không thay thế được nhau:
# assert bundle Worker không chứa code chỉ dành cho Docker
pnpm verify:worker-bundle
# boot Worker bằng workerd
pnpm verify:worker-startup
verify:worker-bundle mới là hàng rào thật. verify:worker-startup không
bắt được class lỗi này — khi cố tình đưa adapter Docker trở lại, wrangler dev
vẫn boot thành công, vì fallback quét stack của BullMQ tìm được frame file:///
ở local nhưng không có trong Worker đã deploy. Hãy đọc startup probe xanh là
"không có throw top-level chạy ngay", chứ đừng đọc thành "cái này deploy được".
6 Provider interfaces
// packages/runtime/src/interfaces/runtime.ts
export interface RuntimeContext {
cache: CacheProvider;
storage: StorageProvider;
database: DatabaseProvider;
search: SearchProvider;
queue: QueueProvider;
media: MediaProcessor;
runtime: 'cloudflare' | 'docker';
}
CacheProvider
get(key: string): Promise<string | null>
set(key: string, value: string, ttl?: number): Promise<void>
delete(key: string): Promise<void>
| Cloudflare | Docker |
|---|---|
Cloudflare KV (env.CONFIG_CACHE) | Redis qua ioredis |
StorageProvider
put(key: string, data: ArrayBuffer | Uint8Array, metadata?: Record<string, string>): Promise<void>
get(key: string): Promise<StorageObject | null>
delete(key: string): Promise<void>
list(prefix: string): Promise<StorageObject[]>
| Cloudflare | Docker |
|---|---|
R2 (env.MEDIA) | S3-compatible MinIO qua @aws-sdk/client-s3 |
DatabaseProvider
getConnection(): DrizzleDatabase
| Cloudflare | Docker |
|---|---|
| Hyperdrive connection string | pg-pool (driver postgres) |
SearchProvider
index(collection: string, documents: Doc[]): Promise<void>
search(collection: string, query: string, options?: SearchOptions): Promise<SearchResult>
delete(collection: string, ids: string[]): Promise<void>
getIndex(collection: string): Promise<IndexInfo>
| Cloudflare | Docker |
|---|---|
| MeiliSearch Cloud qua HTTP | MeiliSearch self-host |
QueueProvider
enqueue(queueName: string, job: Job): Promise<string>
process(queueName: string, handler: (job: Job) => Promise<void>): void
getStatus(jobId: string): Promise<JobStatus>
| Cloudflare | Docker |
|---|---|
| Cloudflare Queues | BullMQ (chạy trên Redis có sẵn) |
Hỗ trợ priority high / normal / low, retry 3 lần với exponential backoff.
MediaProcessor
transform(key: string, options: TransformOptions): Promise<string> // returns URL
getUrl(key: string, transformations: TransformOptions): string
| Cloudflare | Docker |
|---|---|
| CF Image Resizing | Imgproxy với signed URLs |
Operations: resize, crop, format conversion (WebP/AVIF), quality.
Factory & middleware
// packages/runtime/src/factory.ts
export function createRuntime(env: Record<string, unknown>): RuntimeContext {
const mode = (env.LUMIBASE_RUNTIME as string) || 'docker';
if (mode === 'cloudflare') return createCloudflareRuntime(env);
return createDockerRuntime(env);
}
Middleware withRuntime() (apps/cms/src/middleware/runtime.ts) gọi factory này và inject vào c.set('runtime', ...) ở mọi request — chạy ngay sau logger/metrics.
// Sử dụng trong route:
app.get('/example', (c) => {
const cache = c.get('runtime').cache;
const storage = c.get('runtime').storage;
// ...
});
Environment parity
Bộ adapter phải đảm bảo:
- Cùng API responses cho mọi content CRUD bất kể runtime.
- TTL behavior nhất quán giữa KV và Redis.
- Storage operations identical (list/get/put/delete) giữa R2 và MinIO.
- Cùng Drizzle schema và migrations cho cả hai.
- Khi feature unavailable trên một runtime (ví dụ Durable Objects chỉ có CF), log warning và degrade — không fail.
Selecting runtime
# Trên Cloudflare (mặc định khi deploy bằng Wrangler):
LUMIBASE_RUNTIME=cloudflare
# Trên Docker / self-hosted:
LUMIBASE_RUNTIME=docker
Xem apps/docs/content/deployment/environment-variables.md để biết danh sách env vars đầy đủ.
CDC runtime split
ClickHouse CDC uses the same runtime abstraction for the API/control-plane surface, but the stateful replication workers are intentionally not executed inside Cloudflare Workers:
- Cloudflare Workers may host the authenticated CDC API routes and Redis/KV cache invalidation edge components.
- Docker / managed services host Debezium/Kafka, ClickHouse materialized replication, Airbyte, health polling, and long-running deployment steps.
The CDC deployment target is explicit in API payloads via
target: "docker_compose" | "cloudflare_workers". Runtime-specific services
must keep this target explicit instead of silently starting a stateful connector
inside the Workers isolate.
Test strategy
- Unit tests cho từng adapter trong
packages/runtime/src/__tests__/:cloudflare-adapters.test.ts(mock KV/R2).docker-adapters.test.ts(mock ioredis, S3 client, MeiliSearch).
- Integration tests chạy thật stack qua
docker compose uptrong CI workflow.github/workflows/docker.yml.