Phase 1, Data Schema & Sync Map (the build blueprint)
How to read this
Each "entity" below is one table in the database. For each, there's a plain-language purpose, then a field list. The custom system is the single source of truth for everything here; GoHighLevel (GHL) holds its own copy of a few facts and we sync a minimal set both ways (see "The GHL sync map").
Money is stored to the cent, in USD. Dates are stored with a timezone. "Link to X" means the field holds the id of a record in another table (how tables connect).
The big picture
One partner refers people. Some of those referrals become paying clients. Each payment a client makes earns the partner 10%. Those 10% amounts get paid out to the partner, over and over, for the life of the client. In record terms:
partner ─┬─ agreement (their one signed contract)
│
└─ referral ─── (converts) ─── client_payment ─── commission ─┐
(a person (the referred (each payment │
they person becomes the client │
introduced) a client) makes) │
▼
payout ──► Stripe ──► partner's bank
(one payout can bundle
several commissions;
Melanie releases it)
- A partner has exactly one agreement (their signed reciprocal referral contract).
- A partner has many referrals (each person they introduce to Melanie).
- A referral, once it converts into a paying client, has many client_payments (monthly fees, renewals, expansions, every payment that client makes).
- Each client_payment creates one commission (10% owed to the partner). Clawbacks are negative entries in the same commission ledger.
- Commissions get bundled into payouts (the actual Stripe transfers). One payout can cover several commissions, because Melanie releases them on her own schedule.
Phase 1 is Direction A only: partners refer clients IN, Accelerate CMO pays 10% OUT. The contract's reverse direction (Accelerate CMO refers out and collects 10% from a partner) is deliberately not modeled here.
Source-of-truth split (recap)
- Custom database (this schema) owns: partners, agreements, referrals/attribution, client payments, commissions, payouts, everything the partner sees and all money logic.
- GHL owns: the sales/referral pipeline, the underlying contact records, and nurture email campaigns.
- QuickBooks Online owns: client invoicing and payment collection, it is the source of the "invoice paid / payment received" signal (money IN).
- Stripe: partner payouts (money OUT).
- Synced (minimal, one owner per fact): see "The sync map" at the end.
Entity 1, partners
Purpose: the referral partner (the person the contract calls "Client"). Their identity, current status, portal login, and how they get paid.
| Field | Type | What it is |
|---|---|---|
id |
id | Unique partner id (internal). |
status |
enum | Lifecycle: invited → agreement_sent → active → paused → ended. Drives portal access and whether payouts run. |
first_name |
text | From GHL contact on handoff; editable by partner at intake. |
last_name |
text | " |
email |
text | Login identity and payout/notice email. Unique. |
phone |
text | " |
company_name |
text | The partner's business (captured at signing). |
full_address |
text | Captured at signing (contract requires it). |
relationship_type |
enum (nullable) | Business Owner/Leader, Service Provider, PE Firm, other, her existing segmentation, optional. |
ghl_contact_id |
text | Link to the matching GHL contact (the sync key). |
stripe_connect_account_id |
text (nullable) | Their Stripe connected account (how money reaches their bank). Bank details live in Stripe, never here. |
stripe_onboarding_status |
enum | not_started / pending / complete. Payouts run only when complete. Drives the portal's persistent "complete payout setup" prompt and the GHL Payout Setup Incomplete / Payout Setup Complete tag. |
payout_method |
enum | stripe (default). manual_check fallback for a partner who won't use Stripe (recorded, paid outside the system). |
tax_form_status |
enum | W-9 / 1099 tracking: not_collected / collected. Stripe Connect collects the W-9 during payout onboarding and can e-file 1099s, we only prompt the partner to finish Stripe's steps (confirm 1099-NEC vs -K treatment with a tax pro). |
auth_method |
enum | password or magic_link (portal login). |
password_hash |
text (nullable) | If password auth. Never store plaintext. |
last_login_at |
datetime (nullable) | " |
invited_at |
datetime | When the "Made a Move" handoff fired. |
signed_at |
datetime (nullable) | When their agreement completed. |
created_at / updated_at |
datetime | Standard. |
notes |
text | Free notes (Melanie's admin use). |
Entity 2, agreements
Purpose: the one signed reciprocal referral agreement per partner, signed through SignWell. Holds the legal record and the exact values captured at signing.
| Field | Type | What it is |
|---|---|---|
id |
id | Unique agreement id. |
partner_id |
link → partners | The partner who signed. One agreement per partner. |
signwell_document_id |
text | The SignWell document/envelope id. Signing opens in a new browser tab; SignWell returns the partner to the app to set their password. |
status |
enum | sent → viewed → signed → completed. Also voided/declined. |
commission_rate |
number | Defaults to 0.10. Stored per-agreement so a future non-standard rate doesn't break the model; today always 10%. |
referral_window_days |
number | Defaults to 180. Stored per-agreement for the same reason. |
signed_pdf_url |
text | Stored copy of the completed PDF (the partner sees this in their portal). |
contract_version |
text | Which template version they signed (GHL template is v2 as of 2026-07-14). |
client_full_name |
text | Signer field, as signed (snapshot). |
client_company_name |
text | Signer field. |
client_full_address |
text | Signer field. |
client_phone |
text | Signer field. |
client_email |
text | Signer field. |
company_signer_name |
text | Who signed for Accelerate CMO (the authorized officer, e.g. Melanie). |
executed_date |
date | The "Executed this ___" date on the contract. |
sent_at / signed_at / completed_at |
datetime | Timestamps of the signing lifecycle. |
reminder_count |
number | Signing reminders sent so far. Cadence: day 2, day 7. |
last_reminder_at |
datetime (nullable) | When the last reminder went out (Melanie is notified each time). |
escalated_at |
datetime (nullable) | Day-8 escalation to Melanie to follow up personally. |
Entity 3, referrals
Purpose: a person a partner introduces to Accelerate CMO, tracked from introduction through (hopefully) becoming a paying client. This is where attribution and the 180-day window live. When a referral converts, this same record carries the client relationship; each payment hangs off it.
| Field | Type | What it is |
|---|---|---|
id |
id | Unique referral id. |
partner_id |
link → partners | Who made the introduction (the attribution). |
referred_name |
text | The prospective client's name. |
referred_email |
text (nullable) | Minimal contact info. |
referred_company |
text (nullable) | " |
ghl_contact_id |
text (nullable) | The lead's GHL contact, if they live in GHL. |
ghl_opportunity_id |
text (nullable) | The lead's GHL Sales Pipeline opportunity, if any. |
status |
enum | submitted → acknowledged → then either converted (became a client), rejected, or expired. |
acknowledged_date |
date (nullable) | When Melanie acknowledged the referral in writing. This starts the 180-day clock (per contract Section 3). |
window_expires_on |
date (derived) | acknowledged_date + referral_window_days (180). |
window_override |
enum + date (nullable) | Melanie's manual control: none (default, strict), extended_to <date>, or waived (always eligible). Implements the strict-by-default-with-override decision. |
eligibility |
enum (derived) | eligible (converted within window, or override), ineligible_expired (no conversion by the window and no override). Drives whether commissions can accrue. |
is_business_opportunity |
yes/no | Whether it meets the Section 5 criteria (see below). Set when it converts. |
qualifier_flags |
checklist | The Section 5 conditions: full initial payment received; not a pre-existing customer; not in active negotiation before the referral; not referred by another party; lawful. Recorded for the record and disputes. |
rejection_reason |
text (nullable) | If rejected (existing customer, in active negotiation, didn't meet requirements). |
client_start_date |
date (nullable) | When the referred client signed with Accelerate CMO (their engagement start). Set on conversion. |
client_status |
enum | prospect (not yet a client), active_client, ended. |
client_ended_date |
date (nullable) | When the client relationship ended (commissions stop accruing after this). |
lifetime_value_cached |
money | Running sum of this referral's client payments (for fast portal display; the payments table is the truth). |
created_at / updated_at |
datetime | Standard. |
Note on renewals/expansions: the contract pays 10% of Lifetime Value including renewals, so a
renewal or expanded scope is simply another client_payment on the same referral, not a new referral.
(Edge case to confirm: if a partner separately re-refers the same company for a distinct new
engagement later, do we treat it as a new referral? Flagged in open questions.)
Entity 4, client_payments
Purpose: each payment a converted referral (the client) actually makes to Accelerate CMO. This is the event that earns the partner their 10%. It exists whether the client paid digitally or by check.
| Field | Type | What it is |
|---|---|---|
id |
id | Unique payment id. |
referral_id |
link → referrals | Which client relationship this payment belongs to. |
partner_id |
link → partners | Denormalized for fast lookups (always matches the referral's partner). |
gross_amount |
money | The gross revenue of this payment (contract defines gross = total contract value before any expenses/fees/costs). The 10% is calculated on this. |
payment_date |
date | When the client paid. |
received_status |
enum | expected (invoice out, not yet paid) → received (confirmed). Only received earns a commission. |
received_via |
enum | qbo_auto (client paid electronically; QBO recorded it itself) or qbo_manual_entry (Melanie recorded the payment in QBO by hand, checks and anything QBO can't auto-detect). Both arrive via the same QBO event; the admin screen never marks a payment received. |
confirmed_by |
text (nullable) | Who recorded the payment in QBO, if manually entered (from the QBO event data). |
received_at |
datetime (nullable) | When it was marked received (the moment the commission becomes payable). |
source_reference |
text (nullable) | QBO invoice id / QBO payment id / check number, for reconciliation. |
is_initial_payment |
yes/no | Marks the client's first payment (Section 5 requires the "full agreed-upon initial payment" for the referral to qualify). |
created_at |
datetime | Standard. |
Entity 5, commissions (the commission + clawback ledger)
Purpose: the running ledger of what the partner is owed. One positive entry per received client payment (10% of its gross). Clawbacks are negative entries. Grouping entries into a payout is how they get paid.
| Field | Type | What it is |
|---|---|---|
id |
id | Unique ledger entry id. |
partner_id |
link → partners | Whose ledger. |
referral_id |
link → referrals | Which client relationship it came from. |
client_payment_id |
link → client_payments (nullable) | The payment that generated it (null for a standalone clawback adjustment). |
type |
enum | commission (positive) or clawback (negative). |
amount |
money | Signed amount: +10% of the payment's gross for a commission, negative for a clawback. |
rate |
number | The rate applied (0.10), copied from the agreement at time of accrual. |
status |
enum | payable (owed, not yet released) → released (in a payout, sending) → paid. Also on_hold and void. |
payout_id |
link → payouts (nullable) | Which payout paid it. Null while still payable. |
clawback_reason |
text (nullable) | For clawbacks: refund or client cancellation within 60 days of client start (contract Section 3A). |
accrued_at |
datetime | When it hit the ledger. |
Clawback rule (Section 3A): if a client is refunded or the engagement is terminated within 60 days
of that client's start date, Accelerate CMO may deduct the corresponding referral fee from any future
payout. In data terms: create a negative clawback entry; it nets against the partner's next payout.
If the partner has no upcoming payouts to net against, the balance goes negative and carries forward
(flagged in open questions for how Melanie wants to handle a partner who's already been paid).
Entity 6, payouts
Purpose: an actual transfer of money to a partner via Stripe. Bundles one or more payable commission entries. Melanie releases these on her own schedule (or sets auto-release); the contract allows 60 days from each client payment.
| Field | Type | What it is |
|---|---|---|
id |
id | Unique payout id. |
partner_id |
link → partners | Who's being paid. |
amount |
money | Net sum of the commission entries included (commissions minus any clawbacks). |
status |
enum | queued (payable, awaiting release) → released → processing (Stripe) → paid, or failed. |
release_mode |
enum | manual (Melanie clicked release) or auto (she set payouts to auto-release on payment confirmation). |
released_by |
text (nullable) | Who released it, if manual. |
stripe_transfer_id |
text (nullable) | The Stripe payout/transfer id. |
fee_statement |
document/JSON | The Section 6 statement: the list of commissions included and how each 10% was calculated. Generated at payout time; the partner and Melanie can both see it. |
included_commission_ids |
links → commissions | Which ledger entries this payout settled. |
released_at / paid_at / failed_at |
datetime | Lifecycle timestamps. |
failure_reason |
text (nullable) | If Stripe rejected it. |
Entity 7, event_log
Purpose: an append-only record of everything that happened (webhooks received, statuses changed, syncs to/from GHL, payouts released). Needed because this is a money system, and the contract even references "real-time tracking of referral activity." Also the backbone for debugging syncs.
| Field | Type | What it is |
|---|---|---|
id |
id | Unique event id. |
entity_type / entity_id |
text | What the event is about (partner, referral, payout, etc.). |
event |
text | e.g. partner.invited, agreement.completed, payment.received, commission.accrued, payout.released, ghl.sync_out, ghl.webhook_in. |
source |
enum | ghl_webhook, signwell_webhook, stripe_webhook, admin_action, system. |
payload |
JSON | The relevant details / raw webhook body. |
created_at |
datetime | When it happened. |
The GHL sync map (exact)
Only these facts cross the boundary. One owner per fact; no fragile full mirroring.
Into the custom app:
1. [GHL] Partner reaches "Made a Move (attended workshop / Made Intro)" → GHL workflow fires an
outbound webhook → custom app creates the partners record (pre-filled from GHL standard fields)
and starts onboarding (sends the SignWell agreement, which opens in a new tab). The single wire that
kicks off the partner experience. Trigger = entering the stage; manual jumps count.
1b. [GHL] Melanie marks a lead's "Referring Partner" field → GHL workflow
webhook → custom app matches the value to an active partner and creates + acknowledges the
referrals record (the field-set IS the acknowledgment; the 180-day clock starts that day).
No manual referral entry exists in the admin screen.
2. [QBO] A client invoice is paid → QuickBooks Online "invoice paid" webhook → custom app records a
client_payments row as received for the matching referral, making the commission payable. Checks
QBO can't auto-detect are recorded by Melanie in QBO (never in the admin screen), which fires the
same webhook, one door for all money in.
3. [GHL] A referred deal closes (won/lost + close date) → custom app converts or expires the
referral and records whether the close fell inside or outside the 180-day window.
Out to GHL:
1. Partner signs + becomes active → tag the GHL contact Active Referral Partner; the card moves
to Signed Referral Agreement at contract completion, then on to Nurturing at portal activation
(enrolls the nurture campaign). Stage ids are wired from her live pipeline.
2. Partner status change (paused / ended) → update the tag.
3. Payout-setup status → tag Payout Setup Incomplete / Payout Setup Complete as the partner's
Stripe onboarding changes.
QBO ↔ GHL: a paid QBO invoice also marks the corresponding GHL deal paid, so Melanie's sales view stays current.
GHL custom fields needed: GHL has zero custom fields today. The build creates five: Referring
Partner (a DROPDOWN kept in sync with active partners, setting it is also what creates the referral,
per rev 4), Referral Acknowledged Date, Referral Window Expires, Referral Window Outcome, and
Referral Last Payment Date. Pending the custom-field edit scope on the API token (one-time setup).
Not synced: the referral / commission / payout ledger. It lives only in the custom database (GHL's affiliate manager is deliberately not used, see the architecture memory and map).
GHL contact fields available for pre-fill
Confirmed live 2026-07-17: her GHL has zero custom fields, so the handoff pre-fills a partner
record from GHL's standard contact fields only: firstName, lastName, email, phone,
companyName, address1 / city / state / postalCode / country, source, tags,
assignedTo, website. Everything partner-specific (payout details, tax status, signed-contract
values) is collected by the custom tool at intake/signing, not read from GHL.
Key contract rules encoded (traceability)
| Rule | Where it lives in this schema |
|---|---|
| 10% of gross, per payment | commissions.amount = client_payments.gross_amount × agreements.commission_rate |
| Recurring for life of client, incl. renewals | Many client_payments per referral; commissions accrue on each |
| Gross = total contract value before expenses | client_payments.gross_amount is defined as gross |
| 180-day window, strict by default | referrals.acknowledged_date + referral_window_days, eligibility derived |
| ...with Melanie's override | referrals.window_override (extended_to / waived) |
| 60-day payment window | Payout can be released any time within 60 days of client_payments.received_at; not enforced as a block, surfaced as a due-date in admin |
| Clawback within 60 days of client start | Negative commissions entry (type = clawback), nets against next payout |
| Fee statement per payment/payout (Section 6) | payouts.fee_statement |
| Business Opportunity qualification (Section 5) | referrals.is_business_opportunity + qualifier_flags |
| Payout to partner's bank | partners.stripe_connect_account_id, payouts.stripe_transfer_id |