Skip to main content

Store Referral Attribution

How store-specific referral tracking works across auth, portal, and analytics.

Overview

When a user signs up via a store partner’s referral link (?ref=store-{slug}-{hex}), the system:
  1. Persists the referral source on User.referral_source (generic referral flow)
  2. Detects the store- prefix and records a StoreReferral event (store-specific)
  3. Fires a STORE_REFERRAL_SIGNUP PostHog event for analytics
  4. Shows store-level metrics in the partner portal (or via Integration API for external apps)

Data Model

Storemodels/syntropy/store.py

One per partner. Created via Shopify app install (future) or seed data (dev).
FieldTypeNotes
owner_idint FK → user.idThe partner user who owns this store
slugstr uniqueURL-safe store identifier
referral_codestr uniqueFormat: store-{slug[:12]}-{4hex}
categoryStoreCategorysupplement_shop, health_food, online_wellness, etc.
statusStoreStatuspending → active → suspended/deactivated

StoreReferralmodels/syntropy/store.py

One row per referral event (click or signup).
FieldTypeNotes
store_idint FK → store.idWhich store’s link was used
referred_user_idint | None FK → user.idNull until user signs up
referral_codestrDenormalized for query performance
statusstrclickedsigned_upactive
utm_paramsdict (JSON)Preserved from the referral URL

Attribution Pipeline

When a visitor lands with ?ref=store-acme-a1b2, the referral code is stored in a browser cookie. This happens in the landing page JS — no state involvement.

Step 2: Auth Callback — states/shared/auth/_processes.py

handle_referral_cookie() fires after Clerk sign-up completes:
# Generic referral — always runs
update_user_referral_source_sync(self.user.id, referral_source)
capture_event(str(self.user.id), REFERRAL_SIGNUP_COMPLETED, {...})

# Store-specific — only for store- prefixed codes
if referral_source.startswith("store-"):
    record_referral_signup_sync(referral_source, self.user.id)
    capture_event(str(self.user.id), STORE_REFERRAL_SIGNUP, {...})
The store attribution block is wrapped in a nested try/except so that a failure in store tracking never breaks the main signup flow.

Step 3: StoreReferral Record — functions/db_utils/store_referral.py

record_referral_signup_sync(referral_code, user_id):
  1. Looks up Store by referral_code
  2. If an unmatched clicked row exists → updates it with user_id and status=signed_up
  3. Otherwise → creates a new StoreReferral with status=signed_up

DB Utility Functions

All in functions/db_utils/store_referral.py. All are synchronous (use rx.session()).
FunctionPurposeReturns
record_referral_click_sync(referral_code, source_url, utm_params)Record anonymous link clickbool
record_referral_signup_sync(referral_code, user_id)Record user signup, link to click if existsbool
get_referrals_for_store_sync(store_id, limit=50)Recent referral events for a storelist[dict]
get_store_referral_stats_sync(store_id)Aggregate statsdict with keys below

Stats dict shape

{
    "total_clicks": int,
    "total_signups": int,
    "conversion_rate": float,      # percentage, e.g. 12.5
    "recent_signups_30d": int,
}

Consuming Referral Data

Current: Partner Portal (on hold)

Note: In-app partner onboarding is disabled in favor of Shopify app integration (PRD-17). The partner portal remains present for assessment but is config-gated via features.partner_portal. Store creation will be handled by the Shopify app install flow, not an in-app registration form.
PartnerPortalState (states/partner/portal.py) resolves referral data on page load:
Has Store entity?
  YES → use Store.referral_code + get_store_referral_stats_sync()
  NO  → fallback to User.referral_code + get_referral_stats_sync()

Computed vars for components

VarTypeDescription
store_datadictFull Store record (empty dict if no store)
has_storeboolWhether this partner has a Store entity
referral_codestrThe active referral code (store or user)
referral_linkstrFull URL: https://syntropyhealth.bio/?ref={code}
referral_statsdictStats dict (see shape above)
total_signupsintShortcut for referral_stats["total_signups"]
recent_signupsintShortcut for referral_stats["recent_signups_30d"]
referred_userslist[dict]List of referred user records

Future: Integration API (PRD-17 Phase 6)

When the Integration API is built, the same DB utils power the outbound endpoints:
GET /api/v1/analytics/summary          → get_store_referral_stats_sync()
GET /api/v1/analytics/referrals/{id}   → get_referrals_for_store_sync()
The external Shopify app will consume these endpoints to display referral metrics in the partner’s Shopify admin — no in-app portal needed.

Component example (portal, when enabled)

import reflex as rx
from syntropy_journals.app.states.partner.portal import PartnerPortalState


def referral_stats_card():
    return rx.cond(
        PartnerPortalState.has_store,
        rx.el.div(
            rx.el.p(f"Total signups: ", PartnerPortalState.total_signups),
            rx.el.p(f"Last 30 days: ", PartnerPortalState.recent_signups),
        ),
        rx.el.div(
            rx.el.p("Referral stats unavailable — register your store for tracking."),
        ),
    )

PostHog Events

EventFired WhenProperties
REFERRAL_SIGNUP_COMPLETEDAny referral signup (generic)referral_source
STORE_REFERRAL_SIGNUPStore-prefixed referral signupreferral_code
STORE_REFERRAL_LINK_CLICKEDStore referral link clicked(for future landing page use)
STORE_REGISTEREDNew store created(for registration flow)
AFFILIATE_REFERRAL_LINK_COPIEDPartner copies referral link in portalreferral_code
Events are defined in utils/event_taxonomy.py and fired via utils/analytics.capture_event().

Referral Code Convention

Store referral codes follow the pattern: store-{slug[:12]}-{4hex} Examples:
  • store-acme-supps-a1b2
  • store-vitaminwor-f3e4
The store- prefix is the discriminator. The existing validate_referral() in utils/referral.py accepts ^[a-zA-Z0-9_\-]+$ (max 64 chars), so store codes pass validation without changes.