Jobs & revalidation
Background work with cron and idempotent steps; page freshness through a typed registry.
Background jobs
Work leaves the request path by being a job: declared with defineJob,
registered in one typed registry, enqueued with enqueue(id, input) — never
inngest.send directly, which lint enforces. Payloads are validated when the
job runs, not only at enqueue, because they crossed a process boundary as
JSON.
export const cleanupJob = defineJob({
id: 'cleanup-pending-uploads',
cron: '0 3 * * *', // UTC; the event trigger stays, so you can run it on demand
input: z.object({}),
handler: async ({ input, step, log }) => {
const stale = await step.run('find-stale', () => findStale());
await step.run('delete-rows', () => deleteRows(stale));
log.info('cleaned', { count: stale.length });
},
});step.run memoises: on a retry, steps that already succeeded replay from
their recorded result — slice a job so each side effect is its own step, or a
failure in step three re-sends the email from step one. Handlers receive a
log bound with the job id and run id, the job-world request id.
The template ships three real jobs as patterns: transactional email fan-out,
a nightly cleanup of upload rows whose bytes never arrived (bucket object
deleted best-effort before the row, so bytes are never orphaned), and weekly
audit retention — deliberately the only place in the codebase that deletes
from audit_events, doing under the system role what the request path is
structurally unable to do.
Revalidation, typed
Mutations refresh pages through src/lib/revalidate.ts; importing
next/cache anywhere else is a lint error.
revalidate.projects(input.id); // the list and that detail page
revalidate.everything(); // the whole shell — deliberate and bluntThe bug this closes is silent staleness: revalidatePath('/projcets') is a
typo the compiler shrugs at and the UI punishes — the mutation works, the
list quietly stops refreshing, and nothing says why. A registry of named
surfaces turns the typo into a type error, gives route moves one place to
happen, and gives lq generate feature an anchor to register new features
at — a generated slice whose mutations cannot refresh its own pages does not
compile.
These pages are dynamic (session-scoped RSC reads), so path revalidation is
the whole story today; if you introduce 'use cache'/cacheTag, the tag
names belong in the same registry for the same reasons.