Tidebase Tidebase GitHub Start self-hosting
Docs

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_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