Data model
Events
The atomic unit of every Sankofa product. Naming conventions, properties, default enrichment, allow/deny lists, and how events flow into ClickHouse.
An event is a named action with optional structured properties. Sankofa builds funnels, cohorts, retention charts, replays, surveys, and experiments by querying events — so the way you model them determines what you can analyse later. Once you ship checkout_started in v1, you can never rename it without breaking every downstream view.
This page covers naming conventions, property modelling, default enrichment, what gets indexed, and what happens to an event after track returns.
Anatomy of an event
A single event row in ClickHouse looks like this (simplified):
{
"event_name": "checkout_started",
"distinct_id": "user_123",
"anon_id": "anon_6b87f2",
"session_id": "sess_abc123",
"timestamp": "2026-05-09T14:32:01.482Z",
"ingested_at": "2026-05-09T14:32:01.612Z",
"environment": "live",
"properties": {
"cart_value": 49.99,
"item_count": 3,
"currency": "USD"
},
"default_properties": {
"$os": "ios",
"$os_version": "17.4",
"$device_model": "iPhone 15 Pro",
"$app_version": "2.4.1",
"$country": "GH",
"$city": "Accra",
"$timezone": "Africa/Accra"
},
"lib": "@sankofa/react-native",
"lib_version": "1.0.4"
}event_name, distinct_id, and timestamp are required; everything else is filled in by the SDK or the engine.
Naming conventions
Good event names are stable, descriptive, and predictable. Three rules go a long way:
Use snake_case, present tense, verb-noun
signup_completed✓checkoutStarted✗Checkout started✗Past tense is the convention because every event represents a thing that just happened. Verb-noun is grep-friendly — when you're hunting for "everything checkout-related,"
^checkout_matches.Be specific enough to be useful, vague enough to last
payment_failed✓payment_failed_visa✗ (use acard_brandproperty)event✗ (too vague)The card brand is a property dimension that varies; the event itself is "a payment failed."
One event per discrete action
signup_completed✓signup_started_then_completed✗Two distinct moments in a user flow are two events. They get joined into a funnel server-side; you don't need to compress them into one name.
A few well-chosen events from real Sankofa customers:
signup_completed— the moment account creation succeeds.checkout_started,checkout_completed,checkout_abandoned— the funnel triplet.payment_failed— withfailure_code,card_brand,amountproperties.invite_accepted— workspace + role attached as properties.api_request_failed— server-side;endpoint,status_code,latency_msproperties.feature_used— when something fine-grained doesn't yet warrant its own name.
Properties: what to put on the event
Use properties for the dimensions that vary. Examples:
plan—"free","pro","growth","enterprise"currency—"USD","GHS","EUR"screen— the screen the action happened onsource— referrer, campaign, traffic originfailure_code— for error eventsexperiment_variant— auto-attached if you use Switch's exposure tracking; manually set otherwise
Properties accept strings, numbers, booleans, JSON arrays, and JSON objects. Dates are stringified to ISO 8601. BigInt is stringified.
Default properties
Every SDK adds default properties to every event for free — geo, OS, device, app metadata. The engine promotes the highest-value defaults into indexed columns so you can break down or filter by them at query time without paying a JSON-decode cost.
$countrystring$regionstring$citystring$timezonestring$osstring$os_versionstring$device_modelstring$browserstring$app_versionstring$session_idstringSee Default properties for the complete list and per-platform behaviour.
Allow and deny lists
Configure per-project allow / deny lists at Settings → Data → Properties:
- Allow list — only properties named here are persisted. Everything else is dropped at ingest. Use this when you have a strict PII policy.
- Deny list — properties named here are dropped, everything else is persisted. Use this for quick redaction of fields you've identified as PII (
email,phone_number,ssn).
Allow / deny apply to property names only — not values. To redact specific values (e.g. mask all-but-last-four-digits), do it at the SDK call site.
Event ingestion path
After your SDK calls track:
The SDK queues
Events go onto the SDK's offline-first queue. On web that's
IndexedDB; on mobile that's SQLite (via GRDB on iOS, the Android SDK's queue table on Android,sqfliteon Flutter). On server SDKs the queue is in-memory.Flush on schedule
The queue flushes on
flushIntervalSecondscadence (default 30s on mobile, 5s on web), on app suspend / background, onflush()call, or on shutdown.Engine ingests
The flush hits
POST /api/v1/batchwith up tobatchSizeevents. The engine validates the API key, fills inenvironment, applies allow / deny rules, runs the GeoIP lookup, and writes a row to ClickHouse.Available in queries
Events are visible in Live events within ~1 second of ingest. Funnels, cohorts, and retention queries pick them up on the next refresh window (typically 30 s on Pro, near-realtime on Enterprise).
See Ingestion model for the full path including backpressure and retry semantics.