owner: "@d-hinders" status: current covers:
- .nvmrc
- .env.example
- docker-compose.yml
- package.json
- packages/backend/package.json
- packages/frontend/package.json
- packages/sdk/package.json
- packages/mcp/package.json
- packages/mcp-server/package.json
- packages/signer/package.json
- packages/connect/package.json
- packages/demo-merchant-mcp/package.json
- .github/workflows/publish.yml
- scripts/release-bump.mjs last-verified: "2026-09-30"
Haven
Haven is an agentic stablecoin payment wallet. Users create or link a Haven account, add funds to a Haven wallet, and give AI agents constrained spending ability through agent rules and budgets.
Haven is non-custodial smart account software. User funds stay in a user-controlled MetaMask Hybrid DeleGator smart account, shown in product copy as a Haven wallet. An agent's spending authority is a delegation you sign — a budget with audited caveat enforcers (period budget with native refill, optional recipient pin, expiry) that the DelegationManager enforces on-chain at redemption. Haven helps users configure that authority, construct payment payloads, relay independently signed transactions, and understand activity. It does not hold user or agent private keys, make API credentials sufficient to spend, or make discretionary transfer decisions — and it holds no key that can change an account's signer set or redeem a delegation you did not sign.
Are you an AI agent whose user has no Haven account yet?
Read /for-agents.md on the Haven host your user gave you — or
the copy in this repository
if you do not have that host yet.
Your user creates the account and the passkey: those are theirs, they need a
human, and you should never ask for their password. You can do everything else
— including running the connector command from the setup prompt they paste you,
and managing the account from the shell with @haven_ai/cli.
Not to be confused with
AGENTS.md. That file is for contributors to this repository — build, test and review conventions for changing Haven itself. It is not a guide for an agent that wants to USE Haven to pay for something; this section is.
Core Model
Agents should not be wallets. They should be payment actors with constrained authority.
User -> Haven wallet / Hybrid DeleGator (funds and signer authority)
Agent -> Haven credential (identity) + delegate signing key (payment signatures)
Haven -> UI, API, pre-checks, relay, receipts, status
Signed delegation + caveat enforcers -> On-chain agent budget enforcement
API auth is identity. Signature is authority. On-chain delegation state is enforcement.
Haven runs one live on-chain policy rail: the delegation rail (epic #821). Funds move account→recipient directly — no funding leg, no approval queue, and a payment outside the budget, recipient pin or expiry reverts during gas estimation rather than queueing for a human.
The legacy Safe + AllowanceModule rail is RETIRED (epic #1440), not frozen: nothing can
enter it, nothing on it can spend, and its execution machinery is deleted. The closure
sequence is recorded once in the
decision log
and is not restated here (#2640).
Existing Safe rows are untouched and still readable to a direct database query, but
since #2413 no account, agent or dashboard surface displays them — balances, tokens and agents no
longer render there. Transaction history is the named exception: GET /transactions carries no rail
predicate on either its account or its agent list, so legacy names still appear in its picklists. Any residual AllowanceModule permission remains on-chain and outside Haven until
the Safe owner revokes it externally. The Smart Sessions session rail is
likewise retired (#834). See
docs/security/delegation-rail-security-model.md,
docs/product/account-recovery.md for what a legacy
Safe owner can still do, and your non-custody exit path.
What's in the Repo
This is a TypeScript monorepo:
| Package | Description |
|---|---|
packages/backend |
Fastify API for auth, Haven wallets, agents, payments, x402/MPP, receipts, the Fortnox accounting feed, and OpenAPI (the legacy approval queue is gone entirely — its route was deregistered and its table dropped by #2055) |
packages/frontend |
Next.js dashboard for Haven accounts, Haven wallets, agent rules, connect-agent handoff, and activity |
packages/sdk |
@haven_ai/sdk for direct agent integrations, tool definitions, x402/MPP quote/pay/resume helpers, and payment state handling |
packages/mcp |
@haven_ai/mcp local stdio MCP server that reads a local credential file and signs locally |
packages/mcp-server |
@haven_ai/mcp-server hosted/keyless Streamable HTTP MCP server that constructs and relays but never signs |
packages/signer |
@haven_ai/signer local edge signer used with hosted MCP; it holds the delegate key locally and exposes sign-only tools |
packages/cli |
@haven_ai/cli terminal-native, scriptable parallel to the dashboard (login, reads, backend-only management) |
packages/demo-merchant-mcp |
Internal x402 demo merchant MCP server for Base USDC test purchases and Swedish invoice output |
packages/qa-agent |
Internal QA harness for the dev environment: deterministic money-flow scenarios and dev seeding (not published) |
Team Docs
- About Haven
- Documentation index — start here
- Architecture overview
- Architecture diagrams
- Hosted MCP connect flow
- PR Workflow Checklist
- CASP / MiCA Risk Minimisation Guardrails
Prerequisites
- Node.js 24 (LTS) — pinned in
.nvmrc; CI and the Docker images run the same major. (The published agent-runtime packages declare a lowerenginesfloor,>=22— see mcp-runtime-compatibility.) Withnvmorfnm, runnvm use/fnm usein the repo root to match it automatically (otherwisenpm installwarnsEBADENGINE). - Docker Desktop (for local hosting) — docker.com/products/docker-desktop
- A browser wallet (MetaMask, Rabby, etc.) with Gnosis Chain or Base configured — optional: signup uses a passkey, and a wallet is only needed if you want to enrol one as an account signer
Getting Started
1. Clone and install
git clone https://github.com/d-hinders/Haven-AI.git
cd Haven-AI
npm install
2. Configure environment
cp .env.example .env
Edit .env and fill in the required values:
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string (default works with Docker) |
JWT_SECRET |
Yes | Secret for dashboard auth tokens; use a long random string in production |
RPC_URL |
No | Gnosis Chain RPC (default: https://rpc.gnosischain.com) |
RPC_URL_BASE |
No | Base RPC (default: https://mainnet.base.org) |
RELAYER_PRIVATE_KEY |
Yes for on-chain execution | EOA private key that pays gas for relayed transactions; it cannot access user funds |
GNOSISSCAN_API_KEY |
No | Gnosis explorer API key for transaction display |
BASESCAN_API_KEY |
No | Base explorer API key when using an Etherscan-style Base source; Base currently defaults to Blockscout for transactions |
COINGECKO_API_KEY |
No | Token price lookups |
FRONTEND_URL |
No | Backend CORS/link base (default: http://localhost:3000) |
NEXT_PUBLIC_API_URL |
No | Frontend backend URL override (default through local rewrite: http://localhost:3001) |
NEXT_PUBLIC_HAVEN_MCP_URL |
No | Hosted MCP URL shown in connect-agent snippets |
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID |
No | WalletConnect project id; injected wallet connectors can still work without it |
ANTHROPIC_API_KEY |
No | Only for the optional Claude agent demo script |
Setting up the relayer wallet:
The relayer is a throwaway EOA that pays gas for on-chain agent payments. It never has access to user funds — it just submits transactions. Generate one:
node -e "const{ethers}=require('ethers');const w=ethers.Wallet.createRandom();console.log('Address:',w.address);console.log('Key:',w.privateKey)"
Fund the relayer with the native token for each chain you plan to use:
- Gnosis Chain: 0.01 xDAI (enough for thousands of transactions)
- Base: 0.001 ETH
Put the private key in RELAYER_PRIVATE_KEY.
3. Start PostgreSQL
Make sure Docker Desktop is running:
npm run docker:up
4. Start the dev servers
npm run dev
- Frontend → http://localhost:3000
- Backend API → http://localhost:3001
5. Create your Haven account
- Go to http://localhost:3000
- Click Get Early Access → create an account
- Pick your network, then create the account with a passkey — one Face ID / Touch ID prompt
- You'll land on the dashboard with your Haven wallet address
One onboarding path, no transaction. Signup provisions a counterfactual passkey-owned Hybrid DeleGator (
POST /accounts/hybrid) — zero transactions and no gas, so no faucet is needed to get an address. You need native gas tokens only if you later send from the account yourself. Creating or importing a Safe is retired (#1984) and answers HTTP 410; there is no deploy modal.
6. Create an agent
- Go to the Agents tab in the dashboard
- Click Create Agent
- Pick the Haven wallet and network the agent will spend from
- Set the agent budget: token, amount, and reset period
- Sign the budget delegation with your passkey or wallet — one signature, no transaction for you to pay for
- Save the one-time Haven credential when the Done step appears
- Use Connect your agent to add Haven to Claude Code, Cursor, VS Code, Codex CLI, OpenCode, Goose, Amp, or another runtime
The credential contains an agent API key and a delegate signing key. Haven stores only the API-key hash/prefix and never stores the delegate private key. If the API key is exposed or lost, use Payment credentials on the agent detail page to rotate it; the new key is shown once and the old key stops working. If the delegate signing key is exposed or lost, a delegation-rail agent is re-keyed, not replaced: the same agent gets a new signing key and a new API key, keeping its id, name, history and the remainder of its budget — see Replacing an agent's signing key. Legacy AllowanceModule agents are historical records only in the dashboard, without payment-credential, pause/resume, re-key, or revoke controls there. Owners manage any remaining Safe permission outside Haven where they have access; replacement agents use the live delegation flow.
Agent Integration
Most users connect an agent through the dashboard's Connect your agent flow. The hosted MCP path sends only the API key to the hosted MCP endpoint; the delegate signing key stays local with the runtime or @haven_ai/signer.
Developers can also integrate directly with @haven_ai/sdk. The SDK wraps direct payments, quote-first x402/MPP flows, and ready-made tool definitions for Claude/OpenAI-style tool calling.
Install
npm install @haven_ai/sdk@alpha
Direct SDK payment
import { HavenClient } from '@haven_ai/sdk'
const haven = new HavenClient({
apiKey: 'sk_agent_xxx', // from Haven dashboard
delegateKey: '0x...', // delegate signing key held by the agent runtime
baseUrl: 'http://localhost:3001',
})
const result = await haven.pay({
token: 'EURe',
amount: '5.00',
to: '0xrecipient...',
})
console.log(result.txHash) // 0x...
console.log(result.explorerUrl) // https://gnosisscan.io/tx/0x... (or basescan.org for Base)
Tool-calling integration
The SDK ships with pre-built tool definitions for Claude and OpenAI:
import { HavenClient, havenTools } from '@haven_ai/sdk'
const haven = new HavenClient({ apiKey, delegateKey })
const tools = havenTools.claude() // or havenTools.openai()
// When your model returns a Haven tool call:
const toolCall = { name: 'get_allowances', input: {} }
const result = await haven.executeTool(toolCall.name, toolCall.input)
if (toolCall.name === 'get_allowances') {
// Use this for budget, remaining amount, reset-period, or "what can I spend?" questions.
}
See packages/sdk/README.md for the full SDK reference, payment state machine, x402/MPP helpers, and error handling.
MCP paths
| Path | Package | Use when | Custody boundary |
|---|---|---|---|
| Hosted MCP | @haven_ai/mcp-server + @haven_ai/signer |
You want one hosted URL across agent runtimes | Hosted server receives API identity only; local signer holds the delegate key |
| Local MCP | @haven_ai/mcp |
You want a local stdio server beside the agent runtime | Local process reads the credential file and signs locally |
| Direct SDK | @haven_ai/sdk |
You are writing custom TypeScript agent code | Your runtime holds the delegate key and calls Haven with signed payloads |
Testing the Payment Flow
After creating an agent, you can test payments several ways:
Option A: Hosted MCP connection
Use the dashboard's Connect your agent Done step. It creates runtime-specific snippets and one-click deep links where supported. The hosted snippets include the API key only; they do not include the delegate signing key. The local signer or runtime secret store handles signing.
Option B: SDK simulation script
Tests the raw API flow — no AI involved:
# Add to .env:
# AGENT_API_KEY=sk_agent_... (from step 7 above)
# DELEGATE_PRIVATE_KEY=0x... (from step 7 above)
# PAYMENT_TO=0x... (any recipient address)
cd packages/backend
npm run test:payment
This creates a payment intent, signs it with the delegate key, submits it, and confirms on-chain. Output includes the Gnosisscan transaction link.
Option C: Claude agent demo
An optional Claude-powered demo that turns a user task into a Haven tool call:
# Add to .env:
# ANTHROPIC_API_KEY=sk-ant-... (from console.anthropic.com)
# (plus the same AGENT_API_KEY, DELEGATE_PRIVATE_KEY, PAYMENT_TO as above)
cd packages/backend
npm run agent:demo
Or with a custom task:
npm run agent:demo -- "Pay 0.01 EURe to 0xABC... for API access"
Claude receives the task, calls the make_payment tool when appropriate, Haven validates the signed request and relays it on-chain, and Claude summarizes the result.
What this proves: A real AI agent requested and signed a payment from a user-controlled smart account within strict on-chain guardrails, without holding keys to the account and without understanding blockchain mechanics.
How It Works
Architecture
Agent runtime
-> SDK / local MCP / hosted MCP
-> Haven API (identity, policy mirror, construct, relay, status)
-> Signed delegation + caveat enforcers (on-chain budget enforcement)
-> Haven wallet / Hybrid DeleGator (user funds)
- Agent sends a simple payment intent:
{ token: "EURe", amount: "5.00", to: "0x..." } - Haven authenticates the API key, loads the Haven wallet, and selects the agent's budget delegation for that token and recipient
- Haven prepares a redeeming UserOperation and returns the account's exact EIP-712 typed data to sign
- Agent/runtime signs locally with the delegate key; the key never goes to Haven
- Haven submits the sponsored UserOperation without changing amount, token, recipient, or authority boundary
- The caveat enforcers check budget, recipient and expiry on-chain during gas estimation — anything outside the envelope reverts there. Funds move account→recipient directly, with no funding leg
Payment API (3-step flow)
| Step | Endpoint | What happens |
|---|---|---|
| 1. Create intent | POST /payments |
Haven validates, selects the budget delegation, returns typed data to sign |
| 2. Sign & submit | POST /payments/:id/sign |
Agent signs the typed data, Haven verifies and submits the sponsored UserOperation |
| 3. Check status | GET /payments/:id |
Poll until confirmed / failed |
All endpoints authenticate with Authorization: Bearer sk_agent_xxx. Authentication is not payment authority: executable transfers still require the agent-held delegate signature and the on-chain delegation it redeems against.
An account on the retired Safe rail (execution_rail='allowance_module') gets HTTP 410 from all of these, fail-closed with nothing written (#1986).
For x402 and MPP, the SDK and MCP tools use quote/pay/resume flows. The preferred merchant scheme is ERC-7710 direct settlement — one leg, account→merchant, no funding hop. Where a merchant's facilitator cannot redeem a delegation chain (still most of them today), the EIP-3009 bridge (#946) transiently funds the agent EOA from the budget delegation so it can sign a standard X-PAYMENT header; that path deliberately reintroduces a bounded funding leg, which is why the sweep and delegate-balance monitoring machinery is kept. Production merchant facilitation, acquiring, fiat/card rails, settlement, swaps, yield, and advice are not current Haven production surfaces.
Security Model
Independent layers keep the API and signing boundaries separate:
| Layer | What it does | Where it lives |
|---|---|---|
| Hybrid DeleGator smart account | User-managed signer set (passkey and/or wallet), holds all funds | On-chain |
| Signed delegation + caveat enforcers | Per-agent period budget with native refill, optional recipient pin, expiry — checked at redemption | On-chain |
| Delegate signing key | Signs payment payloads within the delegated budget | Agent/runtime/user environment |
| Haven policy mirror | Pre-checks, audit trail, status, and copy — a mirror, never the real spend control | Haven backend |
| Credential scoping | API-key identity, prefix display, rotation, and revocation state | Haven backend |
If Haven is compromised, API keys alone cannot sign transactions. For a live delegation agent, an account owner can pause or revoke it in Haven, and can revoke the underlying on-chain authority directly without needing Haven — see the independent exit path. The dashboard exposes no pause or revoke controls for legacy Safe records; any residual Safe permission must be revoked externally by the Safe owner.
Approver (Safe owner) management is retired (#1988/#1989, epic #1440). Haven never signed an owner change and no longer constructs one: the backend routes and the Settings surface are both deleted. Owners of a legacy Safe manage its owner set directly through Safe's own interfaces with their own key — which was always true, and is the reason removing Haven's builder takes nothing away that Haven was the only source of. Haven no longer relays an owner-signed Safe transaction either: #1989 deleted the screen that composed one, and #2847 deleted POST /safe/exec, the route that relayed it for gas. A wallet-owned Safe loses nothing (sign at app.safe.global); a passkey-owned Safe currently has no self-serve way to move funds out, because Haven's passkey Safe signer is a custom WebAuthn scheme Safe's own interfaces cannot drive. The epic's Base-mainnet census found no passkey-owned Safe, which is why this was accepted as a narrowing — but a census is not a proof and it does not cover non-mainnet accounts. Full user-facing wording: docs/product/account-recovery.md.
Key Management
| Key | Who holds it | What it can do |
|---|---|---|
| Account signer (passkey / wallet) | User device or wallet/hardware environment | Full account control: add or remove signers, grant and revoke agent budgets. On a legacy Safe, the equivalent is the Safe owner key |
| Delegate private key | Your agent | Sign payment intents within the delegated budget only |
| Agent API key | Your agent | Authenticate with Haven API; no signing ability |
| Hosted MCP bearer token | Agent runtime config | Same API identity role as the agent API key |
| Relayer key | Haven server | Pay gas for independently valid signed transactions; no fund access |
Haven never holds account signer keys, Safe owner keys, or delegate private keys.
For architecture constraints around custody, transfer-service risk, relaying, x402/merchant demos, fiat/card rails, swaps, and investment advice, use docs/regulatory/casp-risk-guardrails.md as the required perimeter guardrail.
API Reference
Authentication
Agent endpoints use Bearer token auth:
Authorization: Bearer sk_agent_xxx
Dashboard endpoints use the signed-in user's JWT. The OpenAPI contract is served at /openapi.json.
Endpoints
| Surface | Auth | Examples |
|---|---|---|
| Dashboard auth | None/JWT | /auth/signup, /auth/login, /auth/me |
| Haven wallets | JWT | /user/accounts (list/rename/re-default/unlink), balances and account views. The whole /user/safes* prefix and PUT /user/safe answer 410 naming their replacement (#2914) — the Safe vocabulary is retired from the wire. Creating or importing an account is separately retired — POST /user/accounts, /user/accounts/deploy, /safe/deploy and PUT /user/account all answer 410 (#1984). Create a Haven account on the delegation rail with POST /accounts/hybrid. |
| Agents | JWT | /agents, /agents/:id, /agents/:id/pause, /agents/:id/resume, /agents/:id/revoke, /agents/:id/rotate-key |
| Agent payments | API key | /payments, /payments/:id/sign, /payments/:id, /payments |
| Agent info | API key | /machine-payments/agent, /machine-payments/allowances, /machine-payments/receipts, /machine-payments/:id/status, resume-state endpoints |
| x402 | API key or protocol challenge | /x402 (the legacy internal mpp_demo flow at /demo/mpp/* and POST /machine-payments/authorize is retired — the latter now refuses with HTTP 410, #1328) |
| Activity | JWT | /agent-activity/* for payments, MCP tool calls, and last activity |
/agents/:id/allowances and /agents/:id/allowances/:tokenAddress are retired
410 tombstones. Live agent budgets are signed delegation grants, not allowance
rows.
Payment intent request
POST /payments
{
"token": "EURe",
"amount": "5.00",
"to": "0xrecipient..."
}
Sign sign_data.typed_data verbatim, never the bare hash. The account validates the typed data, not the 4337 hash; @haven_ai/sdk and the MCP signer do this for you.
There is no over-budget approval queue on the delegation rail. A request the remaining period budget cannot cover is refused 403 delegation_budget_exceeded at a pre-check before anything is built (#3503); one to a recipient outside the grant's pin, or outside its validity window, finds no active delegation and is refused 403; anything that still reaches the chain out of policy reverts during on-chain gas estimation — none becomes a pending approval. There is no 202 on this route: #2055 dropped the approval_requests table, so no row is left to replay, and #2105 removed the response from the published contract. Do not keep a pending_approval branch alive.
Payment intent response
{
"payment_id": "uuid",
"status": "pending_signature",
"expires_at": "2025-01-01T00:10:00Z",
"sign_data": {
"hash": "0x...",
"signature_scheme": "eip712_userop",
"typed_data": { "domain": {}, "types": {}, "primaryType": "PackedUserOperation", "message": {} },
"components": {
"account": "0x...",
"token": "0x...",
"to": "0x...",
"amount": "5000000000000000000"
},
"instructions": "Sign sign_data.typed_data with your delegate key using EIP-712 (signTypedData)..."
}
}
Sign and execute
POST /payments/:id/sign
{
"signature": "0x...65_byte_signature"
}
Response on success:
{
"payment_id": "uuid",
"status": "confirmed",
"tx_hash": "0x...",
"chain_id": 100,
"explorer_url": "https://gnosisscan.io/tx/0x..."
}
Available Scripts
| Command | What it does |
|---|---|
npm run dev |
Start backend + frontend in dev mode |
npm run build |
Build SDK, connect, MCP packages, signer, CLI, backend, and frontend |
npm run test |
Run workspace tests where configured |
npm run typecheck |
Run workspace type checks |
npm run preflight |
Run the gates CI will run on your diff, derived from the workflow files (#3150). Start here before pushing. preflight:all ignores the diff and runs every gate; preflight:list prints the plan without running it |
npm run quality |
Typecheck and unit tests across workspaces, then the root build chain. Despite the name this is not the pre-push gate list — it covers none of the ratchets (lint:*, check:*, the coupling gates), and the root build omits two packages. Use preflight |
npm run docker:up |
Start PostgreSQL container |
npm run docker:down |
Stop PostgreSQL container |
npm run docker:logs |
Tail PostgreSQL logs |
From packages/backend/:
| Command | What it does |
|---|---|
npm run test:payment |
Run payment simulation script |
npm run agent:demo |
Run Claude agent payment demo |
Project Structure
Haven-AI/
|-- ABOUT_HAVEN.md # Product and architecture mental model
|-- docs/ # architecture, product/UX, contributing, operations, regulatory docs (see docs/README.md)
|-- packages/
| |-- backend/ # Fastify API, database migrations, relaying, OpenAPI
| |-- frontend/ # Next.js dashboard and connect-agent UX
| |-- sdk/ # @haven_ai/sdk
| |-- mcp/ # Local stdio MCP server; signs locally from a credential file
| |-- mcp-server/ # Hosted/keyless Streamable HTTP MCP server
| |-- signer/ # Local edge signer paired with hosted MCP
| |-- cli/ # @haven_ai/cli terminal parallel to the dashboard
| |-- demo-merchant-mcp/ # Internal x402 merchant MCP demo
| `-- qa-agent/ # Internal QA harness (not published)
|-- .env.example # Environment variable template
`-- docker-compose.yml # PostgreSQL for local dev
Supported Networks & Tokens
Gnosis Chain (chainId: 100)
| Token | Symbol | Decimals | Address |
|---|---|---|---|
| xDAI | xDAI | 18 | Native |
| EURe | EURe | 18 | 0xcB444e90D8198415266c6a2724b7900fb12FC56E |
| USDC.e | USDC.e | 6 | 0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0 |
Base (chainId: 8453)
| Token | Symbol | Decimals | Address |
|---|---|---|---|
| ETH | ETH | 18 | Native |
| USDC | USDC | 6 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
Tech Stack
- TypeScript throughout (backend + frontend)
- Fastify — backend API
- Next.js 15 — frontend dashboard
- PostgreSQL — agents, policies, payment intents, receipts, audit trail
- Safe + AllowanceModule — legacy rail, retired (#1440): reads and owner-signed relay only, no
@safe-global/protocol-kit - MetaMask smart-accounts-kit + permissionless — Hybrid DeleGator delegation rail
- wagmi + viem — wallet connection + blockchain interaction
- ethers v6 — backend blockchain operations
- Model Context Protocol — local and hosted agent tool connections
- Tailwind CSS — styling
- Gnosis Chain + Base — supported EVM networks
- Anthropic SDK — Claude agent demo
Contributing — Hosted Setup & Dev Workflow
Haven runs in production on Vercel (frontend) and Railway (backend + Postgres). Two long-lived branches deploy: dev auto-deploys to the shared dev environment, and main auto-deploys to production. The five published npm packages (@haven_ai/sdk, signer, mcp, connect, cli) are also published automatically from main on a version bump — see Releasing npm packages. For the dev environment's setup and env vars, see docs/operations/dev-environment.md.
Repository workflow
All changes go through pull requests — no direct pushes to main or dev. Feature work flows feature/* → dev → main; the dev-gate workflow only lets dev or hotfix/* merge into main.
- Branch off
dev→ make your changes - Push the branch and open a PR into
devon GitHub - CI runs automatically (type-check + build per surface: SDK, CLI, backend, frontend, MCP, connect, signer)
- Vercel posts a preview URL as a comment on the PR — click to test the frontend live
- Once CI is green, the PR can merge into
devand auto-deploys to the dev environment - Promote
dev → mainwith a separate PR (the only normal path intomain); merging triggers automatic production deploys to Vercel + Railway (~2 min). Emergency fixes can use ahotfix/*branch straight intomain.
Frontend-only changes
The Vercel preview URL points at the production Railway backend. You can test most frontend changes directly against the preview URL — no local setup needed beyond the PR.
Backend changes — test locally first
Vercel previews share the prod backend, so backend changes can't be tested via the PR preview alone. Run the backend locally before opening the PR:
# 1. Start the local Postgres
npm run docker:up
# 2. In one terminal — run the backend on :3001
npm run dev -w packages/backend
# 3. In another terminal — run the frontend on :3000
# (set NEXT_PUBLIC_API_URL=http://localhost:3001 in your .env)
npm run dev -w packages/frontend
Test the full flow locally, then push and open the PR. Once merged, watch the Railway deploy logs to confirm the change deployed cleanly in prod.
Inspecting prod
Collaborators have Viewer access to the Railway project — you can see services, deploy logs, and runtime logs, but not change env vars. If a deploy fails or behaves unexpectedly:
- Railway → backend service → Deployments — build logs and runtime logs
- Railway → Postgres → Data — inspect tables (read-only with Viewer role)
- Vercel previews — every PR has a preview URL with its own build logs (linked from the PR comment)
If you need an env var changed in Railway or a secret rotated, ping the project owner.
Releasing npm packages
The five npx-installed packages — @haven_ai/sdk, @haven_ai/signer, @haven_ai/mcp, @haven_ai/connect, @haven_ai/cli — are published to npm automatically. You never run npm publish by hand.
# 1. Bump all published packages atomically (versions, cross-package pins,
# and source version constants) and verify the connect bundle.
npm run release:bump -- <new-version> # e.g. 0.1.17-alpha.0
# 2. Commit on a release branch and open the PR into `dev` — NOT `main`.
# `dev-gate` only lets `dev` or `hotfix/*` into `main`, so a release/*
# branch aimed at `main` fails by design. Get it green, merge to `dev`.
# 3. Promote `dev → main` (a merge commit, never a squash). Publishing fires
# on THIS step, not on the dev merge — the promotion stays a human step.
On the promotion to main, the Publish packages workflow (.github/workflows/publish.yml) rebuilds dist in dependency order and publishes only the packages whose package.json version is not yet on npm. The dist-tag is derived from the version: a prerelease like 0.1.17-alpha.0 publishes under --tag alpha, a stable 0.2.0 under latest. The connector install command the dashboard hands out names @alpha unless that deployment sets HAVEN_CONNECTOR_CHANNEL (#2422; production leaves it unset), so the prerelease line is what real users get.
- Trigger model: version bump = the gate. npm rejects republishing an existing version, so a normal (non-bump) commit is a no-op.
- Two channels, one file (#2421, epic #2420): the paragraph above is the prod channel (
main→alpha/latest). The same workflow also fires on a package-touching push todevand publishes a0.0.0-dev.<YYYYMMDDHHMM>.<shortsha>snapshot of all five packages under thedevdist-tag — built by the ordinary bump script in a throwaway CI tree, with nothing committed.0.0.0-sorts below every real version, so no^0.1.xrange resolves to a snapshot by accident. A snapshot can reach neitheralphanorlatest, and themainpath refuses a0.0.0-dev.*version; the enforcement points are named in the workflow's header comment. Loop and owner steps:docs/operations/package-dev-channel.md. One file rather than two because npm's trusted-publisher entries are pinned to the workflow filename. - Auth for PUBLISHING: npm Trusted Publishing (OIDC) — no long-lived credential. The workflow grants the publish job
id-token: writeand upgrades npm to ≥ 11.5.1; npm then authenticates the short-lived GitHub Actions OIDC token against a trusted publisher configured per package on npm (pointing atd-hinders/Haven-AI+ workflowpublish.yml, environment field empty — see the bullet below for why that matters). Nothing to leak or rotate on this path, and it's exempt from the 2FA one-time-password prompt that blocks token-based publishes. - Moving
latestis a SECOND job with a DIFFERENT credential (#2647): OIDC authorisesnpm publishand nothing else, sonpm dist-tag addcannot ride on it — putting it in the publish job failed every tag move with E401 on the 0.1.35-alpha.0 release, leavinglatesta release behindalpha. A separatepromote-tagsjob holds a granular npm token in themain-onlynpm-production-tagsGitHub Environment, and has noid-token: write, so it can move a tag and cannot publish. It has to be a separate job: anenvironment:is job-level, and adding one to the publish job would break its trusted-publisher entries (registered with an empty environment field). So there IS one long-lived secret now, and it expires — npm caps write-enabled granular tokens at 90 days, this one on 2026-12-06, after which releases publish andlatestsilently stops moving. Two consequences worth reading: a promotion can be half green (published,latestunmoved), and the durable answer is to stop needing two dist-tags at all. - Provenance: Trusted Publishing auto-generates a signed sigstore provenance statement. npm rejects the upload (
E422) unless eachpackage.jsondeclares arepository.url(with the monorepodirectory) matching the repo — each published package must carry this. - Adding a new published package? Before its first release, (1) configure a trusted publisher for it on npm (package → Settings → Trusted Publisher → GitHub Actions:
d-hinders/Haven-AI/publish.yml), and (2) give itspackage.jsonarepositoryblock. Skipping either makes the first publish fail — on auth (EOTP/OIDC) or provenance (E422) respectively. - Failure isolation (#1159): one package's publish failure does not abort the others — every package is attempted, the run summary reports each outcome (published / skipped / failed), and the job fails at the end naming the failures. Before this, a failure early in the list silently skipped everything after it.
- Not published this way:
mcp-server(Docker → Railway),backend, andfrontend(Vercel/Railway) deploy from branches directly —mainto production anddevto the shared dev environment. - Full details, the dist-wipe rationale, and a manual fallback live in
scripts/README.md.
License
Private — not open source.