Skip to main content

Apps & credentials

An app (also called a publisher) is the service-to-service identity a backend uses to emit events on its own behalf — as opposed to an end user emitting via their own SSO session. If a background job, webhook handler, or backend service needs to call POST /v1/events without a logged-in user attached, it does so as an app.

Registering an app

A tenant-admin registers an app through the Admin API:

// POST /admin/v1/apps
{ "app_id": "billing-svc", "name": "Billing Service", "allowed_event_types": ["invoice.paid"] }

allowed_event_types: [] (or omitted) means no restriction — the app may emit any event type for its tenant. Registering (and rotating) an app writes a fresh ed25519 keypair to the secrets adapter — private key as a secret, public key as plain, non-secret material — and immediately updates the same publish-authorizer instance POST /v1/events enforces against. There is no separate sync step.

Proving you're the app: the service-JWT

An app authenticates by minting its own short-lived service-JWT with the private key it was issued, and sending it as the Bearer token:

{ "app": "billing-svc", "tenant": "acme", "key_ver": 1, "iss": "cns-service-auth", "iat": ..., "exp": ... }

Signed EdDSA (ed25519). iss is always the fixed string cns-service-auth — combined with the EdDSA algorithm, this is exactly what lets the server tell a service-JWT apart from an end-user SSO JWT on POST /v1/events (the one route that accepts either), without needing to know every tenant's IdP issuer in advance. Unlike a render-token, a service-JWT has no single-use nonce — it's a normal Bearer credential, reused across requests until it expires; keep its TTL short and re-mint proactively.

:::caution No TypeScript minting helper yet The Go core ships trust.ServiceMinter for this. The TypeScript SDK's @amphoze/cns-server (CNSServer) expects you to already have a getServiceToken() function that returns a valid service-JWT — it does not include a minting helper itself. If your app backend isn't Go, you currently need to hand-roll the EdDSA JWT above (using the private key your secrets adapter materialized) until a TS/Python minting helper ships. See SDK Reference → TS Server. :::

Once authenticated as an app, POST /v1/events additionally checks (tenant_id, app_id, event_type) against the app's allowed_event_types403 if the app isn't registered or the event type isn't allowed. A successful app-principal emit is always stamped provenance: backend-verified (an authenticated, authorized app backend calling CNS directly is the server anchor — see Trust & render-tokens) and source.app is set from the token. See Guides → Emitting events for the full request shape, including the app-only actor_subject_id field.

Key rotation

POST /admin/v1/apps/{app_id}/rotate-keys

Issues a new key version; the app's public key is resolved by (app, key_version) on every verify, so the old key stays valid until any already-issued token against it naturally expires — the short token TTL is what bounds the overlap window, not a hard cutover. See Guides → Managing apps & key rotation.

Deleting an app

DELETE /admin/v1/apps/{app_id} deregisters the app from publish-authz immediately (a subsequent emit as this app is denied) but does not delete its key material from the secrets adapter — an operator revokes at the source. Flagged as a Phase-1 gap, not an oversight.