Sources and event types

See what a source declares before any data arrives, and why each event type has a short name and a stored name.

Records and everything that hangs off them, plus the rules they obey: minting, event behavior, identity resolution, and what the registry enforces.

the recordspersonentityidentity linkn1attributen1identity link1nattribute1nfile1n1nn1contact0..1nn1eventsource / event type1nassignment1nn1membertarget: the company, or one contactscoresegmentcomputed on read
Each record owns its own identity links and attributes. Contact joins a person to a company; an event belongs to one company and is stamped with at most one acting person. A member owns a company (account owner) or one contact (relationship owner); score and segment are computed on read.

1. The record map

The records

  • Person: one human, deduplicated globally by durable keys, LinkedIn URL first, then email. Exists once across every company they ever touch, keeping their history through job changes.
  • Entity: one company, however many tools know it. Internal ULID id; a domain is one identifier on it, never the id itself.

The connections

  • Contact: the person-to-company edge, one per affiliation. Title, department, and that person's activity at that company live on the edge, so the same human can hold different roles at different companies without colliding.
  • Member: your own team: reps, CSMs, account owners. Managed by you over the API and never minted from source data, so a customer's Slack user can never become your teammate.
  • Assignment: the member-to-record edge saying who owns the account or relationship right now. Serves account_owner and account_team as live joins, never as stale copied fields.
  • Identity link: an external id (domain, slack_channel_id, stripe_customer_id) attached to a person or an entity. It is how every incoming item finds its record, and the thing a delete freezes so a re-sync cannot re-create what you removed.

The data

  • Event: one thing that happened, on one entity's timeline, stamped with the acting person when there is one. Append-only and immutable with provenance on every row: the primary data everything else derives from.
  • Source / event type: the contract an event is validated against: fields, types, requiredness, scope. Predefined data models ship for Slack, Stripe, calendar and more; custom ones are declared over the API.
  • Attribute: a static field you set directly on a record, like tier or region. Never written by ingest and never derived, so declared facts stay separate from observed history.
  • File: an uploaded document on an entity, text-extracted, chunked, and embedded, so its content answers semantic search alongside messages.

Computed on read

  • Score: your 0 to 100 definition, compiled live per record on every read. Edit it and the next query already serves the new score. Scores.
  • Segment: a saved live filter over one grain. Membership is computed at read time, never snapshotted, and its changes feed is the loop you poll to act downstream. Segments.

2. Data models

A data model is the schema for one source: the contract every incoming item is validated against. One data model declares:

  • Its event types, each with typed fields, requiredness, and a scope (entity or person).
  • Which fields are promoted, becoming filterable emf__ columns and scoring inputs.
  • The identity link types its items may carry, so its data can find its records.
  • For person-scope events, the contact mapping naming which fields carry the acting person.

Four person-first data models ship enabled on a fresh deployment: job, education, web, signup. A custom one you declare lands in the same tables and is treated identically everywhere; deployments provisioned earlier may also carry previously shipped models such as slack, stripe, calendar, meetings, crm, and email.

One more belongs to ackDB itself: the system data model. Lifecycle events, record created, identity linked and unlinked, member assigned, attribute edited, score changed, are written through it by ackDB, and POST /ingest refuses it, so the audit trail cannot be forged.

The registry defends stored data: additions are free, while removing a field, changing its type, or making an optional field required is refused unless explicitly forced, and GET /schemas/sources/{sourceType}/editability says what is currently allowed. Creating and editing them lives in Build; what an event itself is lives in Events.

3. Attributes

Attributes are the schema for your own declared facts: fields you set directly on records, never written by ingest and never derived. Events are what happened; attributes are what you say is true.

  • Each has a scope: company, person (survives job changes), or contact (one affiliation only).
  • A type (string, number, boolean, date, string_array) and optional allowedValues that validate every write.
  • A description, read by AI agents for semantic understanding.

Standard attributes ship and cannot be edited; custom ones are yours. Defining and setting them lives in Build.

Link types are the schema of identity: which external ids ackDB will accept and match on.

  • Universal types (domain, plus any you register) are valid from any source; a type flagged canMint may create a company, which is how domainless companies get records.
  • Per-source types (slack_channel_id, stripe_customer_id) are declared by a data model and are match-only: they attach data to existing records and can never mint, so a typo cannot create a phantom.
  • A record deletion freezes its identifiers, and a frozen identifier can neither mint nor attach again.

5. How records mint

A company mints two ways: an ingest item with a domain identity link that matches nothing, or an explicit POST /entities with a domain (or a registered mintable anchor type, for domainless companies). The domain is normalized and becomes the domain link; the name defaults to it.

A person mints from durable keys on incoming data: LinkedIn URL first, then email. One entity can hold many links of one type: two Slack channels, one company.

Source-specific ids never mint. A typo in a stripe_customer_id cannot create a phantom company.

6. How events behave

Events have their own page: the anatomy, the per-company timeseries, and examples. The schema-side rules: each event validates against its data model's field types and requiredness, its type is stored source-prefixed (slack_message), and its declared scope decides whether it may carry the acting person.

7. How people resolve

Person-first: durable keys find the human globally, then the contact edge at this company is found or created. A job change ends the old edge and starts a new one; the person and their history stay whole.

Two hard rules:

  • ackDB never fuses two humans on a guess. A durable key owned by another person is a 409 plus a merge candidate, resolved deliberately.
  • ackDB alone decides internal vs external, from the deployment's internal domains and users config. An isExternal flag on pushed data is ignored.

8. Where an item lands

itempiedpiper.cominvoice.paidmessagematchedfirst eventmintedno identity matchunresolved
Checked in order: match an existing entity, mint a new one (domain links only), or park in the unresolved pool.

Nothing matched is not data lost. The item parks in the unresolved pool (GET /unresolved), and POST /entities/resolve promotes it once you know where it belongs.