01 System architecture
Monorepo: Next.js web + NestJS API + PostgreSQL (via Prisma), calling out to Shopify's Admin API.
The API owns 4 controllers (admin, affiliates, shopify, tracking) + 2 services (core.ts commission engine, shopify.service.ts Admin-API client).
02 ERD — ownership (multi-tenant)
One Merchant (the brand) owns every table through a merchantId foreign key. Crow's-foot = the "many" side.
Delete a Merchant → all 9 tables cascade-delete. (Click has no merchantId — it lives under Affiliate only; see the next diagram.)
03 ERD — the operational core
The transactional heart: Affiliate and its records, with primary keys, foreign keys and cardinality. Dashed lines are accounting links.
Reading it: a Merchant has many Affiliates; a CommissionGroup has many Affiliates (groupId); an Affiliate has many Orders, Payouts, Clicks and FraudFlags; a Payout settles many Orders; an Order can raise FraudFlags.
04 Relationships — exact reference
Every FK with cardinality and on-delete behaviour, straight from schema.prisma.
| Parent | Child | Card. | FK | On delete |
|---|---|---|---|---|
| Merchant | Affiliate · CommissionGroup · ReferralOrder · Payout · ProductCommission · SignupFieldDef · Asset · EmailLog · FraudFlag | 1 : N | merchantId | Cascade |
| CommissionGroup | Affiliate | 1 : N | groupId | optional (nullable) |
| Affiliate | ReferralOrder | 1 : N | affiliateId | Cascade |
| Affiliate | Click | 1 : N | affiliateId | Cascade |
| Affiliate | Payout | 1 : N | affiliateId | Cascade |
| Affiliate | FraudFlag | 1 : N | affiliateId | SetNull |
| Payout | ReferralOrder | 1 : N | payoutId | SetNull |
| ReferralOrder | FraudFlag | 1 : N | orderId | SetNull |
05 DFD — context (level 0)
The whole system as one process, with the three external entities and the data flowing in and out.
Two people (creator, brand) and one machine (Shopify) interact with the platform. Everything else is internal.
06 DFD — level 1 (processes & stores)
The five processes in order, each with the ▤ data stores (tables) it writes.
Left = who triggers it · centre = the process · right = which tables change. (r) means read-only.
07 Sequence — the affiliate loop
Call order across the five participants. Solid = request, dashed = response/effect.
Steps ①–③ onboard & arm the affiliate; ④–⑤ capture & attribute the sale; ⑥–⑦ pay them out.
08 Commission engine — resolution order
calcCommission() in core.ts + a product-level override during sync. First match wins, top to bottom.
- Personal coupon? → commission = 0
- Product / tag rule matches a line item → that rate (affiliate rule > "all")
- Per-affiliate rate set → use it
- Group rate, tiered by sales-this-month
- Merchant default (fallback)
- Base = subtotal or total (commissionBase)
- PERCENT: base × value ÷ 100 · FLAT: fixed
- Tiers:
Group.tiers= [{minSales,maxSales,rate}] - Writes
ReferralOrder.commission, adds toAffiliate.balance
POST /api/admin/sales/:id/refund subtracts the commission & marks the order REFUNDED. Real Shopify refunds aren't auto-synced yet (webhooks item).09 API reference
All routes prefixed with /api, grouped by controller.
| Controller | Endpoints |
|---|---|
| tracking | GET /r/:code · POST /checkout/simulate |
| affiliates | POST signup · POST login · GET :id · POST :id/{profile,payment,referral-code,notifications,password,payout-request} |
| shopify | GET status · POST push-coupons · POST sync-orders |
| admin · dashboard | GET dashboard · analytics · leaderboard |
| admin · affiliates | GET affiliates · GET/POST/PUT/DELETE affiliates/:id · POST :id/{approve,reject,mark-paid} · bulk-approve |
| admin · commission | GET/POST/PUT/DELETE groups · GET/POST/DELETE product-commissions |
| admin · coupons | GET coupons · coupons/assignable · POST/PUT/DELETE coupons · coupons/bulk-update · GET/PUT auto-coupon |
| admin · sales | GET sales · POST sales/:id/{approve,reject,refund} |
| admin · payouts | GET payouts · payouts/:id/invoice · GET/PUT payment-settings |
| admin · settings | GET/PUT settings · branding · notifications · signup-fields · signup-defaults |
| admin · other | GET fraud · POST fraud/:id/resolve · GET/POST/DELETE assets · GET emails · POST emails/bulk |
10 Repo structure & run
partnernook/ ├─ apps/api/ # NestJS 11 + Prisma 6 │ ├─ prisma/ │ │ └─ schema.prisma # 11 models │ ├─ .env # DB url + Shopify token │ └─ src/ │ ├─ main.ts # bootstrap · :4000 · /api │ ├─ app.module.ts │ ├─ prisma.service.ts │ ├─ admin.controller.ts # brand CRM (largest) │ ├─ affiliates.controller.ts # portal + signup │ ├─ shopify.controller.ts # status/push/sync │ ├─ tracking.controller.ts # referral clicks │ ├─ core.ts # calcCommission() │ └─ shopify.service.ts # Admin API client │ └─ apps/web/ # Next.js 16 + React 19 ├─ app/ │ ├─ layout.tsx # root + PublicHeader │ ├─ page.tsx # landing │ ├─ admin/ # CRM pages │ ├─ dashboard/ # affiliate portal │ ├─ signup/ · store/ # public pages │ └─ globals.css ├─ components/ # design system │ ├─ admin-shell · portal-shell │ ├─ ui · data-table · modal · charts │ └─ toast · settings-tabs · public-header └─ lib/api.ts # fetch helper
- Postgres up; set
DATABASE_URLinapps/api/.env - API:
cd apps/api→npx prisma db push→npm run start:dev(:4000) - Web:
cd apps/web→npm run dev(:3000) - Shopify: add
SHOPIFY_SHOP+SHOPIFY_ADMIN_TOKENto the API.env
Scopes: read_orders, write_discounts, read_products. All admin data scoped via one getMerchant() helper (one brand per instance today).