Tidebase Tidebase GitHub Start self-hosting
v0.7The backend for AI agents

One backend for AI agents: identity, a vault, durable runs.

One Postgres backend for the parts every agent needs. Give each agent an identity and a vault so it calls real APIs without ever seeing the keys, resume from the last checkpoint when a run dies, and get queues, schedules, and approval gates in the same place.

$ docker compose up -d postgres && pnpm dev

run_8fa3c1d0 · sync-github-issues Running
plan checkpointed0.4s
connect github vaulted0.3s
GET /issues secret injected · agent never sees itrunning…
checkpoint plan replayed grant repo.read · no key leaked cost $0.04 · 4.2k tok

Your agent needs your keys. Don't hand them over.

To do real work, an agent has to call GitHub, Stripe, your database. The easy path drops the token into the prompt or the environment, where the model and every tool can read it. Tidebase holds the secret and makes the call for the agent, so the key stays on your side of the line.

Agent auth and credentials

The agent makes the call. It never holds the key.

broker.ts TypeScript
// Setup once: vault a secret behind a pinned upstream.await tide.resources.connect('github', { provider: 'static',  baseUrl: 'https://api.github.com',  secret: process.env.GITHUB_TOKEN, scopesAllowed: ['repo.read'] }) // Each agent proves its own keypair identity. No shared key.const { challenge } = await tide.agents.challenge(agentId)const { sessionToken } = await tide.agents.prove(agentId, {  challenge, signature: signChallenge(challenge), // agent's private key})const agent = new Tidebase({ agentSessionToken: sessionToken }) // In a run, the agent asks for exactly what it needs.const grant = await agent.auth(runId).request({  resource: 'github:repo:acme/app', action: 'repo.read',}) // Tidebase injects the vaulted secret. The agent never sees it.await agent.auth(runId).use(grant.grantId, { method: 'GET', path: '/user' })
tide.agents.register / prove

Each agent has an identity

An agent registers a keypair, not one shared API key. To act, it signs a server challenge with its private key and gets a short-lived session token it presents on every call.

tide.resources.connect()

Secrets live in a vault

Envelope-encrypted at rest. The upstream is pinned and scopes are capped, so a leaked grant can't reach an arbitrary host.

auth.use()

Tidebase makes the call

The secret is injected into the outbound request. The agent and the model get the response, never the key.

tide.audit.list()

Scope, audit, revoke

Every grant is a receipt with no secret material. Tighten a scope or revoke access without touching the agent.

The playable proof

Kill the process. Watch it resume.

A live run, checkpointing each step to Postgres. Kill it mid-flight, re-invoke with the same run id, and completed steps replay in 0ms.

run_8fa3c1d0· generate-reportReady
planllm
fetch-sourcestool
summarizellm
write-reporttool
steps re-executed after crash00 / 4 checkpointed
// event stream, press Run
the same workflow, two ways

Delete the glue.

without-tidebase.ts~180 lines
// dedupe so a retry can't double-charge
const seen = await redis.get(`${id}:charged`)
if (seen) return JSON.parse(seen)
await db.runs.update(id, { status: 'charging' })
try {
const res = await charge(card, amount)
await redis.set(`${id}:charged`, res)
await db.runs.update(id, { status: 'charged' })
} catch (e) {
// retry with backoff
await queue.enqueue('retry-charge', { id })
await db.runs.update(id, { status: 'failed' })
throw e
}
// on restart, figure out where we stopped
const done = await db.steps.where({ run: id })
with-tidebase.ts9 lines
const res = await ctx.step('charge-card', {
sideEffects: ['stripe.charge'],
replay: 'auto',
retries: 3,
}, () => charge(card, amount))
// state + resume: handled for you
manual idempotency & dedup keysreplay: 'auto'
The whole API

Five calls. That's the surface area.

generate-report.ts TypeScript
await tide.run('generate-report', { runId }, async (run) => {  const plan = await run.step('plan', makePlan)   const sources = await run.step('fetch-sources',    () => fetchSources(plan))   await run.state.set({ status: 'writing', progress: 0.7 })   const ok = await run.gate('approve-report', {    prompt: 'Send the generated report?'  })   return run.step('write-report',    () => writeReport(sources))})
tide.run() · run.step()

Wrap and checkpoint

Create or resume a run, then checkpoint each unit of work. Completed steps return from Postgres on replay.

run.state.set()

Stream live state

Push status and progress your product UI can subscribe to, in real time.

run.gate()

Pause for a human

Open a durable gate and wait. The run resumes only once a decision exists, and it resolves exactly once.

return run.step()

Finish the run

The last step runs once the gate clears, and its result is checkpointed too.

Two SDKs, one API

Drive it from TypeScript or Python.

Same calls, same run protocol, so both SDKs can drive the same run. Point either at your server and go.

TypeScript @tidebase/sdk
$npm i @tidebase/sdk
Python tidebase
$pip install tidebase

Zero-dependency Python (stdlib only, 3.9+) with an async tidebase.aio. Or skip the SDK and call the HTTP API directly.

Human-in-the-loop

An approval queue, in the run.

The run pauses at a durable gate and waits for a decision, in Studio, your own app, or any webhook surface. It resolves exactly once, and every decision is audited.

run_8fa3c1d0· refund-agentRunning
load-accounttool
check-policyllm
approve-refundgate
issue-refundtool
send-receipttool
// event stream
Time travel

Scrub back to the step the bug entered.

Every step's state is versioned in Postgres. When a run produces a wrong answer, scrub the timeline to the exact step that poisoned it, no print statements, no rerunning.

run_8fa3c1d0· refund-agentWrong result

the run emitted a $2400 refund, scrub back to find where

emit-result
step 5 · emit-resultINHERITED
ctx.state @ emit-result
{
  "refund": 2400.00 ← wrong,
  "status": "awaiting-gate",
}
✕ wrong refund queued, 10× too high
// every step's state is checkpointed
// scrub back to the exact step the bug entered.
✕ emit-result · refund 2400.00 (10× too high)
↳ extract-amount · misread $240.00 → $2400.00
✓ root cause found by replaying stored state
Subagents

Fan out. Join durably.

run.fanout() runs subagents as child runs, each checkpointed independently. If the parent resumes, existing children are reused, no duplicate subagents, no double spend.

run_8fa3c1d0· researcherRunning
planllm
4 parallel sub-agents
search-web
search-docs
search-code
search-issues
synthesizellm

run.fanout() runs 4 sub-agents in parallel · each checkpointed independently · joined on completion

// event stream
Cost & usage

Catch the runaway loop before the bill does.

Token and tool spend is attributed per run and per step, without proxying your LLM calls, so a scraping loop gone wrong shows up on a graph, not on an invoice.

run_8fa3c1d0· scraper-agentRunning
$0.00/ $5.00 cap · 0 tool calls
0% of budgetattributed per run · per step
// event stream
Every run comes with

Batteries included.

The run is the core object. Checkpoints, state and gates are just the start, these services come wired in, so there's no extra infrastructure to build.

Gates & events on your surfaces

Run events stream to any webhook you own, Slack via your bot, an internal tool, or your product, and a pending gate becomes a message your team can approve right there. Decisions happen where people already work, not in yet another dashboard.

Tidebase#ops-agents
generate-report is waiting on approve-report. Send the report to the customer?
Approve Reject

Leases & fencing

Only one worker ever owns a run at a time, and zombie workers are fenced from writing stale results, so a retry can't double-charge a customer or fire the same tool twice.

Recovery webhooks

When a run fails, Tidebase calls a signed endpoint in your app so it can re-invoke the workflow. Every attempt is recorded with its delivery status.

Fanout & forking

Subagents run as child runs, idempotent by name on resume. State is versioned, so snapshots, time travel, and forks fall out of the same model.

Cost & usage

Token spend attributed per run and per step, without proxying your LLM calls, so a runaway agent shows up on a graph before it shows up on the bill.

Credential broker

Secrets are vaulted and injected into the call, so the agent and the model never see them. Every grant is recorded with who approved it and why, with no secret material in the ledger.

Tidebase Studio

Agent observability, built in.

Every run is a full trace: step timeline, live state, pending gates, costs, and the event stream. Self-hosted, in your own Postgres, no secret custody.

0
invariant tests against real Postgres
0
calls in the core API
0
database, your Postgres
0
secrets the agent or model ever sees
localhost:5173 / studio
Tidebase Studio, runs list and trace view
Ask your model

Don't take our word for it.

The docs ship as one file your assistant can read. Open Claude or ChatGPT with it preloaded and have it evaluate Tidebase against your stack, critically.

Read https://tidebase.dev/llms-full.txt. I build multi-step AI agent workflows. Evaluate whether Tidebase would help me, how it compares to Temporal, Inngest, LangGraph checkpointers, or hand-rolling status columns, and be critical: tell me when I should NOT use it.
Ask Claude Ask ChatGPT

llms-full.txt · every docs page also serves a raw .md twin

Keep the keys on your side.

Self-hosted from day one. Point the SDK at Postgres, vault a secret, and let the agent call.

$ git clone https://github.com/BlueprintLabIO/tidebase