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_types — 403 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.