Data model
OpenPartner is event-sourced. Three layers are immutable raw inputs; the rest are derived views or ledgers.
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 | Salted hash. We don’t store raw IPs. |
ua | text | User agent. |
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.
Payout
Stripe Connect transfer record. Never updated — refunds and reversals are new rows.
Why this matters
You can’t lose attribution history because the raw data is intact. You can re-run with a different model. You can audit any commission back to its source click. And exports are lossless because the raw layers are everything you need to re-derive.