[{"data":1,"prerenderedAt":767},["ShallowReactive",2],{"navigation":3,"\u002Fguides\u002Foperations\u002Fauth-api-keys":169,"\u002Fguides\u002Foperations\u002Fauth-api-keys-surround":762},[4,28,85,127,157],{"title":5,"path":6,"stem":7,"children":8,"icon":27},"Getting Started","\u002Fgetting-started","1.getting-started\u002F1.index",[9,12,17,22],{"title":10,"path":6,"stem":7,"icon":11},"Introduction","i-lucide-house",{"title":13,"path":14,"stem":15,"icon":16},"Installation","\u002Fgetting-started\u002Finstallation","1.getting-started\u002F2.installation","i-lucide-download",{"title":18,"path":19,"stem":20,"icon":21},"Quick start","\u002Fgetting-started\u002Fquick-start","1.getting-started\u002F3.quick-start","i-lucide-zap",{"title":23,"path":24,"stem":25,"icon":26},"Architecture","\u002Fgetting-started\u002Farchitecture","1.getting-started\u002F4.architecture","i-lucide-layers","i-lucide-rocket",{"title":29,"icon":30,"path":31,"stem":32,"children":33,"page":84},"Concepts","i-lucide-lightbulb","\u002Fconcepts","2.concepts",[34,39,44,49,54,59,64,69,74,79],{"title":35,"path":36,"stem":37,"icon":38},"Durable agents","\u002Fconcepts\u002Fdurable-agents","2.concepts\u002F1.durable-agents","i-lucide-infinity",{"title":40,"path":41,"stem":42,"icon":43},"Glossary","\u002Fconcepts\u002Fglossary","2.concepts\u002F10.glossary","i-lucide-book-a",{"title":45,"path":46,"stem":47,"icon":48},"Restate in five minutes","\u002Fconcepts\u002Frestate-primer","2.concepts\u002F2.restate-primer","i-lucide-cpu",{"title":50,"path":51,"stem":52,"icon":53},"Entities & addressing","\u002Fconcepts\u002Fentities-addressing","2.concepts\u002F3.entities-addressing","i-lucide-at-sign",{"title":55,"path":56,"stem":57,"icon":58},"Timelines & events","\u002Fconcepts\u002Ftimelines-events","2.concepts\u002F4.timelines-events","i-lucide-list-ordered",{"title":60,"path":61,"stem":62,"icon":63},"Projections & the catalog","\u002Fconcepts\u002Fprojections-catalog","2.concepts\u002F5.projections-catalog","i-lucide-database",{"title":65,"path":66,"stem":67,"icon":68},"Workspaces & execution","\u002Fconcepts\u002Fworkspaces","2.concepts\u002F6.workspaces","i-lucide-terminal",{"title":70,"path":71,"stem":72,"icon":73},"Harnesses","\u002Fconcepts\u002Fharnesses","2.concepts\u002F7.harnesses","i-lucide-plug",{"title":75,"path":76,"stem":77,"icon":78},"Multi-agent patterns","\u002Fconcepts\u002Fmulti-agent","2.concepts\u002F8.multi-agent","i-lucide-network",{"title":80,"path":81,"stem":82,"icon":83},"Lifecycle & control","\u002Fconcepts\u002Flifecycle","2.concepts\u002F9.lifecycle","i-lucide-sliders-horizontal",false,{"title":86,"icon":87,"path":88,"stem":89,"children":90,"page":84},"Guides","i-lucide-book-open","\u002Fguides","3.guides",[91,106],{"title":92,"icon":93,"path":94,"stem":95,"children":96,"page":84},"Agents","i-lucide-bot","\u002Fguides\u002Fagents","3.guides\u002F1.agents",[97,101],{"title":98,"path":99,"stem":100,"icon":93},"Building agents","\u002Fguides\u002Fagents\u002Fbuilding-agents","3.guides\u002F1.agents\u002F1.building-agents",{"title":102,"path":103,"stem":104,"icon":105},"Frontend integration","\u002Fguides\u002Fagents\u002Ffrontend-integration","3.guides\u002F1.agents\u002F2.frontend-integration","i-lucide-monitor",{"title":107,"icon":108,"path":109,"stem":110,"children":111,"page":84},"Operations","i-lucide-server-cog","\u002Fguides\u002Foperations","3.guides\u002F2.operations",[112,117,122],{"title":113,"path":114,"stem":115,"icon":116},"Self-hosting","\u002Fguides\u002Foperations\u002Fself-hosting","3.guides\u002F2.operations\u002F1.self-hosting","i-lucide-server",{"title":118,"path":119,"stem":120,"icon":121},"Auth & API keys","\u002Fguides\u002Foperations\u002Fauth-api-keys","3.guides\u002F2.operations\u002F2.auth-api-keys","i-lucide-key-round",{"title":123,"path":124,"stem":125,"icon":126},"Backup & restore","\u002Fguides\u002Foperations\u002Fbackup-restore","3.guides\u002F2.operations\u002F3.backup-restore","i-lucide-database-backup",{"title":128,"icon":129,"path":130,"stem":131,"children":132,"page":84},"Reference","i-lucide-book-marked","\u002Freference","4.reference",[133,138,143,148,153],{"title":134,"path":135,"stem":136,"icon":137},"Event schema","\u002Freference\u002Fevents","4.reference\u002F1.events","i-lucide-list-tree",{"title":139,"path":140,"stem":141,"icon":142},"Addressing","\u002Freference\u002Faddressing","4.reference\u002F2.addressing","i-lucide-map-pin",{"title":144,"path":145,"stem":146,"icon":147},"Gateway HTTP API","\u002Freference\u002Fgateway-api","4.reference\u002F3.gateway-api","i-lucide-webhook",{"title":149,"path":150,"stem":151,"icon":152},"CLI","\u002Freference\u002Fcli","4.reference\u002F4.cli","i-lucide-square-terminal",{"title":154,"path":155,"stem":156,"icon":83},"Configuration","\u002Freference\u002Fconfiguration","4.reference\u002F5.configuration",{"title":158,"path":159,"stem":160,"children":161,"icon":163},"Contributing","\u002Fcontributing","5.contributing\u002F1.index",[162,164],{"title":158,"path":159,"stem":160,"icon":163},"i-lucide-git-pull-request",{"title":165,"path":166,"stem":167,"icon":168},"The package map","\u002Fcontributing\u002Fpackage-map","5.contributing\u002F2.package-map","i-lucide-boxes",{"id":170,"title":118,"body":171,"description":755,"extension":756,"links":757,"meta":758,"navigation":759,"path":119,"seo":760,"stem":120,"__hash__":761},"docs\u002F3.guides\u002F2.operations\u002F2.auth-api-keys.md",{"type":172,"value":173,"toc":743},"minimark",[174,183,205,209,225,232,274,292,309,320,325,331,344,348,351,395,401,404,419,426,565,576,588,591,598,602,618,622,633,637,726,730,739],[175,176,177,178,182],"p",{},"teaspill authenticates every request at the ",[179,180,181],"a",{"href":14},"gateway",", and it gives you two credentials to do it with. Pick by where the caller runs.",[184,185,186,199],"ul",{},[187,188,189,193,194,198],"li",{},[190,191,192],"strong",{},"API keys"," are the primary path: a server-side secret that authorizes ",[195,196,197],"em",{},"everything"," — spawn, send, control, registration, reads. Put them in backends.",[187,200,201,204],{},[190,202,203],{},"Read tokens"," are optional and narrow: short-lived tokens that let a browser read streams and catalog data directly, and nothing else.",[206,207,192],"h2",{"id":208},"api-keys",[175,210,211,212,215,216,220,221,224],{},"An ",[190,213,214],{},"API key"," is the credential your backend, your UIs' server, and the CLI present to the gateway. Every route except the health check requires one, as ",[217,218,219],"code",{},"Authorization: Bearer \u003Ckey>",". A key looks like ",[217,222,223],{},"tsp_…",", is a 256-bit random value, and is all-or-nothing: there is no per-key scoping at the platform layer.",[175,226,227,228,231],{},"Mint keys with the ",[217,229,230],{},"teaspill keys"," command. It talks to the catalog database directly, so it needs a database connection rather than a gateway URL:",[233,234,239],"pre",{"className":235,"code":236,"language":237,"meta":238,"style":238},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","export DATABASE_URL='postgresql:\u002F\u002Fteaspill:teaspill@localhost:5432\u002Fteaspill?sslmode=disable'\n\nteaspill keys create --label my-backend   # prints the tsp_ token ONCE\nteaspill keys ls                           # id, status, created_at, label — never the token\nteaspill keys revoke \u003Cid | id-prefix | tsp_token>\n","sh","",[217,240,241,249,256,262,268],{"__ignoreMap":238},[242,243,246],"span",{"class":244,"line":245},"line",1,[242,247,248],{},"export DATABASE_URL='postgresql:\u002F\u002Fteaspill:teaspill@localhost:5432\u002Fteaspill?sslmode=disable'\n",[242,250,252],{"class":244,"line":251},2,[242,253,255],{"emptyLinePlaceholder":254},true,"\n",[242,257,259],{"class":244,"line":258},3,[242,260,261],{},"teaspill keys create --label my-backend   # prints the tsp_ token ONCE\n",[242,263,265],{"class":244,"line":264},4,[242,266,267],{},"teaspill keys ls                           # id, status, created_at, label — never the token\n",[242,269,271],{"class":244,"line":270},5,[242,272,273],{},"teaspill keys revoke \u003Cid | id-prefix | tsp_token>\n",[175,275,276,279,280,283,284,287,288,291],{},[217,277,278],{},"create"," prints the plaintext token exactly once; the database stores only its sha256 hash, so the token is never recoverable — capture it now or mint a new one. ",[217,281,282],{},"revoke"," is a soft delete the gateway rejects on immediately; it accepts a full key id, an id prefix, or the plaintext token. Pass ",[217,285,286],{},"--json"," to any subcommand for machine-readable output, or ",[217,289,290],{},"--database-url"," instead of the environment variable.",[293,294,295],"note",{},[175,296,297,300,301,304,305,308],{},[217,298,299],{},"keys"," is the one CLI command that needs ",[217,302,303],{},"DATABASE_URL"," rather than ",[217,306,307],{},"--gateway",". If you run it from outside the Compose network, point it at the host-published Postgres port as shown above.",[310,311,312],"accordion",{},[313,314,317],"accordion-item",{"icon":315,"label":316},"i-lucide-microscope","Why keys are minted against the database, not through a gateway route",[175,318,319],{},"Minting a key is an operator action. The gateway has no admin tier — every route is guarded by an all-or-nothing API key — so there is no privileged caller a \"mint a key\" route could trust without inventing one. The operator who can already reach the catalog database is, by definition, trusted, so key administration lives there directly. This keeps the gateway's auth model to a single, simple rule.",[321,322,324],"h3",{"id":323},"the-bootstrap-key","The bootstrap key",[175,326,327,330],{},[217,328,329],{},"GATEWAY_BOOTSTRAP_API_KEY"," is a literal key the gateway accepts without any database row, so a fresh stack is usable before you have minted anything — the reference deployment uses it to register on first boot.",[332,333,334],"warning",{},[175,335,336,337,340,341,343],{},"The bootstrap key is a dev convenience only. It bypasses the database entirely, so it cannot be revoked without restarting the gateway. Mint real keys with ",[217,338,339],{},"teaspill keys create"," and remove ",[217,342,329],{}," before production.",[321,345,347],{"id":346},"minting-programmatically","Minting programmatically",[175,349,350],{},"The same primitives are available in code, for a provisioning script or an admin panel of your own:",[233,352,356],{"className":353,"code":354,"language":355,"meta":238,"style":238},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { createApiKey } from \"@teaspill\u002Fcatalog\"; \u002F\u002F mints the token + stores its hash\n","ts",[217,357,358],{"__ignoreMap":238},[242,359,360,364,368,372,375,378,381,385,388,391],{"class":244,"line":245},[242,361,363],{"class":362},"s7zQu","import",[242,365,367],{"class":366},"sMK4o"," {",[242,369,371],{"class":370},"sTEyZ"," createApiKey",[242,373,374],{"class":366}," }",[242,376,377],{"class":362}," from",[242,379,380],{"class":366}," \"",[242,382,384],{"class":383},"sfazB","@teaspill\u002Fcatalog",[242,386,387],{"class":366},"\"",[242,389,390],{"class":366},";",[242,392,394],{"class":393},"sHwdD"," \u002F\u002F mints the token + stores its hash\n",[175,396,397,400],{},[217,398,399],{},"createApiKey"," returns the plaintext token once, exactly like the CLI, and persists only the hash.",[206,402,203],{"id":403},"read-tokens",[175,405,406,407,410,411,414,415,418],{},"By default, browser reads go through your backend: your server holds the API key and proxies stream requests. That is fine, but it puts your backend in the path of the chattiest traffic. The ",[190,408,409],{},"read token"," removes it: a short-lived token, minted by your server, that lets a browser read ",[217,412,413],{},"\u002Fstreams\u002F*"," and ",[217,416,417],{},"\u002Fshapes\u002F*"," directly from the gateway.",[175,420,421,422,425],{},"Enable the path by setting ",[217,423,424],{},"GATEWAY_JWT_SECRET"," on the gateway. With no secret set, the path is off and only API keys are accepted. Then mint tokens server-side:",[233,427,429],{"className":353,"code":428,"language":355,"meta":238,"style":238},"import { mintReadToken } from \"@teaspill\u002Fagents-sdk\";\n\nconst jwt = await mintReadToken({\n  \u002F\u002F one entity prefix covers both its timeline and its deltas\n  pfx: \"\u002Fstreams\u002Ft\u002Fdefault\u002Fagents\u002Fresearcher\u002F01j...\u002F\",\n  ttlSeconds: 300, \u002F\u002F keep it short — reconnecting is cheap\n  secret: process.env.GATEWAY_JWT_SECRET!,\n});\n\u002F\u002F hand `jwt` to the browser; it sends `Authorization: Bearer \u003Cjwt>`\n",[217,430,431,454,458,482,487,506,524,548,559],{"__ignoreMap":238},[242,432,433,435,437,440,442,444,446,449,451],{"class":244,"line":245},[242,434,363],{"class":362},[242,436,367],{"class":366},[242,438,439],{"class":370}," mintReadToken",[242,441,374],{"class":366},[242,443,377],{"class":362},[242,445,380],{"class":366},[242,447,448],{"class":383},"@teaspill\u002Fagents-sdk",[242,450,387],{"class":366},[242,452,453],{"class":366},";\n",[242,455,456],{"class":244,"line":251},[242,457,255],{"emptyLinePlaceholder":254},[242,459,460,464,467,470,473,476,479],{"class":244,"line":258},[242,461,463],{"class":462},"spNyl","const",[242,465,466],{"class":370}," jwt ",[242,468,469],{"class":366},"=",[242,471,472],{"class":362}," await",[242,474,439],{"class":475},"s2Zo4",[242,477,478],{"class":370},"(",[242,480,481],{"class":366},"{\n",[242,483,484],{"class":244,"line":264},[242,485,486],{"class":393},"  \u002F\u002F one entity prefix covers both its timeline and its deltas\n",[242,488,489,493,496,498,501,503],{"class":244,"line":270},[242,490,492],{"class":491},"swJcz","  pfx",[242,494,495],{"class":366},":",[242,497,380],{"class":366},[242,499,500],{"class":383},"\u002Fstreams\u002Ft\u002Fdefault\u002Fagents\u002Fresearcher\u002F01j...\u002F",[242,502,387],{"class":366},[242,504,505],{"class":366},",\n",[242,507,509,512,514,518,521],{"class":244,"line":508},6,[242,510,511],{"class":491},"  ttlSeconds",[242,513,495],{"class":366},[242,515,517],{"class":516},"sbssI"," 300",[242,519,520],{"class":366},",",[242,522,523],{"class":393}," \u002F\u002F keep it short — reconnecting is cheap\n",[242,525,527,530,532,535,538,541,543,545],{"class":244,"line":526},7,[242,528,529],{"class":491},"  secret",[242,531,495],{"class":366},[242,533,534],{"class":370}," process",[242,536,537],{"class":366},".",[242,539,540],{"class":370},"env",[242,542,537],{"class":366},[242,544,424],{"class":370},[242,546,547],{"class":366},"!,\n",[242,549,551,554,557],{"class":244,"line":550},8,[242,552,553],{"class":366},"}",[242,555,556],{"class":370},")",[242,558,453],{"class":366},[242,560,562],{"class":244,"line":561},9,[242,563,564],{"class":393},"\u002F\u002F hand `jwt` to the browser; it sends `Authorization: Bearer \u003Cjwt>`\n",[175,566,567,568,571,572,575],{},"The token carries a single path-prefix claim, ",[217,569,570],{},"pfx",". The gateway authorizes a request only if the requested path starts with that prefix, so one token scopes a browser to exactly one ",[179,573,574],{"href":51},"entity's"," streams.",[332,577,578],{},[175,579,580,581,584,585,587],{},"Mint the prefix with a ",[190,582,583],{},"trailing slash",". ",[217,586,500],{}," matches that entity's streams but not a sibling whose id merely starts the same way. Without the slash, the prefix leaks across neighboring entities.",[175,589,590],{},"Keep TTLs short. When a token expires, the gateway returns 401 with a body telling the client to reconnect with a fresh one — cheap, because streams are resumable: the browser resumes from its last offset. Expiry is checked with a small clock-skew leeway so a token that tips over mid-request is not rejected spuriously.",[175,592,593,594,597],{},"A read token is ",[190,595,596],{},"reads-only, by construction",". It is honored only on GET requests to the two read route families; on any write route or non-GET method it is not even considered. A read token can never spawn, send, or control an agent — those always require an API key, so writes never bypass your backend.",[321,599,601],{"id":600},"cors-for-browser-reads","CORS for browser reads",[175,603,604,605,414,607,609,610,613,614,617],{},"Because browsers read cross-origin, the gateway answers CORS preflight and sets response headers for GET on ",[217,606,413],{},[217,608,417],{}," only — never for the write routes. The default allowed origin is ",[217,611,612],{},"*",", which is safe here: the read token, not the origin, gates the actual read. Pin it by setting ",[217,615,616],{},"GATEWAY_CORS_ALLOW_ORIGINS"," to a comma-separated list.",[206,619,621],{"id":620},"you-own-authorization","You own authorization",[175,623,624,625,628,629,632],{},"teaspill has no permissions model. It answers one question — ",[195,626,627],{},"is this a valid credential?"," — and never ",[195,630,631],{},"is this caller allowed to do this particular thing?"," Deciding which user may spawn which agent, or read which conversation, is your backend's job: hold the API key server-side, apply your own authorization, and mint a narrowly-scoped read token only for the streams a given user is allowed to see. This is a deliberate stance, not a gap — a platform-level permissions model would be a second, weaker copy of the one your application already has.",[206,634,636],{"id":635},"environment-reference","Environment reference",[638,639,640,656],"table",{},[641,642,643],"thead",{},[644,645,646,650,653],"tr",{},[647,648,649],"th",{},"Variable",[647,651,652],{},"Default",[647,654,655],{},"Meaning",[657,658,659,672,683,694,709],"tbody",{},[644,660,661,666,669],{},[662,663,664],"td",{},[217,665,303],{},[662,667,668],{},"—",[662,670,671],{},"Postgres connection for API keys. Required unless the bootstrap key is set.",[644,673,674,678,680],{},[662,675,676],{},[217,677,329],{},[662,679,668],{},[662,681,682],{},"Dev-only literal key accepted without a database row.",[644,684,685,689,691],{},[662,686,687],{},[217,688,424],{},[662,690,668],{},[662,692,693],{},"Shared secret enabling the read-token path. Unset means the path is off.",[644,695,696,701,706],{},[662,697,698],{},[217,699,700],{},"GATEWAY_JWT_CLOCK_TOLERANCE_SECONDS",[662,702,703],{},[217,704,705],{},"60",[662,707,708],{},"Clock-skew leeway when checking a read token's expiry.",[644,710,711,715,719],{},[662,712,713],{},[217,714,616],{},[662,716,717],{},[217,718,612],{},[662,720,721,722,414,724,537],{},"Allowed origins for GET reads of ",[217,723,413],{},[217,725,417],{},[206,727,729],{"id":728},"next-steps","Next steps",[175,731,732,733,736,737,537],{},"The read routes these credentials protect are specified in the ",[179,734,735],{"href":145},"Gateway API reference","; using read tokens from a UI is walked through in ",[179,738,102],{"href":103},[740,741,742],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":238,"searchDepth":245,"depth":251,"links":744},[745,749,752,753,754],{"id":208,"depth":251,"text":192,"children":746},[747,748],{"id":323,"depth":258,"text":324},{"id":346,"depth":258,"text":347},{"id":403,"depth":251,"text":203,"children":750},[751],{"id":600,"depth":258,"text":601},{"id":620,"depth":251,"text":621},{"id":635,"depth":251,"text":636},{"id":728,"depth":251,"text":729},"The server-side API key that authorizes everything, the optional read token that lets browsers read streams directly, and why authorization is yours to own.","md",null,{},{"icon":121},{"title":118,"description":755},"vH9vAd8pdTqEybvVF_5k5PmjcyQ8kipuc_ki60I6m8Y",[763,765],{"title":113,"path":114,"stem":115,"description":764,"icon":116,"children":-1},"Run the teaspill stack yourself — the five infrastructure services, your two deployed planes, and the one networking rule that bites everyone.",{"title":123,"path":124,"stem":125,"description":766,"icon":126,"children":-1},"What each store holds, how to capture a consistent backup, and exactly which restore combinations come back cleanly versus lossily — and why.",1784473426487]