# How to run durable AI workflows behind a Next.js route

Don't run a five-minute agent workflow inside a request handler — enqueue a durable Tidebase run from your route, return the `runId`, and let a worker execute it. Next.js App Router handlers speak web `Request`/`Response`, so Tidebase's recovery webhook handler mounts as a route export with no adapter at all.

```typescript
// app/api/reports/route.ts — enqueue, don't execute
import { Tidebase } from '@tidebase/sdk'

const tide = new Tidebase()

export async function POST(request: Request) {
  const { topic } = await request.json()
  const { run } = await tide.enqueue('generate-report', {
    input: { topic },
    dedupeKey: `report-${topic}`
  })
  return Response.json({ runId: run.id })
}
```

```typescript
// app/api/tidebase/route.ts — recovery webhook, zero adapter
import { Tidebase } from '@tidebase/sdk'
import { generateReport } from '@/workflows/generate-report'

const tide = new Tidebase()
tide.workflow('generate-report', generateReport)

export const POST = tide.webhook()
```

Tidebase is an open-source checkpoint layer for AI agents: wrap your steps, and failed runs resume from the last safe point — in your own Postgres, without moving execution into a new runtime.

## The shape: enqueue → worker → state

1. **The route enqueues.** `tide.enqueue` creates a durable `queued` run in Postgres — with dedupe, so a double-submitted form doesn't create two reports — and returns immediately. Your handler stays inside any serverless time limit because it does no agent work.
2. **A worker executes.** A long-lived process (not a serverless function) runs `tide.work({ queues: ['default'] })` and executes registered workflows off the queue, with retries and backoff handled by Tidebase. This is the honest tradeoff stated plainly: Tidebase does not execute your code — you run this worker. On most Next.js hosting that means a small companion process or container, not a route.
3. **The UI reads run state.** Server components or route handlers read progress with `tide.runs.get(runId)` — live state, step results, gate status — straight from your Postgres. Workflows update it with `run.state.patch({ progress })` as they go.

## Recovery and the webhook route

If a worker dies mid-run, the lease expires and Tidebase's reconciler requeues queue runs automatically. For runs created outside queues, mount the recovery webhook (above): Tidebase POSTs a signed `run.resume` payload, the SDK verifies the HMAC (`TIDEBASE_WEBHOOK_SECRET` on both sides) and re-invokes the registered workflow with the same `runId` — completed steps replay from checkpoints per [the replay contract](../replay-contract-is-it-safe-to-rerun.md).

One Next.js-specific caveat: the webhook re-runs the *remaining* workflow inside the route invocation, so it inherits your function's execution limits. If your unfinished tail can be long, prefer queues (the reconciler requeues without needing the webhook) or have the webhook route enqueue instead of executing inline.

## Approval gates in your product UI

A workflow paused at `run.gate('approve-send', ...)` shows up in `runs.get` with a pending gate. Render it in your app and resolve it from a route handler or server action — the gate's webhook channel payload includes a `resolveUrl` and `resolveToken`. The decision is durable, exactly-once, and recorded with the actor. See [human approval gates](../human-approval-gates-for-ai-agents.md).

## What Tidebase does not do here

- **It is not a Next.js background-function provider.** It stores run truth and decides *when* code should run (queues, cron, recovery); the compute is yours.
- **It does not stream tokens.** Stream LLM output to the browser however you do today; checkpoint completed steps.
- **Alpha, opt-in auth.** Set `TIDEBASE_API_KEY` (and the webhook secret) before pointing production traffic at it.

Repo: <https://github.com/BlueprintLabIO/tidebase> · See also: [Queues, schedules, and cancellation](../queues-schedules-and-cancellation.md)