Docs Guides
Deploy
Stores, secrets, serverless and the production checklist.
Poppyseed keeps protocol state (Sessions, tokens, codes, replay records, operations, conversations) in a Store. Pick one, then work through the checklist.
Pick a store
| Deployment | Store |
|---|---|
One long-lived server (next start on a VM or container, one instance) |
memoryStore(). Restarting signs everyone out and drops conversations. |
| Several instances, serverless (Vercel, Lambda, Cloud Run) or anything you restart | postgresStore(db), or your own Store. |
Every instance must share the store: a replay record that only one instance knows is a hole on the others. In production, Poppyseed refuses to start without an explicit store.
import { definePoppyseed, postgresStore } from "poppyseed";import { Pool } from "pg";const pool = new Pool({ connectionString: process.env.DATABASE_URL });export const poppyseed = definePoppyseed({ // … store: postgresStore(pool), // creates table poppyseed_kv on first use});postgresStore takes anything with query(text, params) → { rows }: a pg Pool, Neon's serverless driver, PGlite. It uses one table with a version column, so updates are atomic without holding transactions open, which suits pooled and HTTP drivers. Expired rows are pruned as it goes.
To use another database, implement Store (get, set, add, update, delete, with optional expiry). update must be an atomic read-modify-write. Poppyseed's store tests define the contract; running them against your store is the quickest way to trust it.
Production checklist
POPPYSEED_SECRET: 32+ random characters (openssl rand -base64 48), from your secret manager. The same on every instance. Rotating it signs everyone out.baseUrl: exactly the public origin agents use,https://…. Behind a proxy or CDN this is the public URL; DPoP proofs are checked against it.- HTTPS everywhere. Agents refuse
poppy.jsonover plain HTTP. dev.allowInsecureClientsoff. Only real agents with HTTPS metadata on public addresses get in. On Node, metadata fetches also check the address they actually connect to.waitUntil. On serverless, pass your platform'swaitUntil(Next.jsafter), or slow operations and Company Agent replies can be cut off when the response ends. An operation cut off this way staysin_progress, never falselyfailed; resolve it withpoppyseed.operations.settle.- Timeouts. Conversation reads hold requests up to
conversations.maxWait(25 s). Keep it under your platform's function timeout. - Your login page returns people only to your own site (
return_tois attacker-controlled input). - Staff tools that call
poppyseed.conversationssit behind your own staff authentication. - Mediated Sign-In, if offered:
codeRequiredflags unusual sign-ins,notifytells people when an agent signs in. - Logs. Poppyseed never logs tokens or request bodies. Keep it that way in your own middleware for
/oauth/*,/poppy/*and/mcp.
Next.js specifics
withPoppyseedaddsbeforeFilesrewrites for the protocol's exact paths only. If you already haverewrites(), it keeps them.- The catch-all route can live elsewhere:
withPoppyseed(config, { route: "/api/pap" })with the handlers inapp/api/pap/[...path]/route.ts. - Module state is per process. Don't keep anything protocol-related in module variables; that's what the store is for.
Before you go live
Run the conformance suite against a staging deployment over HTTPS:
npx poppyseed-conformance https://staging.example.comIts agents serve metadata from your machine over HTTP, so staging must accept them while you test; see Testing your site.