Claude
Skills
Sign in
Back

queue

Included with Lifetime
$97 forever

Background job queue with Inngest — serverless, works on Vercel, no Redis needed. Durable functions with retry, progress tracking, and job history. Use this skill when the user says "add queue", "setup jobs", "background jobs", "add inngest", or "async tasks".

Backend & APIs

What this skill does


# Background Job Queue (Inngest)

Serverless background job queue using [Inngest](https://www.inngest.com). Jobs survive crashes, retry with exponential backoff, report progress, and persist history to Postgres. Works on Vercel — no Redis or worker processes needed.

## Prerequisites

- Next.js app with `src/` directory and App Router
- `db` skill applied (Drizzle + Postgres)
- `env-config` skill applied (Zod env validation)
- `auth` skill applied (for user context)

## Installation

```bash
bun add inngest
```

## Environment Variables

Add to `.env.local`:

```env
# Inngest
INNGEST_EVENT_KEY=local
INNGEST_SIGNING_KEY=local
```

Add to `src/env.ts` server schema:

```typescript
INNGEST_EVENT_KEY: z.string().default("local"),
INNGEST_SIGNING_KEY: z.string().default("local"),
```

## What Gets Created

```
src/
├── app/
│   └── api/
│       ├── inngest/
│       │   └── route.ts              # Inngest serve endpoint
│       └── jobs/
│           ├── route.ts              # GET list jobs, POST submit job
│           └── [id]/
│               └── route.ts          # GET job status/result
└── lib/
    ├── db/
    │   └── schema/
    │       └── jobs.ts               # Drizzle schema: jobs table
    └── queue/
        ├── client.ts                 # Inngest client setup
        ├── types.ts                  # Job types and event schemas
        └── functions/
            └── example-job.ts        # Example Inngest function
```

## Setup Steps

### Step 1: Create `src/lib/db/schema/jobs.ts`

```typescript
import { pgTable, text, integer, timestamp, jsonb, uuid } from "drizzle-orm/pg-core";

export const jobs = pgTable("jobs", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: text("user_id").notNull(),
  type: text("type").notNull(),
  status: text("status", {
    enum: ["queued", "running", "completed", "failed"],
  }).notNull().default("queued"),
  progress: integer("progress").notNull().default(0),
  input: jsonb("input"),
  result: jsonb("result"),
  error: text("error"),
  inngestRunId: text("inngest_run_id"),
  startedAt: timestamp("started_at", { withTimezone: true }),
  completedAt: timestamp("completed_at", { withTimezone: true }),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});

export type Job = typeof jobs.$inferSelect;
export type NewJob = typeof jobs.$inferInsert;
```

### Step 2: Add export to `src/lib/db/schema/index.ts`

Find the existing exports in `src/lib/db/schema/index.ts` and add:

```typescript
export * from "./jobs";
```

### Step 3: Create `src/lib/queue/types.ts`

```typescript
type JobEventBase = {
  data: {
    jobId: string;
    userId: string;
  };
};

export type QueueEvents = {
  "job/example": JobEventBase & {
    data: JobEventBase["data"] & {
      prompt: string;
    };
  };
};
```

### Step 4: Create `src/lib/queue/client.ts`

```typescript
import { Inngest } from "inngest";

export const inngest = new Inngest({
  id: "my-app",
});
```

> **Note:** The Inngest client is intentionally untyped at the client level. Type safety is enforced per-function via `createFunction()` event triggers. This allows the generic `/api/jobs` route to send events for any job type without type conflicts.

### Step 5: Create `src/lib/queue/functions/example-job.ts`

```typescript
import { inngest } from "@/lib/queue/client";
import { db } from "@/lib/db";
import { jobs } from "@/lib/db/schema";
import { eq } from "drizzle-orm";

export const exampleJob = inngest.createFunction(
  {
    id: "example-job",
    retries: 3,
  },
  { event: "job/example" },
  async ({ event, step }) => {
    const { jobId, userId, prompt } = event.data;

    // Mark as running
    await step.run("mark-running", async () => {
      await db
        .update(jobs)
        .set({
          status: "running",
          startedAt: new Date(),
          updatedAt: new Date(),
        })
        .where(eq(jobs.id, jobId));
    });

    // Simulate work with progress updates
    const result = await step.run("process", async () => {
      // Update progress to 50%
      await db
        .update(jobs)
        .set({ progress: 50, updatedAt: new Date() })
        .where(eq(jobs.id, jobId));

      // Do the actual work here
      const output = { message: `Processed: ${prompt}`, timestamp: Date.now() };

      // Update progress to 100%
      await db
        .update(jobs)
        .set({ progress: 100, updatedAt: new Date() })
        .where(eq(jobs.id, jobId));

      return output;
    });

    // Mark as completed
    await step.run("mark-completed", async () => {
      await db
        .update(jobs)
        .set({
          status: "completed",
          result,
          completedAt: new Date(),
          updatedAt: new Date(),
        })
        .where(eq(jobs.id, jobId));
    });

    return result;
  }
);
```

### Step 6: Create `src/app/api/inngest/route.ts`

```typescript
import { serve } from "inngest/next";
import { inngest } from "@/lib/queue/client";
import { exampleJob } from "@/lib/queue/functions/example-job";

export const { GET, POST, PUT } = serve({
  client: inngest,
  functions: [exampleJob],
});
```

### Step 7: Create `src/app/api/jobs/route.ts`

```typescript
import { NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";
import { jobs } from "@/lib/db/schema";
import { eq, desc } from "drizzle-orm";
import { inngest } from "@/lib/queue/client";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";

const submitSchema = z.object({
  type: z.string(),
  input: z.record(z.string(), z.unknown()).optional(),
});

export async function GET(request: Request) {
  const session = await auth.api.getSession({
    headers: await headers(),
  });
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const userJobs = await db
    .select()
    .from(jobs)
    .where(eq(jobs.userId, session.user.id))
    .orderBy(desc(jobs.createdAt))
    .limit(50);

  return NextResponse.json(userJobs);
}

export async function POST(request: Request) {
  const session = await auth.api.getSession({
    headers: await headers(),
  });
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const body = await request.json();
  const { type, input } = submitSchema.parse(body);

  // Create job record
  const [job] = await db
    .insert(jobs)
    .values({
      userId: session.user.id,
      type,
      input,
    })
    .returning();

  // Send event to Inngest
  await inngest.send({
    name: `job/${type}`,
    data: {
      jobId: job.id,
      userId: session.user.id,
      ...((input as Record<string, unknown>) ?? {}),
    },
  });

  return NextResponse.json(job, { status: 201 });
}
```

### Step 8: Create `src/app/api/jobs/[id]/route.ts`

```typescript
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { jobs } from "@/lib/db/schema";
import { eq, and } from "drizzle-orm";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";

export async function GET(
  _request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const session = await auth.api.getSession({
    headers: await headers(),
  });
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { id } = await params;

  const [job] = await db
    .select()
    .from(jobs)
    .where(and(eq(jobs.id, id), eq(jobs.userId, session.user.id)))
    .limit(1);

  if (!job) {
    return NextResponse.json({ error: "Job not found" }, { status: 404 });
  }

  return NextResponse.json(job);
}
```

### Step 9: Push database schema

```bash
bunx drizzle-kit push
```

## Usage

### Submit a job

```typescript
const response = await fetch("/api/jobs", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: J
Files: 1
Size: 10.3 KB
Complexity: 20/100
Category: Backend & APIs

Related in Backend & APIs