Skip to content

Concepts

Cowliss is a customer data layer with one loop: event in → durable journey per user → email/webhook out. Everything else exists to make that loop trustworthy in a multi-tenant product.

your apps ──identify/track──▶ ingestion API (/v1, API-key auth)
┌────────────────────┼───────────────────────┐
▼ ▼ ▼
Postgres ClickHouse segment engine
profiles, traits, append-only event recompute on every
identifiers, consent, stream (per app, write; transitions emit
catalog, segments, with historical system.segment_entered /
releases, journeys … timestamps) system.segment_exited
│ │
▼ ▼
journey trigger ──▶ journey executions
(raw events and one durable execution per
segment entries) profile per journey, running
the deployed release
send activities (gates checked
at execution time) ──▶ email via
SES / SNS feedback, signed
webhooks
  • Apps and sources: an app is one product you run, pinned to one environment at creation, and the attribution unit: journey triggers and segment predicates filter by its appId. A source is one inbound pipe into an app, either the api source the app is created with or a provider such as Clerk, and every identify/track names the sourceId it arrives through. The server derives the app and stamps both. One org cleanly holds a whole portfolio. See Apps and sources.
  • Environments: development and production are a hard partition inside the org. The environment lives on the app, ingestion stamps it on every write, and profiles, events, settings, and delivery all stay on their side. See Environments.
  • Profiles and events: identify upserts a profile (Postgres), track appends an event (ClickHouse). Events are immutable; profiles merge RFC 7386-style.
  • Identity: Cowliss generates every profile id (usr_…); every call names who it is about with an identifiers map (userId, anonymousId, clerkId, email). Resolution creates a profile, claims new identifiers for a known one, or merges the profiles when a call names two. There is no alias call. See Identity.
  • Email addresses: an email_addresses ledger per org records every address a profile has held and whether it is verified, by link (Cowliss’s own signed link), imported (a caller asserting it), or provider (an auth provider’s own flag). Taking on an unverified address emits system.email_registered; verifying one emits system.email_verified carrying method. The profile’s emailVerifiedAt and emailVerifiedAddress traits are system-maintained: a caller writing them gets a 422, changing email to a new address clears verification, and changing it back to an address the ledger already verified restores it.
  • Catalog and governance: event/trait names are accept-by-default with a violation queue for the unknown, plus a deny-list and a per-environment ingestionPolicy (permissive/strict) for hard control. Rejection has exactly two sources: denied names (422) and canonical-trait format validations.
  • Segments: flat predicate lists over traits and event history, recomputed synchronously on every write. Membership transitions emit system.segment_entered/system.segment_exited system events, so journeys treat cohorts and raw events uniformly. See Segments.
  • Projects and releases: journeys and email templates are TypeScript in your repo, in a project the cow CLI builds and pushes. A push makes an immutable, numbered release; cow deploy --env makes one release an environment’s current code. Executions stay pinned to the release they started on. See Projects.
  • Journeys: one durable execution per profile per journey per environment, with durable timers for waits and later events delivered as they arrive. Journey code runs in a sandbox with no I/O, so every effect goes through a journaled capability call. See Journeys.
  • Delivery: email directly through AWS SES behind an EmailProvider port, with delivery feedback arriving as SNS HTTPS notifications on a public route; webhooks as Standard-Webhooks-signed JSON POSTs to dashboard-managed destinations. Every attempt is a row in the delivery log: sent, delivered, bounced, complained, failed, or a typed skipped_*.
  • Send gates: marketing sends check, in order: journey enabled, org sending pause, address suppression, org credit, per-user frequency cap, purpose consent, from-domain. Transactional sends skip only consent and the frequency cap.
  • Billing: a monthly free allowance plus a prepaid wallet; one weighted spend over events, emails, webhooks, and journey runs; a hard stop at zero credit. Money is the only gate: there are no plans or feature tiers. See Billing.
  • Compliance: per-purpose consent (emailMarketing, dataProcessing), one-click unsubscribe, one-call erasure across Postgres + ClickHouse + Temporal, JSON export, per-environment event retention via ClickHouse TTL.

The dashboard, the cow CLI, and the MCP server all talk to the same typed REST API, so admin parity is structural: anything you can click, an agent can call. These docs are built to be read by agents too: /llms.txt, per-page markdown, and /openapi.json. See For agents.

Every public contract is versioned and frozen at its version: the /v1 API, the journey guest protocol (major 1, stamped into every release), the envelope and error-code enum, webhook payloads stamped v: 1, and stable CLI/MCP command names. New ideas ship as a new version alongside the old, never as a breaking change.