--- title: Embedded space provisioning description: Broker trust, space credentials, durable host state, and current hosting limitations. kind: Reference audience: integrators --- ## One-space provisioning [#provisioning-a-space-one-space-reference-shape] The one-space convenience API combines broker trust and one data account. A host first creates that trust and renders configuration for a broker it controls: ```ts import { createSpaceAuth, serverConfig } from "@cotal-ai/core"; const auth = await createSpaceAuth(space); const config = serverConfig(auth, [auth], { storeDir, transport }); ``` `space`, `storeDir`, and `transport` are host-selected values. `transport` is required: `{ kind: "plaintext" }` or `{ kind: "tls-required", certFile, keyFile }`. TLS policy applies to the broker listener, not to one space. The host persists the trust material and starts nats-server with this configuration before opening provisioning connections. `config` alone does not start a broker. The following fragment assumes that `brokerUrl` points to that broker and `store` is the injected secret store: ```ts import { ensureDefaultDeliveryClass, mintCreds, newIdentity, setupSpaceStreams, } from "@cotal-ai/core"; import { deliveryCredsKey } from "@cotal-ai/workspace"; const provisionerCreds = await mintCreds(auth, newIdentity(), "provisioner"); await setupSpaceStreams({ servers: brokerUrl, space, creds: provisionerCreds }); await ensureDefaultDeliveryClass({ servers: brokerUrl, space, creds: provisionerCreds, deliveryClass: "durable", }); const deliveryCreds = await mintCreds(auth, newIdentity(), "delivery"); await store.put(deliveryCredsKey(space, { injected: true }), deliveryCreds); ``` [SPEC ยง4](../SPEC.md#4-delivery-modes) requires the creation-time delivery class to be discoverable on the wire. A daemon-backed space records `durable`; it does not rely on a receiver's fallback policy. The delivery credential key is per-space. The bare `delivery.creds` kind is not its store key. For a user-auth space, pass the account created by `createCalloutAuth` in `extraAccounts` when calling `serverConfig`. That callout account is separate from the space data account. The host must provision auth material and its IdP pin and ledger before starting the [auth service](embedding-daemons.mdx#auth-service). `maxFileStore` is an optional positive integer cap in bytes. Other values throw. ### Multiple spaces on a broker Broker trust and space accounts are separate authorities: `createBrokerAuth` mints the one operator + system account a broker trusts, `createSpaceAccountAuth(broker, space)` signs each tenant's data account under it, and `serverConfig(broker, spaces, opts)` renders them all into one config. A host composition can therefore provision several spaces on one broker today. `cotal up` renders that config from every tenant the root's auth directory holds, so booting one space keeps the broker trusting its siblings, and it refuses to render at all while any account record is unreadable. The rest of the CLI lifecycle is still broker-wide: `down`, `clean` and `backup` refuse on a multi-space root rather than scoping to one tenant, and the per-space lifecycle is the remaining multi-space operator layer. See [Known gaps](embedding.mdx#hosted-composition-gaps). #### Space accounts without a reload With the default MEMORY resolver, the broker learns accounts only from its config, so every added or removed space means rendering the whole config again and reloading the broker. A reload takes longer with every account already on it, and it re-runs auth callout for live callout sessions. A host can render the broker with `accountResolver: { kind: "full", directory, limit, intervalMs }` instead. nats-server then keeps each account's signed claim in `directory`, and a space account is added at run time: ```ts import { createSpaceAccountAuth, pushAccountClaim, retireAccountClaim } from "@cotal-ai/core"; const space = await createSpaceAccountAuth(broker, "acme"); // `system` is a connection as a system-account user allowed to publish $SYS.REQ.ACCOUNT.*.CLAIMS.UPDATE. await pushAccountClaim(system, { accountJwt: space.account.jwt, expectedServerIds: [system.info!.server_id], maxWaitMs: 5000 }); // Later, to stop the space: its live connections close and its users are refused. const retired = await retireAccountClaim(broker, space.account.jwt, Math.floor(Date.now() / 1000)); await pushAccountClaim(system, { accountJwt: retired, expectedServerIds: [system.info!.server_id], maxWaitMs: 5000 }); ``` - **A stock root records the choice.** `cotal up --account-resolver full` starts the broker on the full resolver at `/.cotal/account-jwts` and records it in `.cotal/broker-policy.json`. A host that renders the same broker passes `brokerAccountResolver(root, readBrokerPolicy(root)?.accountResolver)` from `@cotal-ai/workspace`, so both renders name the same directory and limit. nats-server switches resolvers only at start, so a reload that changes the resolver is refused. - **The directory is broker state.** It sits beside `store_dir`: give it the same ownership and backup coverage. A pushed claim survives a restart there even when the config does not list it. - **Keep rendering the config.** List every active space in the preloads as before, so a fresh directory boots complete. nats-server keeps a newer pushed claim over an older preload. - **Removal is a retired claim.** Dropping a space from the preloads does not remove it: its last pushed claim stays in the directory and loads again at the next start. Native deletion stays off. The retired claim keeps the account's JetStream data in place; deleting the space's streams is a separate step. - **One writer per account.** The host orders the pushes for one account and owns the set of servers it expects to answer. `pushAccountClaim` refuses an answer other than "jwt updated", an unexpected server, or a missing answer. - **Switching resolvers is a restart.** nats-server refuses to change resolvers on a reload. `cotal up` keeps using MEMORY. ## Issued-authority stores from before 0.111.0 A space whose `KV_cotal_issued_` and `KV_cotal_accepted_` were created before 0.111.0 is refused by every issuer and resolver that binds them. `cotal up --recreate-issued-stores` fixes the spaces `cotal up` starts. For a space your own composition provisions, stop its issuers and resolvers (auth service, callout replicas, managers) and run the same step yourself: ```ts import { connect } from "@nats-io/transport-node"; import { mintCreds, newIdentity, removeIssuedStoresBeforeRetention, setupSpaceStreams, standaloneConnectOpts } from "@cotal-ai/core"; const identity = newIdentity(); const inboxPrefix = `_INBOX_${identity.id}`; // the one inbox the credential may receive on const creds = await mintCreds(auth, identity, "issued-store-recreate"); // one-shot, 30 minutes const nc = await connect({ servers: brokerUrl, ...standaloneConnectOpts({ creds, tls: false }), inboxPrefix }); try { // backupDir must not exist yet; it is created 0700 with a `nats stream backup` layout per stream await removeIssuedStoresBeforeRetention(nc, { space, backupDir, inboxPrefix }); } finally { await nc.drain(); } await setupSpaceStreams({ servers: brokerUrl, space, creds: provisionerCreds }); // creates both again ``` The credential can read, snapshot and delete those two streams and nothing else. The step backs up every store that predates bounded retention, checks each backup against its stream's message count and last sequence, and deletes nothing unless every check passes. It throws `IssuedStoreRecreateRefusal` before writing anything when neither store predates bounded retention. The order, the refusals and what a rollback can do are in [Upgrading](UPGRADING.md#issued-authority-stores-after-01110). ## Provisioning authority [#hazardous-provisioning-primitives] `mintCreds`, the full `Profile`/`CredentialKind` matrix, `createSpaceAuth`, and `stripSpaceAuth` are low-level operator primitives. Handle them as account-authority material: - A holder of a `SpaceAuth` (or a `stripSpaceAuth` bundle, which **keeps** the data signing seed) is a fully-trusted tenant-account authority: it can mint `admin`, `provisioner`, and destructive profiles, not merely `supervisor`, and mint a DM-reading identity. `createSpaceAuth`'s full result holds operator, system, and account seeds in memory. - Choose `profile` and `MintOpts` from **server-side constants**, never from tenant input. `MintOpts` can widen the bounded TTL defaults; cap it at your boundary. `CREDENTIAL_LIFETIMES` is a policy record, not an authorization boundary. - Never log signer material or export it into env. Do not co-locate signer access with an untrusted connector/runtime process at the same OS uid (file permissions do not contain a same-uid reader; see the manager's isolation note). Segregate per tenant; rotate on compromise (`rotateDataAccountSigningKey`). ## Hosted composition gaps [#hosted-composition-gaps] Three composition limits remain. A host must supply the missing isolation or integration, or leave the affected capability unavailable. Membership and live eviction already read their credentials through `SecretStore`. `startMembership` reads the data-account writer and system-account observer there; eviction reads the observer and evictor pair on each operation. Both use per-space keys and check that the observer names the daemon's account. The feed derives that account from the daemon's credential, without `membership.json`. The host still owns provisioning and rotation. An absent observer leaves the feed traffic-only; missing eviction authority refuses eviction. The membership reader can adopt a host-renewed observer with the same issuer and scope at 75% of its lifetime. 1. **Supervisor signer isolation.** `ManagerOptions.secretStore` now injects the one `SecretStore` the manager reads/writes every secret through, including the composed `SpaceAuth` signer (the split trust records), its daemon-cred renewal, and its per-agent kinds. What remains is process isolation: the manager still decrypts the signer in-process at its uid, so untrusted agent children must run under a different uid/container/mount namespace or behind a future remote signer. 2. **Per-space lifecycle on a shared broker.** The trust layer is multi-space (`createBrokerAuth` + `createSpaceAccountAuth` + N-space `serverConfig`, persisted as `broker.json` + `account..json`) and `cotal up` renders the whole tenant list. The CLI still lacks tenant-account provisioning and per-space teardown/backup/restore on that broker. A host composition can add and retire space accounts without a reload through the full resolver ([above](#space-accounts-without-a-reload)). Its broker-wide lifecycle verbs refuse on a multi-space root, naming the tenants. The existing `cotal space add` registers a connection, and `space delete` removes a space's streams; neither supplies that tenant-account lifecycle. 3. **A non-Better-Auth production IdP.** The exchange core (`createIdpBridge`) is EdDSA-generic, but the stock provider and login client are Better-Auth-endpoint-shaped, `cotalAuthProvider` self-registers on import (colliding with a host-owned provider under `resolveAuthProvider`), and the login flow speaks Better Auth's device-code endpoints. A different IdP is a host-built auth composition on the low-level primitives, not a configuration change (see [the IdP callout contract](identity-and-auth.mdx#the-idp-callout-contract)). ## Hosted durability [#hosted-durability] Space-durable **coordination** state (chat/DM/task history, live presence, membership runtime, the durable ACL registry, leases) lives in **JetStream**, written by the delivery daemon and the endpoints. It is broker-resident and needs no host-side durable path. What is **not** in JetStream, and is hosting-critical, is trust and authorization state a host must place and keep: | state | class | where today | hosted injection | |---|---|---|---| | full `SpaceAuth` trust chain (`auth/broker.json` + `auth/account..json`, composed; a stripped signer bundle may instead be mounted at the legacy `auth/auth.json` key) | signing authority | `SecretStore` | `SecretStore` (manager + renewal) | | auth kinds: callout account/creds/xkey, issuer private keys, owner-derivation secret, data-signer projection | signing/identity authority | four `SecretStore` kinds | `SecretStore` (auth-service) | | auth plane instance identity (instance id + serve nkey seed) | restart identity | root `.cotal/space./auth-instance.json` | `SecretStore` (`startAuthService`) | | `delivery.creds` | standing scoped cred | `SecretStore` or `--creds` | `SecretStore` (delivery) | | actor ledger, IdP pin | authorization + trust config | ambient `userAuthStateDir(findCotalRoot(), space)` | none (root-relative; not `store`/`COTAL_HOME`) | | `membership-rw.creds` | standing scoped cred | `SecretStore` | `SecretStore` (delivery + manager renewal) | | membership-observer / connection-evictor creds | scoped $SYS creds | per-space `SecretStore` keys | injected `SecretStore`; account identity is checked against the daemon credential, not `membership.json` | | manager agent creds, actor tokens, sentinel creds | lifecycle authority | `SecretStore` | `SecretStore` (manager `secretStore`) | | `~/.cotal/meshes/space..json` record (holds IdP trust pins/root pointers) | non-secret, integrity-critical | machine home | process-global `COTAL_HOME` only | | auth-health, renewal records | non-secret diagnostics | workspace filesystem | `workspaceRoot` | The `SpaceAuth` trust chain and the auth-service store kinds are **separate** identities/projections, never parts of one document. `auth-service.json` (the live exchange capability) is ephemeral runtime state, not durable, but is sensitive while the daemon runs. `@cotal-ai/workspace` is machine-local operator tooling by design; personas, PID files, and the `current-mesh` pointer are truly local and must **not** sit on a hosted durable path. Everything classed above as an authority is what a hosted composition must provision and persist: signer-bearing server secrets now have `SecretStore` seams; the remaining non-injectable rows are the explicit ambient `workspaceRoot`/cwd paths above. ## Related contracts ### Membership limitations [#membership-limitations] Membership and live-eviction limitations remain relevant to a hosted composition. See [the detailed contract](embedding-provisioning.mdx#hosted-composition-gaps). ### Signer process isolation [#signer-process-isolation] A host must isolate the process that holds signing authority. See [the detailed contract](embedding-provisioning.mdx#hosted-composition-gaps). ### Shared broker lifecycle [#shared-broker-lifecycle] Trust for several spaces and broker-wide lifecycle operations have different boundaries. See [the detailed contract](embedding-provisioning.mdx#multiple-spaces-on-a-broker). ### Other identity providers [#other-identity-providers] Production identity providers need the bridge and provider wiring described in the compatibility contract. See [the detailed contract](identity-service-auth.mdx#idp-compatibility).