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.
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:
// 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.
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.
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_KEYbefore 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