snoozestackdocs

Signals

One envelope for everything the app reports: events your apps track, logs your functions print, plus metrics, traces, and audits. Apps send events with the SDK; functions emit anything with capabilities.signals.emit(type, name, props); and everything lands in the project's signals store — charted in the console, streamed by the CLI, and readable through the query/export API below. The ingest key is write-only: it can send signals but never read them back, which is what makes it safe to ship in browsers and mobile apps.

Why use it

Logs tell you what your servers did; Signals tells you what your users did. Those are different questions, and the second one can't be reconstructed after the fact — if you didn't record that someone opened the checkout screen and then left, no amount of querying production tables later will recover it. Your database holds current state (this user has a subscription), not the sequence of actions that produced it.

The design point here is one vocabulary for the product and the system. Because a function's log lines, its emitted events, and the client's tracked events share an envelope and a store, “what did this user do” and “what did the software do to them” are the same query — and no user data leaves for a third-party analytics service, which is often the deciding factor for anything privacy-sensitive.

Instrument deliberately: a handful of events named after user intent (checkout_started, invite_sent) is far more useful than tracking every click. Event names are effectively permanent — charts and funnels are built on them, so renaming one orphans its history.

Funnels — where people drop off
Track each step (signup_startedemail_confirmed first_project_created) and the multi-step funnel chart shows which step loses people.
Did the feature land?
Ship the change, then compare event volume before and after to see whether anyone actually used the thing.
Debugging a specific user's report
“It didn't work for me yesterday” — the Users page has a per-user activity feed, so you can see the exact sequence of actions they took.
Retention and engagement
Unique users and events-per-user over 7/30/90 days, to tell “lots of traffic once” apart from “people keep coming back.”

The four pages#

PageWhat it's for
OverviewEvents over time segmented by event name, unique users, and a top-events breakdown — the daily health check
EventsThe live stream of raw events: filter by name, search by name or user, expand any row to see every property
UsersEveryone your apps have seen — one row per person, not per distinct_id — with event counts, first/last seen, and an activity feed spanning all of their ids
ChartsYour own dashboard — saved charts built from a metric, filters, and a breakdown (below)

On Charts you pick a metric (total events, unique users, or events per user, over all events or specific ones), add property filters (is / is not / contains / is set / is not set, with value suggestions drawn from your own data), and break results down by event name or any property. Results render as line, area, column, bar, donut, stat, table, or multi-step funnel charts, across 24-hour / 7-day / 30-day / 90-day ranges.

SurfaceAvailability
Portal UIOverview, Events, Users, and Charts — view and build dashboards, no write access
CLIsnoozestack signals streams the hosted feed; signals tail/query/clear work the local dev store
SDK / HTTPsnoozestack.signals.* in the app, capabilities.signals.emit() in a function

From a function#

A function that declares the signals capability writes with one method — the type is what separates a product event from a log line or a metric:

inside a function
capabilities.signals.emit("event", "note_created", { userId: user.id });
capabilities.signals.emit("metric", "render_ms", { value: elapsed });
capabilities.signals.emit("audit", "note_deleted", { by: user.id, noteId });

Under snoozestack dev these land in the local store (.snoozestack/dev-signals.db) — tail them while you build, without sending development noise to hosted ingestion:

terminal
snoozestack signals tail --type event,log # watches live; Ctrl+C to stop
snoozestack signals query --name note_created
snoozestack signals clear

JavaScript / TypeScript#

Every snoozestack-js client has a signals handle. Events are batched in memory and flushed every 10 seconds, at 20 queued events, and when the page is hidden — track() never blocks and never throws.

bash
const snoozestack = createClient(SNOOZESTACK_URL, SNOOZESTACK_ANON_KEY);
snoozestack.signals.track('page_view'); // anonymous device id
snoozestack.signals.identify(user.id); // tie events to a user
snoozestack.signals.track('signup', { plan: 'pro' });
snoozestack.signals.reset(); // on sign-out
await snoozestack.signals.flush(); // force-send (tests, CLIs)
// Standalone (no database/auth client needed):
import { SnoozestackSignals } from 'snoozestack-js';
const signals = new SnoozestackSignals(SNOOZESTACK_URL, SNOOZESTACK_ANON_KEY);

iOS / Swift#

The SnoozestackSignals Swift package (iOS, macOS, tvOS, watchOS) queues events on disk — they survive app kills — and flushes on a timer, at batch size, and when the app is backgrounded. It ships in the same snoozestack-swift package (add SnoozestackSignals instead of, or alongside, Snoozestack).

Swift
SnoozestackSignals.configure(url: "https://<ref>.snoozestack.com", apiKey: ANON_KEY)
SnoozestackSignals.shared.identify(user.id)
SnoozestackSignals.shared.track("purchase", properties: ["sku": "pro", "price": 9.99])

HTTP API (/api/signals/v1/events)#

The SDKs are thin wrappers over one endpoint, so any HTTP client can send events:

signals — /api/signals/v1/events
curl -X POST "https://<ref>.snoozestack.com/api/signals/v1/events" \
-H "apikey: $ANON_KEY" -H "Content-Type: application/json" \
-d '{"events": [
{"name": "signup", "distinct_id": "u-42", "props": {"plan": "pro"},
"ts": "2026-07-06T12:00:00Z", "insert_id": "b3f1c1e2-…"}
]}'
# → {"ok": true, "ingested": 1}
FieldRequiredMeaning
nameyesEvent name (≤ 200 chars), e.g. page_view, signup
distinct_idnoWho did it — a user id or device id; powers the unique-user counts
propsnoJSON object of properties (≤ 8 KB serialized)
tsnoWhen it happened (ISO 8601 or epoch ms); defaults to arrival time. Clamped to the last 7 days / next 5 minutes
insert_idnoIdempotency key — a retried batch is never double-counted

Up to 100 events per request. ts is clamped into a window around now — no earlier than 7 days ago, no later than 5 minutes ahead — so events queued on a device with a wrong clock still land somewhere sane, but historical backfill is not possible: importing a year of events from another tool would file them all under the 7-day floor. The key can come as an apikey header, Authorization: Bearer, or an ?apikey= query parameter (for navigator.sendBeacon). Raw events are browsable on the Events page and readable in bulk through the export API below, and the aggregates are available to scripts via the management API.

Retention and ingestion quota#

Signals are kept for a per-project retention window — 90 days by default — after which a background job deletes them; ask us to raise it if you need longer history. Ingestion is also subject to a per-project rolling 24-hour volume quota (1,000,000 signals/day by default). A batch that would push the project over quota is rejected outright with 429, never partially ingested or silently dropped:

429 — over quota
curl -i -X POST "https://<ref>.snoozestack.com/api/signals/v1/events" \
-H "apikey: $ANON_KEY" -H "Content-Type: application/json" \
-d '{"events": [...]}'
# HTTP/1.1 429 Too Many Requests
# {"error": "Signals ingestion quota exceeded for this project (resets on a rolling 24h window). Contact support for a higher limit."}

Both numbers are per-project account limits, same as your project/table/storage limits — contact us to raise either one.

Export API (/signals/v1/query)#

The ingest endpoint above is write-only — a key can send signals but never read them back through it. For building an external dashboard, polling for new signals, or a one-time bulk export, use the export endpoint instead: same public key-based auth as ingest, butGET and read-only.

query — /api/signals/v1/query
curl "https://<ref>.snoozestack.com/api/signals/v1/query?type=event&since=2026-08-01T00:00:00Z" \
-H "apikey: $ANON_KEY"
# → application/x-ndjson, newest first, one JSON object per line:
# {"id":"9214","type":"event","name":"signup","distinct_id":"u-42","ts":"2026-08-22T18:04:11.000Z","props":{"plan":"pro"}}
# {"id":"9213","type":"event","name":"page_view","distinct_id":"u-42","ts":"2026-08-22T18:03:58.000Z","props":{}}
ParamDefaultMeaning
typeeventOne of event / log / metric / trace / audit
sincenow − 24hISO 8601 or epoch ms — start of the window (inclusive)
untilnowISO 8601 or epoch ms — end of the window (exclusive)
limit1000Rows per page, max 5000
cursorContinuation token from a prior response's X-Next-Cursor header

Each line has the same field names the ingest endpoint accepts (id, type, name, distinct_id, ts, props), so a consumer never has to reverse-engineer an internal shape. If a page doesn't reach the end of the requested window, the response carries an X-Next-Cursor header — pass it back as ?cursor= to fetch the next page; a response with no such header means you've reached since. The key can come as an apikey header, Authorization: Bearer, or an ?apikey= query parameter, same as ingest.

Identity — who a person is#

Until you call identify(), events carry an anonymous id ($anon-…) that the SDK mints once and stores — in localStorage on the web, UserDefaults on iOS — so a returning visitor is the same person, not a new one. There is no fingerprinting: nothing is derived from the device, IP, or browser characteristics.

When identify() runs, the SDK reports both ids and snoozestack records that they are the same person. Everything the visitor did before signing up stays attached to their account, so a funnel can span sign-up and unique-user counts stop counting one human twice. The id is persisted too, so a page reload keeps them identified — call identify() on every load, it's a no-op when nothing changed.

bash
snoozestack.signals.track('landing_view'); // $anon-3f2a (anonymous)
snoozestack.signals.identify('u-42'); // both ids are now one person
snoozestack.signals.track('signup'); // u-42
snoozestack.signals.alias('legacy-id-9'); // link an id you minted elsewhere
snoozestack.signals.reset(); // sign-out: start a fresh person
RuleWhy
Two account ids never mergeA shared laptop or kiosk would otherwise chain everyone who signed in on it into one person. Applies transitively, so linking through a shared device is refused too
A person stops merging past 20 idsAnything larger is a shared device, not a person. The merge is refused rather than allowed to swallow the project
reset() ends device continuitySign-out means the next person on this browser starts clean instead of inheriting the last one's identity
Reserved $-prefixed events$identify, $alias, and $reset are identity plumbing. They are stored (visible on Events) but never counted in charts, the builder, or the Users list

Resolution applies at query time, so the Users page lists people rather than ids — labelled with the account id, and badged with how many ids merged into it. Raw signal rows are never rewritten; the identity graph lives alongside them. Identity is recorded from the moment you deploy this — links that were never captured can't be reconstructed, so history from before stays as it was.

Anonymous identity that survives storage clearing#

Browsers evict script-writable storage aggressively — Safari caps it at seven days — so an anonymous visitor can come back looking brand new. Where the project API shares your site's domain, snoozestack also sets a first-party sb_did cookie and uses it to reconnect a cleared visitor to who they were. It is HttpOnly, holds nothing but an opaque id, and no code of yours has to touch it.

SetupAnonymous identity survives
Site hosted on snoozestack (<ref>.snoozestack.com)Fully — the API is same-origin, so the cookie is first-party
App and API on the same origin via the relative /api baseFully — the API is same-origin however the site is reached
Default <ref>.snoozestack.com API, app on another domainUntil storage is cleared — the cookie is third-party there and browsers drop it, so identity falls back to localStorage alone

There is nothing to configure for any of these. The one that costs identity is an app calling the project across origins; serving the app from snoozestack — on the project URL or on a custom domain — keeps the call same-origin and the cookie first-party.