# How to wire Tidebase into an Express app

Enqueue durable runs from your Express routes, run workflows in a worker process, and mount the recovery webhook with a five-line adapter. The one Express-specific rule: the webhook route must receive the **raw** body — Tidebase's handler verifies an HMAC signature over the exact bytes, so `express.json()` must not touch it first.

```typescript
import express from 'express'
import { Tidebase } from '@tidebase/sdk'
import { generateReport } from './workflows.js'

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

const app = express()
const handler = tide.webhook()

// Raw body on this route only — signature verification needs exact bytes.
app.post('/tidebase', express.raw({ type: 'application/json' }), async (req, res) => {
  const response = await handler(
    new Request(`http://internal/tidebase`, {
      method: 'POST',
      headers: req.headers as HeadersInit,
      body: req.body
    })
  )
  res.status(response.status).json(await response.json())
})

app.post('/reports', express.json(), async (req, res) => {
  const { run } = await tide.enqueue('generate-report', {
    input: { topic: req.body.topic },
    dedupeKey: `report-${req.body.topic}`
  })
  res.json({ runId: run.id })
})

app.get('/reports/:runId', async (req, res) => {
  const detail = await tide.runs.get(req.params.runId)
  res.json({ status: detail.run.status, state: detail.state })
})

app.listen(3000)
```

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 worker process

Routes enqueue; a separate long-lived process executes:

```typescript
// worker.ts
import { Tidebase } from '@tidebase/sdk'
import { generateReport } from './workflows.js'

const tide = new Tidebase()
tide.workflow('generate-report', generateReport)
await tide.work({ queues: ['default'] })
```

This is the honest tradeoff, stated plainly: Tidebase does not execute your code — it holds the queue, the checkpoints, and the retry/backoff state in Postgres, and your worker claims and runs the jobs. If the worker dies mid-run, the lease expires and the reconciler requeues the run; completed steps replay from checkpoints when it's claimed again, per [the replay contract](../replay-contract-is-it-safe-to-rerun.md).

## Why the raw-body dance

`tide.webhook()` returns a fetch-style handler (`Request → Response`) and verifies the `x-tidebase-signature` header by HMAC over the body text. Express's JSON middleware would parse and discard the original bytes, breaking verification — hence `express.raw` on that one route, and the small `Request` adapter. (On fetch-native frameworks — Hono, SvelteKit, Next.js — the handler mounts with no adapter; if you're starting fresh, that's the smoother path.)

Set `TIDEBASE_WEBHOOK_SECRET` on both the Tidebase server and your app; the SDK rejects unsigned or tampered payloads.

## Gates in your own UI

A run paused at `run.gate(...)` surfaces a pending gate in `runs.get`. The gate's webhook channel payload carries a `resolveUrl` and `resolveToken`, so an Express route in your admin panel can approve or reject it — durable, exactly-once, recorded with the actor. See [human approval gates](../human-approval-gates-for-ai-agents.md).

## What Tidebase does not do here

- **No embedded job runner.** There is no `tidebase.start()` inside your Express process; the worker is explicit and yours.
- **No request-scoped magic.** A workflow is a plain function; everything it needs arrives via `input`.
- **Alpha, opt-in auth.** Set `TIDEBASE_API_KEY` before exposing the Tidebase server beyond localhost.

Repo: <https://github.com/BlueprintLabIO/tidebase> · See also: [How to run durable AI workflows behind a Next.js route](nextjs.md)