Skip to main content

Implementation Plan: Job-Runtime Provider Pluggability

Translates spec.md into architecture and tech choices. Behaviour lives in the spec; this owns the "how." Provider research: providers.md. Rationale: ADR-015. Deep architecture: architecture/job-runtime-providers.md.

Feature ID: job-runtime-providers Spec: ./spec.md Tasks: ./tasks.md Status: Draft Last updated: 2026-05-28


1. Architecture Summary

The seam already exists (dispatcher interfaces + DI symbols, architecture/job-runtime-providers.md §2). The plan binds those symbols to the active provider instead of TriggerService, builds the contract + conformance harness, re-houses Trigger.dev, then adds four providers.

2. Tech Choices

ConcernChoiceRationale
AbstractionNew job-runtime capability + IJobRuntimeProvider in packages/plugin/Plugin-first (Principle I); mirrors existing capabilities.
SelectionEVER_WORKS_JOB_RUNTIME env, default triggerMirrors ADR-005 EVER_WORKS_*_BACKEND; infra is global, not per-user.
Trigger providerRe-house existing TriggerServiceZero behaviour change; lowest risk.
Temporal@temporalio/{client,worker,workflow,activity}Official SDK (NN #22); MIT server, free self-host + Cloud.
BullMQbullmqOfficial SDK; already a transitive dep; Redis-native.
Postgres queuepg-bossPostgres-native (BullMQ can't do Postgres); reuses platform DB.
InngestinngestOfficial SDK; SaaS-only (SSPL).
ConformanceShared Vitest/Jest suite against IJobRuntimeProviderMirrors ADR-005 LockProvider contract suite.
Worker hostingPer-provider entrypoints + compose/k8s artifactsModels genuinely differ (push vs pull); cannot fully abstract.

3. Data Model

New entities

None required on the platform schema for the core abstraction. Run state continues to live in WorkGenerationHistory (triggerRunId generalised to runtimeRunId, with triggerRunId kept as an alias column read-path for back-compat).

Optional (later): a small job_runtime_runs mirror table if we want a provider-neutral run index for an admin UI. Not in v1.

Migrations

  • If triggerRunIdruntimeRunId rename is adopted: forward-only, two-phase (add runtimeRunId, backfill from triggerRunId, dual-write, switch reads, drop later) per database.md §6.2 and workstation NN #16. Default plan: keep triggerRunId as-is, treat it as the opaque run id for all providers (cheaper, no migration). Decide in tasks T2.
  • pg-boss creates its own pgboss schema in the platform DB on boss.start() — isolated, additive, not a platform migration.

DTOs / contracts

@ever-works/contracts unchanged for external API consumers. New internal types (JobRunStatus, ScheduleSpec, JobEnqueueOptions, JobRuntimeId, IJobRuntimeProvider) live in packages/plugin/src/contracts/capabilities/job-runtime.interface.ts (shipped EW-685 P0; re-exported from @ever-works/plugin via the capabilities barrel).

4. API Surface

No new public endpoints. The internal callback controller (/internal/trigger/*) is generalised to /internal/jobs/* with /internal/trigger/* kept as an alias route (back-compat for the in-flight Trigger.dev worker during rollout). Auth header generalised x-trigger-secretx-internal-secret (alias retained).

MethodEndpointDescriptionStatus
POST/internal/jobs/remote/callSuperJSON RPC (was /internal/trigger/remote/call)Generalised
GET/internal/jobs/works/:id/contextWorker work/user/token contextGeneralised

5. Plugin Surface

  • Capability job-runtime → contract at packages/plugin/src/contracts/capabilities/job-runtime.interface.ts (shipped EW-685 P0). No separate job-runtime.category.ts registration file — capability strings (job-runtime-enqueue, job-runtime-cancel, job-runtime-status, job-runtime-schedule, job-runtime-worker-host) live in JSDoc on the interface itself, mirroring how dns.interface.ts (EW-734/735) declares its capability strings.
  • New plugins under packages/plugins/: job-runtime-trigger (re-house), job-runtime-temporal, job-runtime-bullmq, job-runtime-pgboss, job-runtime-inngest. Each: package.json with everworks.plugin (category job-runtime, configurationMode: 'admin-only'), tsup build, vitest, settings JSON-Schema with x-secret/x-envVar/x-scope: global.
  • Binding factory packages/agent/src/tasks/job-runtime.providers.ts resolves the active provider and binds all *_DISPATCHER symbols to it.

6. Web / CLI Surface

  • Web: none for v1 (no end-user runtime picker). Optionally, an admin read-only "active runtime" badge in settings (nice-to-have, not required).
  • CLI / internal-cli: a job-runtime status diagnostic (which provider is active + reachability) — optional.
  • MCP: none.

7. Background Jobs

The recurring jobs that exist today must be expressed per provider (each provider's native cron):

TriggerWhenWhat it doesIdempotency strategy
schedule dispatcherevery N minclaim + dispatch due WorkSchedulesruntime-neutral CAS claim (scheduled-updates)
deploy-ready pollerevery 2 minflip mid-deploy works to READYidempotent poll
agent heartbeat dispatcherevery N mindispatch agent heartbeatsCAS claim
data-repo-sync / recurrence / mission-tick / research-rerunvariousexisting cron tasksexisting idempotency

Each provider implements these via its scheduler (Trigger.dev schedules.task, Temporal Schedules, BullMQ repeatable, pg-boss schedule, Inngest cron). The CAS claim makes "exactly one firing per tick" a non-load-bearing convenience, so providers with weaker single-firing guarantees remain correct.

8. Security & Permissions

  • All provider credentials are x-secret: true, x-scope: global, resolved through the settings hierarchy; never returned in API responses.
  • The internal callback channel stays authenticated (constant-time secret compare, allow-listed RPC targets, @SkipThrottle) — provider-neutral.
  • Self-host/air-gap providers (Temporal/BullMQ/pg-boss) MUST NOT contact any SaaS; verified by the air-gap compose profile test.
  • Inngest's signing-key webhook is the trust boundary for the push model; validate signatures on the serve() endpoint.

9. Observability

  • WorkGenerationHistory remains the canonical status surface (status, recentLogs, durationInSeconds) — provider-neutral.
  • Each provider bridges run logs to its native dashboard and to the shared MonitoringModule (Sentry/PostHog), exactly as the Trigger.dev worker does today (monitoring.md).
  • Startup logs the active runtime + experimental warning when applicable.

10. Phased Rollout

  1. Phase 0 — Contract & seam (no behaviour change). Define job-runtime capability + IJobRuntimeProvider; build the binding factory bound to trigger by default; amend Constitution Principle IV. Ship dark.
  2. Phase 1 — Re-house Trigger.dev. Move TriggerService into job-runtime-trigger behind the contract. Existing trigger e2e suite must pass unchanged. This is the proof the abstraction is faithful.
  3. Phase 2 — Conformance harness. Build the provider-agnostic suite; run it green against trigger.
  4. Phase 3 — pg-boss (first new provider). Highest value (enables "just Postgres" OSS deploys). Worker entrypoint + compose profile + conformance green. GA-track.
  5. Phase 4 — BullMQ. Redis worker + compose profile + conformance green. Experimental → GA.
  6. Phase 5 — Temporal. Workflow/Activity split + worker deployment + start-dev CI + conformance green. Experimental → GA. (Largest lift.)
  7. Phase 6 — Inngest (SaaS). serve() functions + cron + cancel; documented SaaS-only. Experimental.
  8. Phase 7 — Docs & matrix. Provider deploy guides, env reference, plugin-count canonical doc, ADR/architecture cross-links finalised.

Each provider stays behind an experimental flag (startup warning) until its conformance suite is green in CI.

11. Risks & Mitigations

RiskLikelihoodImpactMitigation
Re-housing Trigger.dev subtly changes behaviourMedHighPhase 1 gate = existing trigger e2e passes unchanged; diff is mechanical move.
Worker-hosting models too different to share orchestratorMedHighKeep agent orchestrator + SuperJSON channel reused; only the host wrapper differs per provider; prove with pg-boss first.
Cooperative cancel (BullMQ/pg-boss) leaves zombie runsMedMedReuse existing throwIfGenerationCancelled checkpoints + the three-layer terminal-write defense.
Multi-hour jobs trip BullMQ/pg-boss timeoutsMedMedTune lock renewal / visibility timeout; document; conformance includes a long-job case.
Inngest per-step limits break the long pipelineMedMedExpress pipeline as steps under Inngest; keep Inngest experimental; document the constraint.
Five providers rot at different ratesHighMedShared conformance suite in CI; non-default = experimental + best-effort; only trigger is supported-default.
Maintenance/CI costHighLow-MedMatrix CI runs conformance per provider; providers are independently installable plugins.

12. Constitution Reconciliation

  • I Plugin-first — providers are plugins. ✓
  • II Capability-drivenjob-runtime capability resolved from registry. ✓
  • III Source-of-truth repos — untouched. ✓
  • IV Long-running work via Trigger.devAMEND to "via the configured job-runtime provider (Trigger.dev default)"; ships in Phase 0 PR with ADR-015 cross-link. ⚠️→✓
  • V Forward-only migrations — pg-boss schema additive; any triggerRunId rename two-phase. ✓
  • VI Tests — conformance suite + per-provider e2e. ✓
  • VII Secrets — all runtime creds x-secret. ✓
  • VIII Plugin-count canonical doc — updated as providers land. ✓
  • IX Behaviour-first — behaviour in spec, impl here. ✓
  • X Backwards-compatible — default path unchanged; additive. ✓

13. References