Skip to content

Sales Referrals — Deploy Runbook

One-time / per-environment steps to ship the sales-referral overhaul. Run staging first, verify, then prod — never straight to prod. For the day-to-day console see Sales Referrals — Admin Setup.

This change reshapes the data model (splits Referral into SalesReferrer + attribution Referral), adds a payout state machine, a payout-release job, and new Stripe webhook events. So a deploy is code + webhook config + data migration + job scheduling, in that dependency order.

0. Pre-deploy audit — PR #387 Connect-account overlap ⚠️

PR #387 (referral Connect fix) shipped to main after this branch was cut. For zero-referral users it persisted the referral-payout Connect account id to Organization.connectSettings.stripeConnectAccountId. The new model keeps the referrer's account on SalesReferrer.referrerStripeConnectAccountId, and the migration does not copy from connectSettings — that field is shared with the storefront Connect feature, so a blind copy would mis-attribute a storefront account as a referral-payout account.

Before deploying to a given environment, audit (read-only) for anyone who onboarded via #387 and would otherwise appear "disconnected":

  • Find orgs where connectSettings.stripeConnectAccountId is set and the owner/referrer's SalesReferrer.referrerStripeConnectAccountId is empty.
  • For each, confirm by hand whether that Connect account is a referral-payout account (vs a storefront account) before copying it onto the SalesReferrer.

As of 2026-07-08 this is expected to be ~0 rows (#387 is not on prod yet and the referral feature is unlaunched). If the audit is empty, there's nothing to reconcile. The migration script header documents this too.

1. Ship code to staging

bash
cd platform && bash deploy-staging.sh

(Full deploy — the change touches API, worker, and web. Don't use --api-only/--web-only here.)

2. Add the new Stripe webhook events (staging endpoint)

On the staging Stripe webhook endpoint, subscribe these. The first is easy to get wrong — the engine listens for invoice.paid, not invoice.payment_succeeded:

  • invoice.paid (commission creation trigger)
  • invoice.payment_failed
  • customer.subscription.updated
  • customer.subscription.deleted
  • charge.refunded
  • charge.dispute.created

invoice.payment_failed and the subscription/charge events are new for this change. Add them via the Stripe Dashboard (test mode) or the CLI, pointing at the staging endpoint.

3. Migrate the data (dry-run, then apply) — staging

The migration splits legacy Referral docs into SalesReferrer + attribution Referral, preserving _id so ReferralCommission references survive. It's idempotent.

bash
# DRY RUN first — writes nothing, prints the plan + any FK-integrity flags
MONGODB_URI="$(gcloud secrets versions access latest --secret=STAGING_MONGODB_URI)" \
  pnpm --filter @gbp/api exec tsx src/scripts/migrate-referral-model.ts --dry-run

# Review the report. Then apply:
MONGODB_URI="$(gcloud secrets versions access latest --secret=STAGING_MONGODB_URI)" \
  pnpm --filter @gbp/api exec tsx src/scripts/migrate-referral-model.ts

Re-running is safe — converted docs lose referralCode and are skipped.

4. Verify on staging

  • Console loads: /admin/sales-referrers shows the roster with canonical totals.
  • Referrer flow: as a test user, Settings → Referrals → Generate code → Connect Stripe (Express test onboarding) → status resolves to Connected.
  • Release job scheduled: confirm the worker registered the hourly release_referral_payouts job.
    bash
    gcloud run services logs read gbp-worker-staging --region=us-central1 --limit=100 \
      | grep -Ei "release_referral_payouts|referral-payout"
  • End-to-end (the real test): pay a test invoice for an attributed org → a held commission appears. Temporarily lower referralPayoutHoldDays (or advance the hold) and confirm the release job transfers it and it flips to paid with a stripeTransferId. Refund the invoice on a still-held commission and confirm it voids (full) or recomputes (partial).

5. Prod — repeat after sign-off

Only after staging is verified and the owner signs off:

bash
# (repeat the §0 audit against PROD_MONGODB_URI first)
cd platform && bash deploy-production.sh

# Add the SAME webhook events to the PROD Stripe webhook endpoint (live mode).

# Migrate prod (dry-run then apply):
MONGODB_URI="$(gcloud secrets versions access latest --secret=PROD_MONGODB_URI)" \
  pnpm --filter @gbp/api exec tsx src/scripts/migrate-referral-model.ts --dry-run
MONGODB_URI="$(gcloud secrets versions access latest --secret=PROD_MONGODB_URI)" \
  pnpm --filter @gbp/api exec tsx src/scripts/migrate-referral-model.ts

6. Post-deploy verification (prod)

  • /admin/sales-referrers loads with real data; totals look sane.
  • The release_referral_payouts job is scheduled on gbp-worker-prod.
  • Watch the first real invoice.paid after deploy create a held commission:
    bash
    gcloud run services logs read gbp-api-prod --region=us-central1 --limit=100 \
      | grep -Ei "referral|commission"
  • After the 30-day hold on the first commission elapses, confirm it releases and pays out.

Config reference

SettingWhereDefault
Payout hold window (days)PlatformConfig.referralPayoutHoldDays30
Default commission rate (%)REFERRAL_DEFAULT_COMMISSION_PERCENT (per-rep override in console)7
Attribution window (days)REFERRAL_DEFAULT_ATTRIBUTION_WINDOW_DAYS (per-rep override in console)60
Admin allowlistPLATFORM_ADMIN_USER_IDS (+ PLATFORM_ADMIN_ENFORCE=true)
Capture cookie signingreuses ENCRYPTION_KEY
Cross-subdomain capture cookieREFERRAL_COOKIE_DOMAIN (optional, e.g. .localmarketing.so)host-scoped
Release job cadencerelease_referral_payouts cron15 * * * * (hourly)

Rollback

Code rolls back by redeploying the previous revision (gcloud run services update-traffic ... --to-revisions=<prev>=100). The migration is not auto-reversible — it converts docs in place. If you must roll back after migrating, restore the referrals collection from the pre-migration Atlas snapshot. Because the migration preserves _id, ReferralCommission references stay valid either way.


Next: Failed Jobs Dashboard