Environments & Microservices
Project/environment isolation, per-definition environment gating, and microservice placement
Environments & Microservices
Three orthogonal mechanisms control where your definitions run and whose data they see. Understanding them prevents the two classic distributed-deployment surprises: jobs invisible across services, and staging crons firing in production.
1. project — the isolation boundary
Every piece of state — Redis keys, broker queues, Postgres rows — is scoped by project + environment:
const oqron = new Oqron({
mode: "redis-postgres",
project: "shop", // ← the logical application
environment: "production", // ← the deployment stage
redis: process.env.REDIS_URL,
postgres: process.env.DATABASE_URL,
});Services that share queues MUST share the same project. A producer with
project: 'shop' and a consumer with project: 'shop-workers' write to
different keyspaces on the same Redis — the consumer will never see the jobs.
This is the #1 cause of "distributed worker isn't receiving anything".
Isolation is symmetric on both planes:
- Redis: every key is prefixed
oqron:<project>:<environment>:… - Postgres:
projectandenvironmentare real columns in every table
So one Redis + one Postgres can safely serve many apps and stages.
2. environments — per-definition gating
Every definition type accepts an environments allow-list. Absent = runs everywhere. Present = the definition is inert in any environment not listed — it doesn't schedule, consume, or seed records there:
// Fires ONLY in production — staging/dev deployments ignore it entirely
Cron({
name: "nightly-billing",
expression: "0 0 * * *",
environments: ["production"],
handler: runBilling,
});
// Consumer active in production + staging, never in dev
Worker({ topic: "emails", environments: ["production", "staging"], handler });
// Works on every module: Queue, Schedule, Webhook, RateLimit, Cache, Topic, Subscription
RateLimit({ name: "api", environments: ["production"], tiers: [/* ... */] });Gating happens at two layers:
- A module whose definitions are all gated out doesn't even boot in that environment (used = enabled respects gating).
- In mixed modules, only the matching definitions enter the engine's working set.
It works identically in memory mode — gating is a pure in-process check against config.environment, independent of the backend.
3. Definition placement — which service runs what
A service only runs the modules it defines. That's the whole microservice story — no extra configuration:
// API node: produces jobs, runs nothing heavy
const reports = Queue<{ id: string }>({ name: "reports" }); // publisher-only (no handler)
const oqron = new Oqron({
mode: "redis",
project: "shop",
environment: "production",
redis: URL,
});
await oqron.start();
// → only the queue module boots here, and only as a producer// Worker node: consumes, no HTTP concerns
Worker<{ id: string }>({
topic: "reports",
concurrency: 10,
handler: buildReport,
});
const oqron = new Oqron({
mode: "redis",
project: "shop",
environment: "production",
redis: URL,
});
await oqron.start();
// → only the worker module boots here// Dedicated scheduler node: crons that enqueue work for the workers
Cron({
name: "hourly-report",
expression: "0 * * * *",
handler: async () => {
await reports.add({ id: newReportId() });
},
});Same project + same Redis = they cooperate. Different binaries, different scaling, zero code rewrites between monolith and microservices — a single process defining everything behaves identically.
Putting it together
Cron({
name: "cleanup",
expression: "*/30 * * * *",
environments: ["production", "staging"], // never in dev
durable: true, // crash-safe fires (see Scheduler docs)
handler: cleanup,
});| Question | Mechanism |
|---|---|
| "Why can't my worker see the producer's jobs?" | Same project + environment required |
| "Don't run this cron in staging" | environments: ['production'] |
| "Run heavy processing on separate machines" | Define Worker() only in the worker service |
| "One Redis for staging + production?" | Yes — isolation is automatic per environment |
| "One Postgres for three apps?" | Yes — project column isolates rows |
Leader election & multi-node scheduling
When the same scheduler definitions run on multiple nodes (e.g. three replicas of a scheduler service), leader election ensures only one node ticks the schedules at a time — the others stand by and take over on leader failure. Queue/worker/webhook consumption is not leader-gated: every node claims and processes jobs concurrently, coordinated by atomic broker claims.