MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-61

1.6.2 Inngest SDK + serve route + `lib/jobs/` wrapper (`defineJob`, `sendEvent`); env wiring; smoke job

Done
Description

Estimate: 28m · Depends on: 1.6.1

Land the production runtime in main. Three concrete deliverables:

  • SDK + serve route: install inngest as a runtime dependency. Mount the serve route at app/api/inngest/route.ts using Next 16's App Router serve() adapter. The Inngest client instance is the singleton from lib/jobs/client.ts.
  • lib/jobs/ wrapper:
    • lib/jobs/client.ts — exports the singleton inngest client, configured with id: "motir-core" and eventKey from lib/env.ts.
    • lib/jobs/defineJob.ts — the canonical wrapper around inngest.createFunction(). Forces every job to declare: id (e.g., "email.send"), the matching event name (always id-derived to keep the convention 1:1), retries (default 3), concurrency (optional), idempotency (optional event-payload-keyed template), and a typed handler signature. The wrapper writes a job_run row before invoking the user handler and updates it on completion / failure — this is what powers the operator dashboard in 1.6.5 without relying on Inngest's API for the read path.
    • lib/jobs/sendEvent.ts — wraps inngest.send() with the same workspace-scoped event-payload shape every job uses ({ name, data: { workspaceId, ...payload } }); throws if workspaceId is missing (the durable invariant — every event is workspace-scoped, no untenanted background work).
    • lib/jobs/registry.ts — the array of registered functions the serve route mounts. New jobs land in this file; the serve route imports from here, not from individual job files (so adding a job doesn't change the serve route).
    • lib/jobs/types.ts — the discriminated-union type for job events; every job's event name lives here. Type-safety blocks sendEvent("typo.event.name") at compile time.
  • Env + CI: add INNGEST_SIGNING_KEY + INNGEST_EVENT_KEY to lib/env.ts's requiredEnv. Add placeholders in CI's workflow env block (same pattern as the Better-Auth + Google OAuth env vars in Stories 1.1.2 + 1.1.4). Add real values in Vercel for preview + prod via the Vercel-Inngest Marketplace integration if available; fall back to manual env-var entry otherwise (record the chosen install path in PRODECT_FINDINGS.md).
  • Smoke job: register a single throwaway system.ping job in the registry that returns a static payload. This is what 1.6.2's tests exercise; it stays in the registry until 1.6.4 replaces it with the canonical-pattern system.daily-health-check job.

4-layer rule: NO route file outside app/api/inngest/ imports anything from inngest directly. Routes call sendEvent(); services own the job handler; repositories stay single-Prisma-op. The defineJob wrapper enforces this with a typed handler signature ((ctx, services) => ...) that injects the service layer.

What's deliberately deferred: the actual production job (email.send) is 1.6.3. The retry/idempotency/DLQ patterns are 1.6.4. The dashboard is 1.6.5. This Subtask only lands the SDK + wrapper + smoke job.

Acceptance criteria

  • inngest in dependencies; @inngest/test in devDependencies.
  • app/api/inngest/route.ts mounts the serve route via Inngest's Next App Router adapter; route accepts GET (registration probe) + POST (invocation) + PUT (registration).
  • lib/jobs/ contains client.ts, defineJob.ts, sendEvent.ts, registry.ts, types.ts per the description; every public export has typed signatures with no any.
  • defineJob writes a job_run row at start and updates it on completion / failure (the schema for job_run lands in this Subtask as a Prisma migration — see schema bullet below).
  • Prisma migration adds job_run: id (cuid), workspace_id (FK, ON DELETE CASCADE, nullable for system events), function_id (text), event_name (text), event_id (text, indexed), attempt (int), status (enum: running, succeeded, failed), started_at, finished_at (nullable), duration_ms (nullable), failure (jsonb nullable: { message, stack, code? }), idempotency_key (text, nullable, indexed). Indexes: (workspace_id, started_at desc), (workspace_id, status, started_at desc). RLS deferred to 1.6.4 (the patterns Subtask folds RLS in alongside the DLQ table to keep migrations atomic by concern).
  • INNGEST_SIGNING_KEY + INNGEST_EVENT_KEY in lib/env.ts; CI placeholders in .github/workflows/ci.yml; Vercel env vars set for preview + prod.
  • system.ping smoke job registered; Vitest test in tests/jobs/ping.test.ts drives the in-process harness and asserts the function runs, returns the static payload, and writes a job_run row with status: succeeded.
  • docs/jobs.md created with: runtime overview, defineJob API reference, sendEvent API reference, the "how to add a new job" recipe. Deeper sections (idempotency, retries, DLQ, operator runbook) added in 1.6.4 + 1.6.5.
  • No route file outside app/api/inngest/ imports from inngest; an ESLint no-restricted-imports rule enforces this.
  • All quality gates green; existing tests + E2E stay green; CI build succeeds against the placeholder Inngest env values.

Context refs

  • motir-core/CLAUDE.md — 4-layer rule (auto-loaded)
  • The 1.6.1 findings entry — the validated patterns to mirror exactly
  • lib/env.ts — the requiredEnv pattern (extend it; don't re-shape it)
  • lib/auth/index.ts + lib/users/repo.ts + lib/workspaces/service.ts — exemplars of the canonical service/repo split defineJob's handler signature must mirror
  • prisma/schema.prisma — the schema file the job_run migration extends
  • Inngest TS SDK + Next App Router adapter docs