launch-quickly

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 blunt

The 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.

On this page