1.6.2 Inngest SDK + serve route + `lib/jobs/` wrapper (`defineJob`, `sendEvent`); env wiring; smoke job
Estimate: 28m · Depends on: 1.6.1
Land the production runtime in main. Three concrete deliverables:
- SDK + serve route: install
inngestas a runtime dependency. Mount the serve route atapp/api/inngest/route.tsusing Next 16's App Routerserve()adapter. The Inngestclientinstance is the singleton fromlib/jobs/client.ts. lib/jobs/wrapper:lib/jobs/client.ts— exports the singletoninngestclient, configured withid: "motir-core"andeventKeyfromlib/env.ts.lib/jobs/defineJob.ts— the canonical wrapper aroundinngest.createFunction(). Forces every job to declare:id(e.g.,"email.send"), the matchingeventname (alwaysid-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 ajob_runrow 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— wrapsinngest.send()with the same workspace-scoped event-payload shape every job uses ({ name, data: { workspaceId, ...payload } }); throws ifworkspaceIdis 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 blockssendEvent("typo.event.name")at compile time.
- Env + CI: add
INNGEST_SIGNING_KEY+INNGEST_EVENT_KEYtolib/env.ts'srequiredEnv. 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.pingjob 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-patternsystem.daily-health-checkjob.
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
inngestindependencies;@inngest/testindevDependencies.app/api/inngest/route.tsmounts the serve route via Inngest's Next App Router adapter; route accepts GET (registration probe) + POST (invocation) + PUT (registration).lib/jobs/containsclient.ts,defineJob.ts,sendEvent.ts,registry.ts,types.tsper the description; every public export has typed signatures with noany.defineJobwrites ajob_runrow at start and updates it on completion / failure (the schema forjob_runlands 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_KEYinlib/env.ts; CI placeholders in.github/workflows/ci.yml; Vercel env vars set for preview + prod.system.pingsmoke job registered; Vitest test intests/jobs/ping.test.tsdrives the in-process harness and asserts the function runs, returns the static payload, and writes ajob_runrow withstatus: succeeded.docs/jobs.mdcreated with: runtime overview,defineJobAPI reference,sendEventAPI 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 frominngest; an ESLintno-restricted-importsrule 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— therequiredEnvpattern (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 splitdefineJob's handler signature must mirrorprisma/schema.prisma— the schema file thejob_runmigration extends- Inngest TS SDK + Next App Router adapter docs