OqronKitOqronKit

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: project and environment are 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:

  1. A module whose definitions are all gated out doesn't even boot in that environment (used = enabled respects gating).
  2. 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-service/index.ts
// 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-service/index.ts
// 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
scheduler-service/index.ts
// 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

one definition, full control
Cron({
  name: "cleanup",
  expression: "*/30 * * * *",
  environments: ["production", "staging"], // never in dev
  durable: true, // crash-safe fires (see Scheduler docs)
  handler: cleanup,
});
QuestionMechanism
"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.

On this page