Data model
OpenPartner is event-sourced. Three layers are immutable raw inputs; the rest are derived views or ledgers.
Immutable means rows aren’t edited in place. They’re still deleted when a brand closes its account (after a 30-day grace period), and a person’s identities, events and clicks can be deleted on request.
Raw tables (immutable)
Click
Every click on a partner link.
| Field | Type | Notes |
|---|---|---|
clickId | ULID | Primary key. |
partnerId | ULID | Which partner gets credit. |
campaign | string? | Optional grouping. |
landingUrl | text | Where the user landed. |
referer | text? | HTTP referrer. |
ipHash | text | Truncated SHA-256 of the visitor’s IP. Raw IPs aren’t stored in the database, but the hash isn’t salted, so treat it as personal data. |
ua | text | User agent. |
utm* | text? | utm_source, utm_medium, utm_campaign, utm_term, utm_content from the link. |
country | text? | Visitor’s country, when the edge provides it. |
fraudFlag | text? | velocity, manual or revoked when a click is flagged. |
createdAt | timestamptz | Click time. |
Event
Conversion events from the SDK or server-side.
| Field | Type | Notes |
|---|---|---|
eventId | ULID | Primary key. |
userId | string | Customer’s user identifier. |
type | string | Event name. |
amount / currency | int / string | Money in minor units. |
metadata | jsonb | Free-form. |
occurredAt | timestamptz | Event time. |
Derived (re-runnable from raw)
Identity
Bridge between a userId and the clickId they came from. Written when op.identify() is
called and there’s an active click cookie.
Attribution
Result of running the attribution model over raw data. References the event and the click(s) credited. Re-derivable.
Ledger (immutable, audit-grade)
Commission
Computed from Attribution + program rules. Never updated in place — corrections are written as new rows referencing the original.
| Field | Type | Notes |
|---|---|---|
sourceCommissionId | ULID? | Set on a recruiter commission (two-tier): the recruit’s commission it’s a share of. Null on every other commission. |
A recruiter commission sits on the recruit’s Attribution, with partnerId the recruiter. So a
commission’s partner isn’t always its attribution’s partner.
Payout
Stripe Connect transfer record. Never updated — refunds and reversals are new rows.
Recruiting (two-tier commissions)
Who recruited whom, and what a program pays for it, live on the Partner and Program rows. Recruitment isn’t derived from clicks or events, so exports carry these columns as they are.
| Table | Field | Type | Notes |
|---|---|---|---|
Partner | recruitedByPartnerId | ULID? | The partner who recruited this one. Null when nobody did. |
Partner | recruitedAt | timestamptz? | When the recruitment started. Set exactly when recruitedByPartnerId is. Only events from then on pay the recruiter. |
Partner | recruitCode | string? | The partner’s own code for their join-page link (?ref=). Unique per account. |
Program | recruiterCommissionPercent | decimal? | The recruiter’s share, as a percent of the recruit’s commission (10.00). Above 0, at most 50. Null = off, the default. |
Program | recruiterCommissionMonths | int? | How many months after recruitedAt the recruiter keeps earning. 1–120; null = no limit. |
Commission | sourceCommissionId | ULID? | See Commission. |
A partner can’t be their own recruiter, and two partners can’t recruit each other.
Why this matters
You can’t lose attribution history because the raw data is intact. You can audit any commission back to its source click. And exports carry both layers: an import restores attributions and commissions as they were stored, and the raw clicks, identities and events travel with them, so another system can re-derive attribution from the raw layers.