Skip to main content

Quickstart

The fastest way to understand CNS is to emit one event and watch it come back. This page uses the repo's dev fixture — a local cns-server with no cloud credentials — so you can follow along without provisioning ClickHouse, Centrifugo, or AWS SSM first. For a real deployment, see Self-Hosting & Operations.

:::note Prerequisites A Go 1.25.5+ toolchain (to build cns-server locally) and curl. Node.js if you want to run the TypeScript snippets below. :::

1. Start an instance

From the repository root:

scripts/dev-fixture.sh

This script (verified in AGENTS.md's 2026-09-16 dev-fixture changelog entry):

  1. builds and boots cns-server on 127.0.0.1:8098 with an in-memory event store, an in-memory ed25519 keystore (never AWS SSM), the sso-jwt identity adapter using a fixed local HMAC secret, and the realtime adapter left unconfigured (noop) — explicitly unsetting any AWS_*/ClickHouse/Centrifugo variables from your shell so the fixture can never touch real infrastructure;
  2. seeds three example events so GET /admin/v1/activity has something to show immediately;
  3. mints a bearer token for a fixed sample subject and grants it tenant-admin;
  4. prints where it wrote that token and blocks in the foreground (run the script itself in the background if you want the server to keep running).

The fixture hardcodes its sample identity — subject sub_dev_fixture, tenant katyayani (a fixed literal in the script, not something this guide chose) — and writes the minted token to the path named by $TOKEN_FILE (the script prints the exact path it used). Export it for the rest of this walkthrough:

export CNS_DEV_API="http://127.0.0.1:8098"
export TOKEN="$(cat <path the script printed>)"

Confirm it's up:

curl -s "$CNS_DEV_API/healthz"
# -> ok

curl -s -H "Authorization: Bearer $TOKEN" "$CNS_DEV_API/admin/v1/me"
# -> {"subject_id":"sub_dev_fixture","tenant_id":"katyayani","roles":["tenant-admin","platform-operator"]}

:::info Not ready to run it locally? The rest of this page works identically against any running cns-server — swap CNS_DEV_API/$TOKEN for your instance's URL and a real bearer token (see Authentication and Managing apps & key rotation). :::

2. Emit your first event

This walkthrough authenticates as an end user (a subject principal) — the token above is exactly that shape. A backend service emitting on its own behalf uses an app/publisher service-JWT instead; see Emitting events: app vs. subject principal.

import { CNSClient } from "@amphoze/cns-client";

const client = new CNSClient({
baseUrl: process.env.CNS_DEV_API!, // e.g. "http://127.0.0.1:8098"
getToken: () => process.env.TOKEN!,
});

const event = await client.emit("quickstart.hello", {
data: { message: "hello from the quickstart" },
});

console.log(event.event_id, event.seq, event.provenance);
// -> "01J...", 0, "client-asserted"

:::caution Not yet published @amphoze/cns-client is implemented (sdk/ts/packages/client) but not yet published to a package registry (workspace version 0.1.0). Until it is, consume it from within the sdk/ts npm workspace, or build it (npm run build in sdk/ts/) and reference the resulting dist/ directly. This is a real gap, not an oversight — flagging it here rather than a working npm install command. :::

Every event lands in the chronological store immediately, whether or not any policy matches it as notify-worthy (see Policies) — that's why the next two steps work with no extra setup.

3. Read it back from the feed

curl -s -H "Authorization: Bearer $TOKEN" "$CNS_DEV_API/v1/feed" | python3 -m json.tool
{
"Events": [
{
"event_id": "01J...",
"tenant_id": "katyayani",
"event_type": "quickstart.hello",
"actor": { "subject_id": "sub_dev_fixture" },
"target": { "subject_ids": [] },
"provenance": "client-asserted",
"delivery": "instant",
"event_time": "2026-09-20T10:00:00Z",
"ingest_time": "2026-09-20T10:00:00.123Z",
"session_id": "...",
"seq": 0,
"data": { "message": "hello from the quickstart" }
}
// ...the 3 seeded example events...
],
"NextCursor": "",
"HasMore": false
}

:::caution Verified wire shape: PascalCase, not the lowercase in ROUTES.md GET /v1/feed serializes adapters/store.PageResult directly (json.NewEncoder(w).Encode(result) in api/handlers.go), and that Go struct has no JSON tags — so it marshals as Events/NextCursor/HasMore (Go's default, PascalCase), not the lowercase events/next_cursor/has_more that both api/ROUTES.md's prose and the TypeScript SDK's PageResult type currently claim. This is a real, source-verified discrepancy between the docs/SDK and the running server, not a documentation choice — see the caution box in API Reference → Data plane for the full detail and what it means for @amphoze/cns-core's PageResult type. :::

GET /v1/feed defaults to the caller's own subject_id when none is given, and matches events where the caller is actor or target — which is why the event you just emitted (actor = you) shows up immediately.

4. Watch it live

curl -N -H "Authorization: Bearer $TOKEN" "$CNS_DEV_API/v1/stream"

In a second terminal, emit another event (step 2 again) — with a configured realtime adapter, it would arrive on the open connection as one data: <event JSON>\n\n SSE frame per event, matching the caller's own subject by default.

:::caution Won't work against the plain dev fixture — verified, not a fixture bug internal/adapters/realtime/noop's AuthorizeSubscribe fails closed (returns Deny) by design, and GET /v1/stream calls it as a second authorization check whenever a realtime adapter is wired at all (cmd/cns-server's buildRealtime always wires something — noop when unconfigured, never nil). The dev fixture never sets CNS_CENTRIFUGO_ADDR, so its realtime adapter is noop — meaning GET /v1/stream returns 403 against an out-of-the-box fixture, regardless of whether you're authorized to read the feed. This is a genuine Phase-1 gap, verified by reading internal/adapters/realtime/noop/noop.go and api/handlers.go's getStream directly, not documented elsewhere as of this writing. To see this step actually stream, point CNS_CENTRIFUGO_ADDR at a real Centrifugo instance (see Infrastructure setup). :::

Also note: GET /v1/stream and the TS SDK's CNSClient.subscribe() are two different transports, not the same feature via two doors — see Realtime & streaming before you reach for subscribe() expecting it to hit this endpoint.

Next steps