Embedded space provisioning
One-space provisioning
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:
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:
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 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.
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.
Issued-authority stores from before 0.111.0
A space whose KV_cotal_issued_<space> and KV_cotal_accepted_<space> 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:
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 againThe 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.
Provisioning authority
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 astripSpaceAuthbundle, which keeps the data signing seed) is a fully-trusted tenant-account authority: it can mintadmin,provisioner, and destructive profiles, not merelysupervisor, and mint a DM-reading identity.createSpaceAuth's full result holds operator, system, and account seeds in memory. - Choose
profileandMintOptsfrom server-side constants, never from tenant input.MintOptscan widen the bounded TTL defaults; cap it at your boundary.CREDENTIAL_LIFETIMESis 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
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.
- Supervisor signer isolation.
ManagerOptions.secretStorenow injects the oneSecretStorethe manager reads/writes every secret through, including the composedSpaceAuthsigner (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. - Per-space lifecycle on a shared broker. The trust layer is multi-space
(
createBrokerAuth+createSpaceAccountAuth+ N-spaceserverConfig, persisted asbroker.json+account.<key>.json) andcotal uprenders the whole tenant list. The CLI still lacks tenant-account provisioning and per-space teardown/backup/restore on that broker. Its broker-wide lifecycle verbs refuse on a multi-space root, naming the tenants. The existingcotal space addregisters a connection, andspace deleteremoves a space's streams; neither supplies that tenant-account lifecycle. - 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,cotalAuthProviderself-registers on import (colliding with a host-owned provider underresolveAuthProvider), 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).
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.<key>.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.<hex>/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.<key>.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 and live-eviction limitations remain relevant to a hosted composition. See the detailed contract.
Signer process isolation
A host must isolate the process that holds signing authority. See the detailed contract.
Shared broker lifecycle
Trust for several spaces and broker-wide lifecycle operations have different boundaries. See the detailed contract.
Other identity providers
Production identity providers need the bridge and provider wiring described in the compatibility contract. See the detailed contract.