A lightweight, embedded hybrid background worker for serverless. Run safe, batched, deferred tasks directly inside Vercel / Next.js / Cloudflare Workers / AWS Lambda — no persistent worker server. Incoming traffic ("hybrid triggers") drains the queue, with strict self-limiting budgets so you never blow past a function timeout.
pnpm add hybridq
# optional drivers (pick one)
pnpm add @upstash/redis # edge / connectionless
pnpm add ioredis # node TCP- Producers call
queue.enqueue(payload)from any API route. - Jobs live in a pluggable store (
MemoryAdapterfor dev,RedisAdapterfor prod) — serverless is stateless, so the queue must be external. - Triggers drain the queue: either piggy-backing on normal inbound requests
(
withTrigger) or a dedicated authenticated endpoint (createTriggerEndpoint) that your browser or a cron pings. - Every run respects a budget (
maxJobsPerTrigger,maxExecutionTimeMs, optionalmaxCpuBudgetPct). When the budget is hit, unprocessed jobs are released back and the function returns cleanly.
Crash-safety is built in: claimed jobs hold a short lease; if the function dies mid-run, the lease lapses and the work is reclaimed — nothing is lost.
import { defineQueue, MemoryAdapter } from "hybridq/server";
const queue = defineQueue<{ to: string }>({
name: "emails",
adapter: new MemoryAdapter(),
budget: { maxJobsPerTrigger: 25, maxExecutionTimeMs: 8000 },
});
await queue.enqueue({ to: "[email protected]" });
const report = await queue.process(async (job) => {
await sendEmail(job.payload.to);
});
// { processed, failed, released, stoppedBy, elapsedMs }import { Redis } from "@upstash/redis";
import {
defineQueue,
RedisAdapter,
fromUpstash,
RedisLock,
createPayloadCipher,
} from "hybridq/server";
const driver = fromUpstash(Redis.fromEnv());
const cipher = createPayloadCipher(process.env.HYBRIDQ_ENC_KEY); // payload-at-rest
export const queue = defineQueue<{ email: string }>({
name: "emails",
adapter: new RedisAdapter(driver, { cipher }),
lock: new RedisLock(driver), // single-flight: no double-processing
budget: { maxJobsPerTrigger: 50, maxExecutionTimeMs: 9000 },
});For ioredis, reuse one socket across invocations:
import IORedis from "ioredis";
import { fromIORedis, getSharedIORedis } from "hybridq/server";
const driver = getSharedIORedis(() =>
fromIORedis(new IORedis(process.env.REDIS_URL!)),
);// app/api/orders/route.ts
import { withTrigger } from "hybridq/server";
import { queue } from "@/lib/queue";
export const POST = withTrigger(
async (req) => {
await queue.enqueue({ email: "[email protected]" });
return Response.json({ ok: true }); // user gets this immediately…
},
{
queue,
handler: async (job) => sendReceipt(job.payload.email),
// …draining happens AFTER the response, on the same warm function:
// ctx is Cloudflare's ExecutionContext; on Vercel pass `{ waitUntil }`.
skipIfEmpty: true,
},
);// app/api/_worker/route.ts
import { createTriggerEndpoint } from "hybridq/server";
import { queue } from "@/lib/queue";
export const POST = createTriggerEndpoint({
queue,
handler: async (job) => sendReceipt(job.payload.email),
auth: {
secret: process.env.WORKER_SECRET, // X-Worker-Trigger-Secret, or…
hmacSecret: process.env.WORKER_HMAC, // short-lived signed tokens
},
});The browser never holds the worker secret. Mint a short-lived HMAC token on the server and inject it into the page; the client just forwards it.
// server: mint a token for the page
import { signTrigger } from "hybridq/server";
const token = signTrigger(process.env.WORKER_HMAC!, "/api/_worker");// client: ping on activity + during idle time, throttled
import { startTriggerClient } from "hybridq/client";
const trigger = startTriggerClient({
endpoint: "/api/_worker",
token, // from the server-rendered page
throttleMs: 5000,
useIdleCallback: true,
});
// trigger.stop() on SPA unmount| Concern | Mechanism |
|---|---|
| Trigger abuse / DDoS | Pre-shared X-Worker-Trigger-Secret (constant-time compare) or replay-resistant HMAC tokens (signTrigger/verifyTrigger). |
| Sensitive payloads in Redis | Optional AES-256-GCM createPayloadCipher — payloads are sealed before they touch the store. |
| Duplicate processing | RedisLock (SET NX PX + compare-and-delete) gives single-flight drains; lease TTLs auto-recover crashes. |
hybridq/server:defineQueue/Queue,MemoryAdapter,RedisAdapter(fromUpstash,fromIORedis,getSharedIORedis),drain,RedisLock/MemoryLock,withTrigger,createTriggerEndpoint,authorizeTrigger,createPayloadCipher,signTrigger,verifyTrigger, plus all types.hybridq/client:startTriggerClient.
MIT