Skip to content

AppiFire Referral Program (30% Revenue Share)

Status: Implemented (MVP).

Admin-only Referral console at /app/referral (visible only to zainalam1001@gmail.com), subscription commission accrual, and merchant referral code entry on Billing.

Related: Shopify App Pricing · Credits wallet · Promo codes


AreaStatusNotes
Partner / referral codesYesReferralPartner table; unique code
Referral admin consoleYes/app/referral tabs + partner detail page
Shop attributionYesshops.referred_by_partner_id + source/source
Commission ledgerYesReferralCommission rows (pendingapprovedpaid / void)
Program settingsYesReferralProgramSettings singleton row
Merchant code entryYesBilling card “Have a referral code?”
Affiliate / partner portalNoOut of scope for MVP — admin manages partners
Automatic bank payoutsNoMark paid manually; pay outside Shopify (Wise / PayPal / bank)
Credit-pack commissionsNoSubscription revenue only in MVP

RuleValue
Commission rate30% (3000 bps), overridable per partner
Qualifying revenuePaid subscription only ($20/mo → $6/mo commission)
Duration12 months from first paid period, then stop
Hold14 days before pendingapproved
Min payout (settings)$50 (5000 cents) — advisory for manual payouts
CurrencyUSD
Code after first paymentNo (default allowCodeAfterPaid = false)
AttributionOne shop → one partner; merchant code or admin assign

Free plan installs alone do not earn commission.

Example: referred shop pays $20/mo → partner earns $6/mo for up to 12 months while the shop stays paid (max $72 lifetime per referral if they stay paid the full window).


AreaRoute / locationPurpose
Admin console/app/referralTabs: Overview, Partners, Earnings, Attributions, Settings
Partner detail/app/referral/partners/:idProfile, referred shops, mark commissions paid
Merchant code entry/app/billing“Have a referral code?” card
Accrual (confirm)app/routes/app.billing.confirm.jsxAccrue on paid subscription confirm
Accrual / void (webhook)app/routes/webhooks.app_subscriptions.update.jsxAccrue on ACTIVE renew; void pending on cancel
Accrual / void (sync)app/lib/billing-plans.server.jsAccrue on paid sync; void when plan drops to free
Shared helpersapp/lib/referral.shared.jsCode normalize/format, rates, USD formatting
Server logicapp/lib/referral.server.jsAccrue, approve held, void, apply code, assign
Schemaprisma/schema.prismaModels + shop attribution fields
Migrationprisma/migrations/20260713120000_add_referral_tables/Tables + shop columns + settings seed

Admin route files:

  • app/routes/app.referral.jsx
  • app/routes/app.referral_.partners.$id.jsx (URL /app/referral/partners/:id, pathless nesting escape)

Same allowlist as Promo codes: zainalam1001@gmail.com.

PieceImplementation
Allowlist emailPROMO_ADMIN_EMAIL in app/lib/promo.shared.js
CheckcanAccessPromoAdmin / assertPromoAdmin in app/lib/promo.server.js
NavshowReferralAdminNav in app/routes/app.jsxReferral link
Route guardEvery /app/referral* loader/action calls assertPromoAdmin

Access passes if session email or shop email matches the allowlist. Merchants never see partner management, earnings, or settings — nav hide alone is not enough; loaders return 403.


FieldPurpose
idUUID
codeUnique referral code (normalized, no hyphens stored)
name, emailPartner contact
statusactive | paused | rejected
commissionRateBpsDefault 3000 (= 30%)
payoutCurrencyUSD
notesAdmin notes
createdAt / updatedAt
FieldPurpose
referredByPartnerIdFK nullable
referredAtWhen attribution was set
referralSourcecode | admin
referralCodeEnteredAtWhen merchant entered a code
FieldPurpose
partnerId / shopIdFKs
periodStart / periodEndBilling period
revenueCentsQualifying revenue for period
commissionCentsRate × revenue
sourcesubscription (MVP)
shopifyChargeIdCharge / subscription id when available
statuspending | approved | paid | void
paidAt / payoutReferenceManual payout tracking

Idempotency: unique on (shopId, periodEnd, source).

5.4 ReferralProgramSettings (singleton id = singleton)

Section titled “5.4 ReferralProgramSettings (singleton id = singleton)”
FieldDefault
enabledtrue
defaultCommissionRateBps3000
revenueBasissubscription
durationMonths12
holdDays14
minPayoutCents5000
attributionWindowDays90 (reserved for future cookie/landing)
allowMerchantCodeEntrytrue
allowCodeAfterPaidfalse

flowchart TD
install[Shop installs] --> attr[Attribute via Billing code or admin]
attr --> free[Shop on free plan]
free --> upgrade[Shop upgrades to paid]
upgrade --> webhook[Billing confirm / subscription webhook / plan sync]
webhook --> calc[Create ReferralCommission pending]
calc --> hold[Hold 14 days]
hold --> approve[Approve on next Referral admin load]
approve --> balance[Approved unpaid balance]
balance --> payout[Admin marks paid after Wise/PayPal transfer]

accrueCommission in app/lib/referral.server.js:

  1. Program must be enabled; shop must have an active partner attribution.
  2. Still inside 12 months from the first non-void commission period (or this period if first).
  3. Creates pending row for $20 revenue × partner rate (default 30% = $6).
  4. Skips quietly if already accrued for that (shopId, periodEnd, source).

On each admin Referral page load, approveHeldCommissions moves pendingapproved when older than holdDays.

On subscription cancel / decline / plan sync to free, voidCommissionsForShop voids pending rows for that shop.


Single side-nav item Referral/app/referral with tabs:

TabWhat you do
OverviewKPIs (active partners, referred/paying shops, pending/approved/paid sums), recent attributions & earnings
PartnersCreate / edit / pause / activate; App Store link with ref=CODE; open detail
EarningsFilter by status/partner; bulk approve, void, mark paid (optional payout reference)
AttributionsAssign by shop domain; search; clear (force override for paid shops)
SettingsToggle program, rates, duration, hold, min payout, merchant code entry flags

Partner detail (/app/referral/partners/:id): profile, referred shops, commissions, mark approved → paid with payout reference.

Public App Store link format:

https://apps.shopify.com/appifire-ai-chat?utm_source=referral&ref=CODE

(Shopify does not always pass ref into the app; merchants should still enter the code on Billing, or you assign in Attributions.)


On /app/billing:

  • Card “Have a referral code?” when settings allow entry and the shop is not yet attributed.
  • After apply: shows attributed partner code; cannot re-apply.
  • Blocked when shop is already paid unless allowCodeAfterPaid is enabled in Settings.
  • Self-referral blocked when shop email matches partner email.

  1. Run migration if needed: npx prisma migrate deploy (or migrate dev locally).
  2. Open the app as zainalam1001@gmail.comReferral.
  3. Partners → create partner (name, email, code).
  4. Share the App Store ref= link and/or tell the merchant to enter the code on Billing.
  5. When they go paid, a pending commission appears (Overview / Earnings / partner detail).
  6. After the hold, status becomes approved (auto on admin load).
  7. Pay the partner outside Shopify; Mark paid and store Wise/PayPal reference.
  8. Adjust program knobs under Settings without redeploying.

MetricValue
Paid plan$20 / month
Partner share30% → $6 / month
Max duration12 months → $72 lifetime per paid referral (if they stay paid)
10 active referred paid shops$60 / month partner cost
Your net from those 10$140 / month after commissions

  • Merchant-to-merchant “refer a friend for free credits” (use promo codes).
  • Multi-level / MLM commissions.
  • Automatic ACH inside Shopify Admin.
  • Non-USD payouts.
  • Public partner apply form / self-serve portal.
  • Cookie / landing-page install attribution.
  • Commissions on one-time AI credit packs (revenueBasis = subscription_and_credits is stored but not accrued yet).
  • CSV export (can add later; ledger is in-admin today).

This file began as a design plan. MVP decisions locked and built:

  • Subscription-only revenue, 12-month cap, 14-day hold, $50 min payout setting.
  • Single /app/referral page with tabs (not separate nav items).
  • Reuse canAccessPromoAdmin as-is.
  • Merchant code entry only before first payment (default).
  • Void pending commissions on cancel/downgrade within hold.