Changelog
Aug 24, 2026
4 entries
Monorepo Decision Reversed — Two Separate Repos
The "Update (2026-08-04): Monorepo — Resolved" entry below is superseded. Per Infrastructure & DevOps/Git Repository Strategy's 2026-08-24 reversal, frontend and backend now live in two separate repositories (sellvia-frontend, sellvia-backend), not one repo with apps/frontend/apps/backend. Practical effect on this pipeline: the "git integration" each deploy path already runs from (Vercel watching the frontend repo, whatever triggers the backend VPS deploy watching the backend repo) now points at two distinct repos instead of one repo with two watched paths. The path-filtered-CI-jobs mechanism…
Frontend and Backend Are Separate Repos
Per Git Repository Strategy's reversal, sellvia-frontend and sellvia-backend are two separate clones, not apps/frontend/apps/backend inside one repo — corrected in "Local Setup — Actual Commands" above. Nothing else in this guide (environment variables, checklists, failure modes) changes — those were never monorepo-specific.
Reversed — Two Separate Repositories, Not a Monorepo. The Decision section below (originally "Monorepo," 2026-08-04) is superseded. Frontend…
⚠️ Update (2026-08-24): Reversed — Two Separate Repositories, Not a Monorepo. The Decision section below (originally "Monorepo," 2026-08-04) is superseded. Frontend and backend now live in two separate repositories, alongside this documentation repository as a third — three repos total. The monorepo's original reasoning (atomic cross-cutting PRs, fewer moving parts for a solo founder) is kept below for history, but is no longer the operating model. Everything under "Decision," "Structure," "CI Runs Only What Changed," and the 2026-08-04 "Service-Scoped Feature Branch Naming" update is supersed…
Per Git Repository Strategy's reversal, the "Monorepo" and "service-prefixed branch naming" bullets below no longer apply — replaced by two…
Two separate repos (sellvia-frontend, sellvia-backend) — independent ownership, independent release cadence per person; the coordination overhead this trades away is handled via API/API-CONTRACT-SHEET.md's status column and matching tracking-issue numbers across the two repos' PRs (see Git Repository Strategy's "Coordinating Cross-Cutting Changes") - Plain feature branch naming (feature/<thing>) — a service prefix is no longer needed since each repo only ever contains one service's branches - Independent CI per repo — a frontend PR doesn't wait on backend tests and vice versa, because they're…
Aug 23, 2026
76 entries
Contrast Verification RESOLVED
Actually computed against the relative-luminance WCAG formula (not guessed): | Text color | On black (#000000) | Result | | --- | --- | --- | | White #FFFFFF | ~21:1 | Pass (AAA) | | Lime #BFFF13 | 17.5:1 | Pass (AAA) — lime was never the actual risk; it's high-luminance and safe as text too, though still used sparingly per Design System's intent | | Gray 01 #A1A1AA | 8.2:1 | Pass (AAA) | | Gray 02 #71717A (original) | 4.35:1 | Fail — under the 4.5:1 AA minimum for normal text | Fix applied: Gray 02 changed to #787882 (same hue, channels raised ~7 points) → 4.82:1, passes AA with margin. Updat…
Campaign → Offer Vocabulary Reconciled
All "campaign" references above renamed "Offer" (Core Activation Action, the aha-moment description, and the campaign_published event reference, now offer_published) — no separate Campaign entity exists (01. Domain Model, 2026-08-23). This doc previously implied Offer and Campaign were still two separate things ("publish first campaign... not just create an Offer"); corrected to describe one entity's draft → live transition.
Corrected Checkout/Currency/Entity Assumptions
Section 1 previously assumed SellVia has its own hosted checkout page and used stale "Campaign" naming and USD pricing. Corrected: SellVia has no checkout of its own (checkout happens on the merchant's Shopify store, reversed 2026-08-07); the entity is "Offer," not "Campaign" (Campaign retired, merged into Offer); and MVP currency is PKR only.
Reconciled for the Offer Entity Merge
This doc referred throughout to "Campaign" (module names, feature headers, prose). Campaign was merged into Offer (01. Domain Model) — every such reference above is corrected to "Offer," matching sibling 02. Technical Architecture docs' convention.
MAJOR REVISION — Pakistan-Only Market, Paddle Removed, Shopify-Only Integration, Offer Absorbs Campaign
Four founder decisions, all effective immediately, superseding the entries above where they conflict. ### Market scope: Pakistan-only for MVP (REVERSES "USD/EUR/GBP only, PKR dropped") Alternatives considered: Continuing global USD/EUR/GBP scope (original MVP decision, now reversed) Reasoning: Founder decision to launch to the Pakistani market exclusively — every merchant onboarded for MVP is a Pakistani business. This resolves the still-open "beachhead niche" question from Product Roadmap/Product Vision by geography rather than product category, and removes the entire reason PKR was dropped i…
Reconciled to the Offer Entity Model and Current Sale-Status Vocabulary
This doc previously referred to "Campaign" (retired, merged into Offer — see 01. Domain Model) and to a Sale verified → refunded/clawback framing that no longer exists (clawback was eliminated 2026-08-07; sales now move through acceptance_status: reported → accepted/rejected, per State Machines). Both are corrected above.
Paddle Removed, Swich Confirmed, Pakistan/PKR Only
Founder decisions, full reasoning in 02. Architecture Decision Log. Every "Paddle" above means Swich now: - Formula: "Paddle processing fees" → "Swich processing fees." Same principle (a real cost line separate from SellVia's 2% platform fee) — exact rate unconfirmed pending a real Swich signup conversation, so the "roughly 2.9% + $0.30 per charge" figure above is Paddle's, not a verified Swich number; don't carry it forward as an estimate. - Data source table: "Paddle's Balance Transactions API (fee field per transaction)" → Swich's equivalent transaction/fee API — exact endpoint unconfirmed.…
Paddle → Swich, Pakistan/PKR Only, Offer Replaces Campaign
Founder decisions, full reasoning in 02. Architecture Decision Log. - "Payments: Paddle" → "Payments: Swich" — Swich's Python SDK (if one exists) or direct REST API calls, not yet confirmed which pending real integration docs. "Why Paddle" section above (managed KYC/tax-form collection) is not fully true of Swich — Swich is a processor, not a Merchant of Record, so it does *not* absorb KYC/tax-form collection the way Paddle's reasoning here assumed; see 05. Tax Considerations for the resulting gap. This section's reasoning is superseded, not just its vendor name. - "I/O-bound (Paddle API calls…
Paddle Removed, Swich Confirmed — Offer Replaces Campaign
Founder decisions, full reasoning in 02. Architecture Decision Log. - Every "campaign"/"Campaign" above means "offer"/"Offer" — no separate Campaign entity (01. Domain Model). - "Merchant's Paddle account gets restricted/flagged" → "Merchant's Swich billing gets restricted/flagged." The auto-pause response above is unchanged in principle — all of that merchant's live Offers transition to paused automatically — but the trigger event is now whatever Swich's equivalent restriction/risk-flag webhook is, not Paddle's seller.updated. Exact Swich event name/shape unconfirmed pending real integration.…
MAJOR REVISION — Pakistan/PKR Only, Offer Replaces Campaign
See 02. Architecture Decision Log for full reasoning. This section corrects the rules above. Multi-Currency section is superseded: SellVia is Pakistan-only for MVP, PKR is the only supported currency. USD/EUR/GBP support (and the reasoning about Paddle handling them) no longer applies to MVP — it's a post-MVP item now, see Full Product Vision (Post-MVP). The "PKR dropped because Paddle can't pay it out" reasoning is moot: Paddle itself is removed (no processor for MVP; working default is direct bank transfer — see MVP Scope, Payment Flow). "Campaign" throughout this document (and Campaign Rule…
Payout Rules Section Corrected — No Instant Paddle Split
Payout Rules above still described commission crediting "instantly per sale (via Paddle split)" — stale, corrected. Per Commission Engine's reversed model (also 2026-08-07/2026-08-23), commission is calculated and recorded as owed when a merchant-reported sale is accepted, and only becomes available in the creator's balance once the corresponding merchant billing cycle is billed and paid via Swich — not instantly, and not via Paddle (Paddle is removed for MVP). The $50 threshold mechanism itself is unchanged; the PKR-equivalent figure is still TBD, same open item as elsewhere in this doc set.
Paddle Removed, Swich Confirmed — Real Open Question This Creates
Founder decisions: Pakistan-only market, Paddle removed, Swich (swichnow.io) confirmed same day. Full reasoning: 02. Architecture Decision Log. Every "Paddle" above means "Swich" for the mechanics that still apply (dispute notification via webhook, funds held pending resolution, evidence-submission window, a dispute fee — all generic to any processor, not Paddle-specific in principle). Genuinely new question this raises, not just a rename: SellVia's billing through Swich isn't purely card-based — the doc above assumes a Paddle-style card chargeback flow specifically. Swich's checkout also offe…
Frontend Deploys Via Vercel; Migration Step Corrected
Per Hosting Strategy's 2026-08-07 decision, the frontend (Next.js) now deploys via Vercel's own git-integrated pipeline (deploy-on-push, preview deployments per PR), not the "git pull → npm install → build → restart" VPS mechanics described above under "Deploy Mechanics" and reiterated in the 2026-08-03 "frontend only" update — those mechanics are retired for the frontend. Also correcting an error in that same 2026-08-03 update: the "Deploy Mechanics" migration step ("Run database migrations") was written before the frontend/backend split and got folded into what the 2026-08-03 update then lab…
Pakistan/PKR Only, Offer Replaces Campaign, No Processor
See 02. Architecture Decision Log for full reasoning. - "Campaign.commission_rate" throughout this doc means "Offer.commission_rate" — no separate Campaign entity (01. Domain Model). - Currency is PKR, not USD. The worked example below replaces the earlier $68.00 example. - "Merchant owes SellVia" is no longer billed via Paddle — billed via Swich (confirmed 2026-08-23, replacing the interim bank-transfer working default). Same three-way math, different processor: Swich generates the billing-cycle invoice/payment-link, merchant pays through it, webhook confirms. See 05. Payment Flow, 05. Payout…
Reconciled to Offer Entity Model and PKR Currency
This file was never revised for the Offer/Campaign merge or the PKR-only currency decision. "Campaign" terminology throughout (list/table view, creation form, discovery card, empty states) is now "Offer," and the Stat Card example uses a PKR figure instead of USD.
PKR-Only, Offer Replaces Campaign
Founder decisions, full reasoning in 02. Architecture Decision Log. - Currency enum flipped: was CHECK (currency IN ('USD','EUR','GBP')) (with PKR deliberately excluded), now CHECK (currency IN ('PKR')) — the exact reverse. If multi-currency returns post-MVP (Full Product Vision), this constraint is where that change lands. - applications' unique constraint moves from (campaign_id, creator_profile_id) to (offer_id, creator_profile_id) — no separate Campaign table (01. Domain Model, 03. Table Specifications). - Status enums on the offers table (was campaigns) — same four values (draft/live/paus…
Paddle removed, Swich confirmed (02. Architecture Decision Log) — the exact domains above are placeholders pending real Swich integration do…
Rollout approach: start with Content-Security-Policy-Report-Only (logs violations without blocking anything) before switching to enforcing Content-Security-Policy — CSP misconfiguration is a real way to silently break merchant card updates in production; report-only mode catches this before it's user-facing, not after. Prefer nonces over unsafe-inline for scripts where Next.js supports it (via middleware-generated nonces) — unsafe-inline on script-src specifically defeats a large part of what CSP protects against (XSS).
Paddle → Swich, Pakistan/PKR Only
Founder decisions, full reasoning in 02. Architecture Decision Log — reflected inline above.
Note on "Offers — commission rate and status included"
The 2026-08-04 section above bundles commission rate and status into Offers' last-write-wins bucket. That's correct only as of 2026-08-23: there's no separate Campaign table as of that date (Campaign was merged into Offer, see Domain Model / Table Specifications) — three weeks after the section above was originally written. The underlying two-tier conflict-resolution strategy itself is unchanged by this; this note just corrects the provenance of that one clause.
Gray 02 Corrected for WCAG AA — RESOLVED
Gray 02 changed from #71717A to #787882. Actual contrast verification (see UX/Accessibility) found the original value — Tailwind's zinc-500 — measured 4.35:1 against black, failing the 4.5:1 AA minimum for body text by a small margin. #787882 keeps the same hue/blue-gray tint and measures 4.82:1, comfortably compliant. Lime and Gray 01 were also verified and need no change — see Accessibility doc for full numbers.
MAJOR REVISION — Campaign Removed, Merged Into Offer
Resolves this doc's own open question ("Does Offer need its own entity separate from Campaign?") — founder decision: "offer is offer, it is not turning into any campaign at all." There is no Campaign entity. Every field and relationship the Campaign entity used to hold now lives directly on Offer. ### Offer (revised — was "Offer (Product)" above, now absorbs Campaign) - belongs to Merchant - name, price, category (digital/physical) - commission_rate (%, merchant-set, e.g. 20% — moved from Campaign) - status: draft / live / paused / ended (moved from Campaign — see State Machines) - example: Gl…
Two Stale Field Claims Corrected
Commission rate range removed: the original Campaign entity section above claimed a "range 10–40% shown on the commission slider" for commission_rate. This contradicted Business Rules' explicit, more specific rule that the merchant sets the commission % freely with no platform min/max — corrected in place above. Sale.status vocabulary corrected: the original Sale entity section above still listed status: pending / verified / disputed, left unfixed by this doc's own 2026-08-07 update (which added acceptance_status as a new field but didn't remove the stale "verified" status value). Per State Ma…
Auth Provider Is Ory Kratos, Not Clerk — "The Clerk Boundary" Superseded
The auth provider switched from Clerk to Ory Kratos on 2026-08-04 (see Hosting Strategy's Clerk → Ory Kratos update). The "The Clerk Boundary" section above, and the "(via Clerk, per the boundary above)" note under transactional email, are superseded — read "Clerk" as "Ory Kratos" throughout: it's Kratos that sends email verification and password reset/change by default, and the same two-option decision (Kratos's default sending vs. custom SMTP through mail.wesellvia.com) still applies. On the SMTP question specifically: Ory Kratos's courier service sends transactional email through a configur…
MAJOR REVISION — Offer Replaces Campaign, Shopify Webhook Replaces Merchant-Sales Snippet, No Paddle
Founder decisions, full reasoning in 02. Architecture Decision Log. Every /campaigns route above is renamed /offers — no separate Campaign entity (01. Domain Model): - GET /offers, POST /offers, PATCH /offers/:id, PATCH /offers/:id/status (replacing the four /campaigns routes) - POST /offers/:id/applications, GET /offers/:id/applications (replacing the Applications group's /campaigns/:id/... routes) - POST /admin/offers/:id/vet (replacing POST /admin/campaigns/:id/vet) - Everywhere above that says "campaign"/"campaigns" (Affiliate Links section, rationale paragraphs) means "offer"/"offers." PO…
Paddle → Swich, Pakistan/PKR Only
Founder decisions, full reasoning in 02. Architecture Decision Log. Every PADDLE_* env var and Paddle-specific checklist item above is superseded: - PADDLE_API_KEY / PADDLE_WEBHOOK_SECRET → SWICH_API_KEY / SWICH_WEBHOOK_SECRET (exact names pending real Swich integration docs), across Local/Staging/Production. - "Paddle CLI forwarding webhooks to localhost" → whatever Swich's equivalent local-testing tool is (unconfirmed — Swich may not have a CLI-forwarding tool the way Paddle did; may require a tunneling tool like ngrok pointed at /webhooks/swich instead — Needs clarification). - "Fake/test u…
Paddle → Swich
Founder decisions, full reasoning in 02. Architecture Decision Log. The "Paddle mode" table column and every "Paddle test/live mode" reference above mean Swich now — same separation principle (test mode for Local/Staging, live only for Production), same severity of concern (a test/live mixup on a real payments system). PADDLE_API_KEY in the Configuration section is superseded by SWICH_API_KEY. CLERK_SECRET_KEY in that same line is separately stale, unrelated to this update — auth switched to Ory Kratos back on 2026-08-04.
MAJOR REVISION — Campaign Removed, Paddle Fields Gone
Founder decisions, full reasoning in 02. Architecture Decision Log; schema-level detail in 03. Table Specifications (updated same date). - Campaign entity removed — "offer is offer, it is not turning into any campaign at all." OFFERS ||--o{ CAMPAIGNS : has and CAMPAIGNS ||--o{ APPLICATIONS are gone; OFFERS ||--o{ APPLICATIONS connects directly, one fewer hop than the original diagram. - BillingCycle added to this diagram — it existed in Table Specifications since 2026-08-07 but was never added to this ER diagram/summary until now; corrected above. - Paddle-derived fields removed, Swich fields…
Envelope Shape Reconciled to API-CONTRACT-SHEET.md
The example above previously showed an error-only body (no data key), lower_snake_case codes, and a status field embedded in the error object. Corrected to match API-CONTRACT-SHEET.md, the canonical living contract doc: {"data": null, "error": {"code", "message"}}, SCREAMING_SNAKE_CASE codes, no embedded status.
SaleVerified and ClawbackApplied in the enum above are retired/superseded, not current values — see Update (2026-08-23) below.
Never updated, never deleted — append-only, matching the "never hard-delete the financial chain" principle already established in Soft Delete Policy, taken to its natural conclusion.
Retired Event Types
Two values in the financial_events.event_type enum above are retired, not current: - SaleVerified — superseded by the acceptance_status lifecycle (reported → accepted/rejected), per 01. Business Logic → State Machines. The old "verified" framing was replaced 2026-08-07. - ClawbackApplied — clawback doesn't exist; commission is never clawed back at all, per 01. Business Logic → Commission Engine (see also 03. Database → Table Specifications, where commissions.clawed_back is marked removed).
⚠️ This Doc Predates Two Major Reversals
Same scope of staleness as System Architecture's 2026-08-23 update — this doc's entire "Key Events" table, diagram, and worked "Full Chain" section describe SellVia-hosted checkout with an instant Paddle split, both reversed 2026-08-07, on top of Paddle itself being replaced by Swich 2026-08-23. A line-patch would misrepresent how confident this correction is, so flagging the scope instead: - transaction.completed → "already split by Paddle" — wrong twice over: there's no live split (periodic billing since 2026-08-07) and no Paddle (Swich since 2026-08-23). The real event chain is: Shopify ord…
Campaign → Offer and Sale-Status Vocabulary Reconciled
Campaign events renamed to Offer events: per 01. Domain Model's 2026-08-23 revision (Campaign merged into Offer, no separate entity), campaign_created/published/paused/ended are renamed offer_created/published/paused/ended above — this doc had never been swept for that rename until now. Sale event renamed to match current lifecycle vocabulary: sale_verified is replaced with sale_reported and sale_accepted (plus sale_rejected), matching 01. State Machines' "reported → accepted / rejected" model — "verified" no longer applies, since SellVia trusts a merchant-reported sale rather than witnessing…
Contrast status — RESOLVED 2026-08-23: Lime (#BFFF13) and Gray 01 (#A1A1AA) both verified compliant against black (17.5:1 and 8.2:1). Gray 0…
Contrast status — RESOLVED 2026-08-23: Lime (#BFFF13) and Gray 01 (#A1A1AA) both verified compliant against black (17.5:1 and 8.2:1). Gray 02 was the actual failure at 4.35:1 — corrected to #787882 (4.82:1). See [UX/Accessibility] for the full computed table. No longer an open risk. - Source: [UX/Accessibility], [Security/Security Checklist].
Paddle Removed, Swich Confirmed
Founder decisions, full reasoning in 02. Architecture Decision Log. "Reconciliation Against Paddle" above means Swich now — same principle (Swich's own transaction records are the ultimate source of truth for actual money movement, since that's the system that actually executes the transfer). Currency is PKR only. See 05. Reconciliation (updated same date) for the mechanics.
Paddle → Swich, Offer Replaces Campaign, Shopify-Only, Pakistan/PKR
Founder decisions, full reasoning in 02. Architecture Decision Log. Less deep staleness than System Architecture/Event-Driven Architecture (this doc's UI-layer content ages better), but real corrections: - §2/§3 "campaign creation/management," "campaign discovery/browse" → "offer creation/management," "offer discovery/browse" — no separate Campaign entity (01. Domain Model). - §4 "Merchant billing card collection — a Paddle Checkout form" → Swich billing connect — same role (the only payment-widget-adjacent surface on SellVia's own frontend), different vendor, and possibly a different shape en…
~~A payment processor / MoR provider~~ — resolved 2026-08-23: Swich, confirmed as the MVP processor (see MVP Scope, Architecture Decision Lo…
~~A payment processor / MoR provider~~ — resolved 2026-08-23: Swich, confirmed as the MVP processor (see MVP Scope, Architecture Decision Log). What's genuinely deferred now: a Merchant-of-Record provider (Paddle-like — one that absorbs tax/compliance obligations, not just moves money) — relevant if/when SellVia expands beyond Pakistan and needs multi-jurisdiction tax handling Swich doesn't provide. - Subscription/tiered pricing — the earlier $49/mo idea was dropped in favor of a flat 2% fee; could return as a decoupled paid tier for extra features (analytics, priority placement) later, not as…
Campaign → Offer and Sale-Status Vocabulary Reconciled
Merchant Funnel corrected: "Offer created → Campaign published" treated Offer and Campaign as two separate funnel steps — there's no separate Campaign entity (01. Domain Model, 2026-08-23). Collapsed to "Offer created → Offer published," reflecting the Offer's own draft → live status transition. "First sale verified" is renamed "First sale accepted," matching 01. State Machines' reported → accepted/rejected model (see 11. Analytics → Events, updated same date).
campaign_id → offer_id
No separate Campaign table — applications.campaign_id is renamed applications.offer_id (01. Domain Model, 03. Table Specifications, both updated 2026-08-23). The index above is corrected to match; no change to why it's needed or how it's used.
Campaign → Offer and Sale-Status Vocabulary Reconciled
"Live campaign" → "live offer" (Active merchants definition) — no separate Campaign entity (01. Domain Model). sale_verified and "verified Sales" → sale_accepted / "accepted Sales" throughout (Click-to-sale conversion, Time-to-payout, Refund/chargeback rate), matching 01. State Machines' reported → accepted/rejected model and 11. Analytics → Events' same-date update. Time-to-payout also gets a caveat above about the billing-cycle-gated payout timing under the current model.
MAJOR REVISION — Pakistan/PKR Only, Shopify Webhook Replaces Snippet, No Paddle
Founder decisions, full reasoning in 02. Architecture Decision Log. This corrects the Confirmed Flow diagram and every Paddle mention above. Revised flow (replaces the Confirmed Flow diagram above): text Follower clicks creator's link ↓ SellVia records the click, redirects to the merchant's Shopify store (tracking parameters attached) ↓ Customer completes purchase on the MERCHANT's Shopify checkout — SellVia is not involved in this transaction at all ↓ Shopify fires an orders/paid webhook → SellVia (POST /webhooks/shopify-sales) — Shopify-only for MVP, not a generic snippet/pixel (reverses the…
Paddle → Swich
Founder decisions, full reasoning in 02. Architecture Decision Log. "Paddle webhook failures" and "discrepancy between internal records and Paddle" (both in the What Gets Monitored list) mean Swich now — same monitoring principle, same alert-immediately severity. "Paddle/Ory Kratos/Supabase reachability" (Better Uptime section) → "Swich/Ory Kratos/Supabase reachability." Also monitor POST /webhooks/shopify-sales failures (05. Payment Flow) — a second money-relevant webhook source that didn't exist when this doc was written.
MAJOR REVISION — Pakistan-Only, No Processor, Shopify-Only, Offer Absorbs Campaign
Four founder decisions. Full reasoning in 02. Architecture Decision Log; this section is the scope-level summary. Market: Pakistan only for MVP. Every merchant is a Pakistani business. This is the resolved "beachhead" — geography, not a product niche. Currency: PKR only. USD/EUR/GBP support is not needed for MVP (it moves to the deferred list — see Full Product Vision (Post-MVP)). Payments processor: Swich (swichnow.io) — confirmed 2026-08-23, replacing the earlier "no processor, manual bank transfer" working default. A Pakistani payments infrastructure company, PCI-DSS v4.0.1 certified, cover…
~~Exact local settlement rail~~ — resolved 2026-08-23: Swich, covering both billing and payout. Not yet done: the actual signup/integration…
~~Exact local settlement rail~~ — resolved 2026-08-23: Swich, covering both billing and payout. Not yet done: the actual signup/integration work. - New (2026-08-23): Swich onboarding requirements for a 10–25-merchant Private Beta volume — pricing, KYC, minimums — not yet confirmed, needs a real vendor conversation before build starts.
Paddle Removed, Swich Confirmed, Pakistan/PKR Only
Founder decisions, full reasoning in 02. Architecture Decision Log. - "Sale in one currency, creator's payout account set up for another" — moot. Market is Pakistan-only, PKR only; there's no cross-currency case for MVP. This case returns only if multi-currency comes back post-MVP (Full Product Vision). - "Duplicate/replayed Paddle webhook" — means Swich webhook now; the idempotent-processing requirement (02. Event-Driven Architecture) is unchanged in principle, just a different processor's webhook payload. - Chargeback dispute fee — see 05. Chargebacks' 2026-08-23 update: the 5-dispute grace…
MAJOR REVISION — Shopify-Only Webhook Replaces the Universal Snippet, Paddle Removed
Founder decision, full reasoning in 02. Architecture Decision Log. Two changes, both reverse decisions this doc made on 2026-08-07: ### Merchant integration: Shopify webhook only (reverses "Universal Onboarding Snippet") "For now we are just going with Shopify only." Every MVP merchant runs Shopify, so the universal snippet's main advantage (works on any platform) doesn't matter, and Shopify's native orders/paid webhook becomes the primary — and only — mechanism for MVP, not a deferred reliability upgrade. This is more reliable than the snippet, not less: it fires server-side, so it isn't affe…
Paddle Removed, Swich Confirmed
Founder decision: Pakistan-only market, no Paddle ("instead of paddle or anything"), Swich (swichnow.io) confirmed as the processor same day. Full reasoning: 02. Architecture Decision Log. Creator Payout, revised: 1. Background job periodically checks which creators have wallet_balance_cents >= [PKR threshold — not yet re-specified for the currency change, flagged as an open item in 01. User Flows' 2026-08-23 update] 2. For each, the job calls Swich's payout/disbursement API — routed to whichever method the creator registered at onboarding (bank account, JazzCash, or EasyPaisa; Raast where Swi…
"Campaign" → "Offer"
Per 01. Domain Model's 2026-08-23 revision, "campaign" in this table now reads "offer" — no separate Campaign entity exists. No permission logic changed, only terminology. Follow-up correction (same date): the "Approve high-commission/high-risk campaign" row was missed in the original sweep above and still said "campaign" — corrected to "offer" in the Matrix table now. No other stale Campaign or merchant-payout references found in this file; the rest of the matrix (payout row refers to creator payouts, which are unaffected by the merchant-billing model change) is unchanged.
Pakistan/PKR Only, Swich Confirmed as Processor
Market is Pakistan-only for MVP, currency is PKR only (replaces the original $68.00/USD example above). "Collected via periodic billing" no longer means a Paddle charge — it means Swich (confirmed 2026-08-23, same day as the Paddle removal): SellVia generates a billing-cycle invoice through Swich, the merchant pays through Swich's checkout, a webhook confirms it. The 2% flat fee and no-subscription model are unchanged. Full reasoning: 02. Architecture Decision Log. Note: Swich is a payment processor, not a Merchant of Record — its own transaction fees (not yet confirmed, pending a real signup…
"Campaign" Retired as a Term
"Campaign" no longer names a distinct entity — merged into Offer, per founder decision ("offer is offer, it is not turning into any campaign at all") and 01. Domain Model's 2026-08-23 revision. Any doc still using "Campaign" as a noun for the commission-bearing listing should be read as "Offer." Also: commission and price are PKR only for MVP (Pakistan-only market), and the merchant's own checkout is Shopify specifically, not any e-commerce platform — see 02. Architecture Decision Log.
~~Payout mechanism~~ — resolved 2026-08-23: Swich (superseding the earlier PayPal/bank-transfer/Paddle framing entirely — see MVP Scope, Arc…
~~Payout mechanism~~ — resolved 2026-08-23: Swich (superseding the earlier PayPal/bank-transfer/Paddle framing entirely — see MVP Scope, Architecture Decision Log)
Correction — "MVP Definition" Checkout Claim Never Got the 2026-08-07 Reversal
The "MVP Definition" section above still described "SellVia Checkout only" as the model and listed external-site checkout tracking as deferred — that was superseded by the 2026-08-07 reversal (see MVP Scope, Money Flow) and never got corrected here. Corrected: SellVia has no hosted checkout; external-site tracking (redirect + webhook/pixel attribution on the merchant's own Shopify store) is the current MVP model.
~~What's the actual niche/category for the "beachhead" cohort~~ — resolved 2026-08-23 as geography, not category: Pakistan-only for MVP (see…
~~What's the actual niche/category for the "beachhead" cohort~~ — resolved 2026-08-23 as geography, not category: Pakistan-only for MVP (see Update below). Whether a category focus is also needed *within* Pakistan is still open. - What follower/audience-size floor (if any) applies to creator eligibility? The FAQ on [wesellvia.com](http://wesellvia.com) implies "no," but this needs an explicit rule for moderation/quality control.
Pakistan-Only Market, No Processor, Shopify-Only
Founder decision, full reasoning in 02. Architecture Decision Log: - Target market narrows to Pakistan only for MVP — every Merchant is a Pakistani business. This is the resolved beachhead: geography, not a product niche. - Currency: PKR only, replacing USD/EUR/GBP for MVP. - Payment processor: Swich (swichnow.io), confirmed 2026-08-23 — replaces the interim "no processor, bank transfer" default. Covers both merchant billing (recurring invoice-links) and creator payout (disbursement across bank/JazzCash/EasyPaisa/Raast) under one vendor. Not a Merchant of Record like Paddle was — SellVia keeps…
Paddle Removed, Swich Confirmed
Founder decisions, full reasoning in 02. Architecture Decision Log. Pakistan-only market, PKR only. Every "Paddle" above (title, Process section, "Paddle API," "Paddle's payout/balance reports") means Swich now — the mechanics are otherwise unchanged: a periodic automated job compares SellVia's internal Sale/Commission/PlatformFee/BillingCycle/Payout records against Swich's own transaction records, and flags mismatches for Admin review. Exact Swich API endpoints for pulling these records are unconfirmed pending real integration.
Paddle Removed, Swich Confirmed
Founder decisions, full reasoning in 02. Architecture Decision Log. Mechanics on this page are processor-agnostic already (the credit nets against a future billing cycle, regardless of who bills that cycle) — the only change is that "billing cycle" now means a Swich invoice/payment-request, not a Paddle charge. Currency is PKR only. No other change to this doc's logic.
campaigns Table Removed
The campaigns table was merged into offers (01. Domain Model, 03. Table Specifications). The campaigns.offer_id → offers and applications.campaign_id → campaigns rows above are replaced with a single applications.offer_id → offers row.
Response Envelope Reconciled to API-CONTRACT-SHEET.md
The shape above previously omitted the error key entirely. It's been corrected to match API-CONTRACT-SHEET.md, the canonical living contract doc: every response carries both data and error keys, with exactly one non-null.
Vercel Move — Nginx Now Fronts FastAPI Only
Per Hosting Strategy's 2026-08-07 decision, the frontend (Next.js) deploys on Vercel. This VPS's Nginx no longer routes any frontend traffic — it fronts the FastAPI backend only: text Cloudflare → Nginx → FastAPI backend (internal) Next.js/frontend traffic is served directly by Vercel and never passes through this VPS Nginx at all. The "Role" and "Nginx handles" sections above, and any references to routing to the Next.js process, are superseded by this.
Pakistan-only market, PKR only, payment processor is Swich (swichnow.io — confirmed same day, replacing Paddle and a brief interim manual-ba…
Design system (binding): black #000000 background, lime #BFFF13 accent used sparingly (primary CTA/highlights/focus only, never large blocks), white/gray text hierarchy — Gray 02 muted text is #787882, corrected 2026-08-23 from the original #71717A, which measured 4.35:1 and failed WCAG AA (see UX/Accessibility) — Outfit (headlines/CTAs) + Figtree (body/labels), no gradients/glassmorphism/glow, thin borders not shadows, 10–12px radii, restrained animation (fades/opacity/2–4px movement only). Source: UX/Design System. - Responsive: Web-responsive only for MVP (no native mobile app). 12-column d…
D2–D6 collapsed. The five screens below previously described two separate entities — "Offer" (D2/D3) and "Campaign" (D4/D5/D6) — for what is…
Update (2026-08-23): D2–D6 collapsed. The five screens below previously described two separate entities — "Offer" (D2/D3) and "Campaign" (D4/D5/D6) — for what is now one entity per the Offer/Campaign merge (see global note above). Collapsed into three screens: D2 Offers List, D3 Create/Edit Offer, D4 Offer Detail, each merging the more complete/current details from both duplicate specs. Everything downstream (formerly D7–D14) is renumbered D5–D12 accordingly.
Pakistan-only market, payment processor is Swich (swichnow.io — confirmed same day, for the /onboarding/merchant/paddle, /onboarding/creator…
⚠️ Update (2026-08-23): Pakistan-only market, payment processor is Swich (swichnow.io — confirmed same day, for the /onboarding/merchant/paddle, /onboarding/creator/payout, /settings/billing routes below — same routes, but they now render a Swich billing-connect / payee-registration flow, not embedded Paddle Checkout), Shopify-only merchant integration (/onboarding/merchant/tracking-snippet becomes a Shopify connect/OAuth step, not a copy-paste snippet), and no separate Campaign entity (merged into Offer). Full reasoning: Technical Architecture/Architecture Decision Log. FEATURE_LIST.md has be…
campaigns Removed From Scope
campaigns was merged into offers (01. Domain Model) — dropped from the soft-deletable table list above and from the open question, both now speak only of offers.
Campaign State Machine Renamed — Now the Offer State Machine
No entity or transition logic changes — this is a rename only, following 01. Domain Model's 2026-08-23 revision (Campaign merged into Offer, no separate entity). Every "Campaign" reference above (the Campaign State Machine section, its mermaid diagram, the draft→live gates, the pause/end honoring rules, the mid-flight commission-rate-change rule) applies identically to Offer now — read "Campaign" as "Offer" throughout this document. One gate changes in substance, not just name: the second draft→live gate ("tracking snippet verified installed") is now "Shopify webhook connected and verified" pe…
Dead Clawback Reference and Stale Merchant Payout Language Corrected
Sale State Machine's original code block still said "verified → refunded (triggers clawback per Commission Engine's 14-day rule)" — no such rule exists; commission is never clawed back, per Commission Engine's 2026-08-07 confirmation. Corrected in place above. Payout State Machine's note on merchants still described merchants riding "Paddle's standard rolling payout schedule" — stale under the current external-tracking model, where merchants never receive a payout from SellVia at all. They keep their own revenue directly from their own Shopify checkout and are instead billed by SellVia via Swi…
⚠️ This Doc Predates Two Major Reversals — Diagrams and "Core Principle" Are Stale, Not Just the Paddle Name
This is the oldest, least-maintained doc in Technical Architecture. Every diagram above (both mermaid versions) and the "Core Principle" section still describe SellVia-hosted checkout via Paddle — a model reversed on 2026-08-07 (01. Money Flow) and further revised on 2026-08-23 (02. Architecture Decision Log). This is deeper staleness than a find-and-replace can fix responsibly, so this update flags the scope rather than papering over it with a partial patch: - "Core Principle: hosted-checkout marketplace" — wrong. SellVia has no hosted checkout at all; customers buy on the merchant's own Shop…
MAJOR REVISION — campaigns Merged Into offers, Paddle Fields Removed
Founder decisions, full reasoning in 02. Architecture Decision Log. This is the literal schema-level version of 01. Domain Model's 2026-08-23 revision. ### campaigns table — REMOVED Merged into offers. Migration: add commission_rate and status directly to offers, backfill from the corresponding campaigns row, then drop campaigns. ### offers — REVISED | Field | Type | Notes | | --- | --- | --- | | commission_rate | numeric | merchant-set, no platform bounds — moved from campaigns | | status | enum | draft / live / paused / ended — moved from campaigns | | currency | text | PKR only for MVP — wa…
MAJOR REVISION — Pakistan-Only Narrows (and Reopens) This Whole Doc
Founder decision: Pakistan-only market, no Paddle. Full reasoning: 02. Architecture Decision Log. - "Handled by Paddle" section above no longer applies at all. No 1099 generation, no W-8/W-9 collection, no Paddle Tax — there is no MoR provider. Every tax-adjacent task Paddle was doing natively now has no owner — this is a real, newly-opened gap, not a simplification. - Swich (confirmed same day as a later update below) does not close this gap. This is the single most important thing to understand about the Swich decision from a compliance angle: Swich is a payment processor/gateway, not a Merc…
Paddle Removed, Swich Confirmed, Pakistan/PKR Only
Founder decisions, full reasoning in 02. Architecture Decision Log. "Paddle processing fees" (Cost Per User section) and the Net Contribution formula's "Paddle fees" both mean Swich processing fees now — same role in the math, exact rate unconfirmed pending a real Swich signup conversation (see 11. Automated Monthly P&L's 2026-08-23 update). All figures in PKR, not USD.
Paddle Removed, Swich Confirmed, Offer Replaces Campaign
Founder decisions, full reasoning in 02. Architecture Decision Log. - "Campaign"/"campaign" throughout this page means "Offer"/"offer" — no separate Campaign entity (01. Domain Model). The self-dealing rule, duplicate-application constraint (now offer_id, creator_profile_id), and soft-delete handling above are all unchanged in substance. - "Creator's Paddle onboarding incomplete" → "Creator's Swich payee registration incomplete." Same hard-block gate (resolved: block link activation entirely, not "link works but payout held") — only the processor name changes. See 05. Payout Process, [FEATURE_…
Pakistan/Shopify Scope — "Campaign" Above Means "Offer"
Per 02. Architecture Decision Log: no separate Campaign entity (01. Domain Model) — every "Campaign" above (Merchant Flow steps 3–6, Creator Flow steps 2–3, both diagrams) means Offer. Market is Pakistan-only, PKR only; buyer checkout happens on the merchant's own Shopify store, not a SellVia-hosted checkout; "buys via checkout" in the Creator Flow Diagram means the merchant's Shopify checkout, attributed back via Shopify webhook, not a live per-sale split — commission is credited once the merchant's billing cycle is settled via Swich (not bank transfer, as this note previously said), not "ins…
Merchant Role Corrected — Billed, Not Paid Out
Merchant bullet "Receives payouts (their share of each sale)" was stale — under the current external-tracking model, SellVia does not pay merchants a payout at all. Merchants collect their own revenue directly from their own Shopify checkout and are instead billed by SellVia via Swich for the commission + platform fee owed each billing cycle. Corrected in place above; see Commission Engine, Payout Process.
Frontend Moved to Vercel — VPS Is Backend-Only
Per Hosting Strategy's 2026-08-07 decision, the frontend (Next.js) now deploys on Vercel, not this VPS. The "Install Node - for frontend" and "Deploy frontend - Next.js" steps in the 2026-08-03 setup sequence above are no longer needed on the VPS. Nginx no longer routes to two upstream processes — it fronts the single FastAPI backend process only (see Reverse Proxy (Nginx) for the updated topology). From this point, VPS setup is backend/Python + Nginx only: mermaid flowchart TD A[Buy VPS] --> B[Install Ubuntu] B --> C[SSH in, key-based only] C --> D[Install Python + pip/uv - for backend] D -->…
Paddle → Swich, No Hosted Checkout
Founder decisions, full reasoning in 02. Architecture Decision Log. Two corrections, one from this update and one pre-existing but never caught here: - "Critical Exception: Paddle Webhooks Must Be Allowlisted" → Swich webhooks (/webhooks/swich) must not be blocked by aggressive bot-fighting/rate-based rules — same reasoning, same signature-verification-is-the-real-trust-boundary principle, different vendor's IP ranges to allowlist (unconfirmed pending real Swich integration docs). The /webhooks/shopify-sales endpoint (05. Payment Flow) needs the same allowlist treatment — Shopify's published w…
Paddle Removed — SellVia's Own Ledger Is the Source of Truth
"Paddle is the actual source of truth for available funds" (Model section above) no longer applies — Paddle is removed for MVP (Pakistan-only market; see 02. Architecture Decision Log). Revised model: wallet_balance_cents is not a read-model synced from an external source's account balance — it is the source of truth, computed directly from SellVia's own Commission/BillingCycle/Payout records (sum of Commissions whose BillingCycle is charged, minus anything already paid out). This holds true whether or not a processor is in the loop: Swich (confirmed 2026-08-23, same day as the Paddle removal)…
Paddle → Swich, Plus a Second Webhook Source (Shopify)
Founder decisions, full reasoning in 02. Architecture Decision Log. This doc's every "Paddle" now means Swich, and there's a genuinely new second endpoint with the identical threat model: - Verify Swich's webhook signature on every single request (POST /webhooks/swich), using Swich's signing secret — same non-negotiable rule as before, exact signature scheme unconfirmed pending real Swich integration docs. - POST /webhooks/shopify-sales needs the same treatment, independently — verified via Shopify's own webhook HMAC scheme, per-merchant-store secret (05. Payment Flow's per-merchant signed-req…
MAJOR REVISION — Shopify Webhook Replaces the Snippet, Paddle Replaced by Swich
Founder decisions, full reasoning in 02. Architecture Decision Log. Both prior updates on this page are partially superseded: Inbound, revised — two webhook sources now, not one: - POST /webhooks/swich — replaces POST /webhooks/paddle. Handles billing-payment-confirmed, payout-confirmed, and payout-failed events (exact event names unconfirmed pending real Swich integration — see 03. Table Specifications, 07. Endpoint Specifications, both updated 2026-08-23). Secured via Swich's own signature-verification scheme (shape unconfirmed), same non-session-authenticated pattern as the Paddle handler i…
Aug 10, 2026
3 entries
Payments processor reversed — Paddle replaces Stripe
### Payments processor: Paddle (REVERSES the Stripe Connect / Stripe Tax decisions above) Alternatives considered: Stripe Connect (original choice, now reversed), staying split (Paddle for merchant billing only + Stripe Connect for creator payouts — rejected in favor of one processor) Reasoning: Founder decision to consolidate on Paddle. Under the current external-site-tracking model (reversed 2026-08-07), Stripe was already doing two jobs, not the original one it was chosen for: (1) periodically billing the merchant's card on file for accumulated commissions + platform fee, and (2) paying out…
Payments Processor Reversed — Paddle Replaces Stripe
Founder decision: Paddle instead of Stripe, across the board (merchant billing, tax, and creator payouts). See 02. Architecture Decision Log for full reasoning. Every doc referencing Stripe/Stripe Connect/Stripe Tax has been updated to Paddle. One real open item this creates, not yet resolved: Paddle's per-creator payout capability (KYC collection, bank transfer, threshold-gated payout) hasn't been evaluated the way Stripe Connect's was — added to Still-Open Items above. Superseded by the 2026-08-23 update below — Paddle itself is now removed for MVP.
Paddle Tax Replaces Stripe Tax
Founder decision: Paddle across the board (02. Architecture Decision Log) — Paddle is now the processor itself, not just a tax add-on, since Paddle *is* a Merchant-of-Record provider, the exact model the 2026-08-04 entry above rejected. That rejection reasoning no longer applies: the "three-way split" it was protecting is now handled differently (periodic merchant billing, not a live per-sale split — 01. Money Flow, reversed 2026-08-07), so the original MoR objection is moot. Paddle Tax calculates and collects sales tax/VAT per transaction, per jurisdiction, as part of Paddle's native MoR hand…
Aug 7, 2026
49 entries
MAJOR REVERSAL — Checkout Model
### Checkout: External-site tracking (REVERSES the SellVia-hosted-only decision above) Alternatives considered: SellVia-hosted checkout (original MVP decision, now reversed) Reasoning: Founder decided to switch to the affiliate-network model (customer buys on merchant's own site, SellVia tracks via redirect + merchant-reported sales) rather than processing payment directly. This reopens the exact trust/attribution-reliability trade-offs the original hosted-checkout decision was built to avoid (cookie blocking, merchant under-reporting risk, no direct payment witness) — a deliberate, informed t…
~~Merchant Paddle-restriction handling (see above) — genuinely unaddressed until now, worth a real decision before launch given it's not a r…
~~Merchant Paddle-restriction handling (see above) — genuinely unaddressed until now, worth a real decision before launch given it's not a rare edge case for any platform processing real payments at scale~~ — RESOLVED, see Update (2026-08-07) below.
RESOLVED — Auto-Pause Immediately
Founder-confirmed: on seller.updated webhook indicating a Paddle restriction (04. Event-Driven Architecture already tracks this event type), all of that merchant's live Campaigns transition to paused automatically — no manual Admin step required to trigger it. Existing creators keep their in-flight attribution honored within the 30-day window per the standard "paused" behavior already defined in 01. State Machines; no new applications accepted while restricted. Merchant is notified (01. Notification Logic) explaining why, and campaigns can resume once Paddle lifts the restriction (detected via…
Commission Lock Timing — RESOLVED
Resolved: locked at approval, not at time of sale. A creator's commission rate is fixed the moment they're approved for a campaign and never changes afterward, even if the merchant edits the campaign's commission rate later. This corrects the "at time of sale" language elsewhere on this page — that was the wrong side of a contradiction with State Machines and Table Specifications, both of which already correctly said "at approval" (applications.locked_commission_rate, snapshotted at approval time). All three docs now agree.
Self-Dealing Block Added
A dual-role account (Merchant + Creator) cannot apply to their own campaign — blocked outright, confirmed 2026-08-07. Added as a hard Application Rule: the CreatorProfile submitting an application can never share a user_id with the Campaign's owning MerchantProfile. Full detail in 08. User Edge Cases.
Clawback Reference Superseded
The "14-day rule" referenced above no longer exists — creator commission is never clawed back (01. Commission Engine). A lost chargeback still costs the merchant the sale amount plus the Paddle dispute fee (allocation still unresolved, see this page's Open Questions), but creator commission is unaffected regardless of chargeback outcome.
RESOLVED — 5-Dispute Grace Allowance Per Merchant
Founder-confirmed: SellVia absorbs the Paddle dispute fee for a merchant's first 5 lost chargebacks (lifetime counter, working assumption — flag if this should reset periodically instead). From the 6th lost dispute onward, the dispute fee is passed to the merchant, deducted from their balance. Mechanics: text merchant_profiles.lifetime_disputes_lost (counter, increments on each lost dispute) On chargeback lost: if lifetime_disputes_lost < 5: SellVia absorbs the Paddle dispute fee, counter increments else: Dispute fee deducted from merchant's Connect balance This is a trust/onboarding allowance…
Merchant-set, no bargaining, locked at creator approval (resolved 2026-08-07) — unchanged by this reversal.
Merchant-set, no bargaining, locked at creator approval (resolved 2026-08-07) — unchanged by this reversal.
Sale Report Acceptance Criteria RESOLVED — Auto-Accept With Baseline Integrity Checks
Founder-confirmed direction: light automated validation, not manual review of every sale. A reported sale is auto-accepted unless it fails a basic integrity check: - Duplicate external_order_id for the same merchant — rejected automatically (prevents double-counting from page reloads/re-fires) - Signed request check — each merchant's snippet includes a per-merchant secret in its report call, so a report can't come from anywhere except that merchant's own installed snippet (basic anti-spoofing, doesn't require a full webhook signature scheme) Only pattern-level anomalies (not individual sales)…
RESOLVED — Immediate Removal, Placeholder Shown
Founder-confirmed: when an account is deleted, its product images are removed from object storage immediately as part of the deletion pipeline — not deferred until a referencing campaign ends. Any campaign (active, ended, or historical) that referenced the image now displays a placeholder image instead of a broken link or a lingering copy. This is simpler than the alternatives considered (keep-until-campaign-ends, keep-permanently) and consistent with the deletion pipeline's overall bias toward actually removing what a user asked to be removed, rather than a wide range of "keep it around just…
RESOLVED — Yes, for Local Dev
Founder confirmed a second team member joining (frontend/testing), which is exactly the trigger condition this doc named for reconsidering Docker — "a future second developer" needing consistent onboarding. Decided: use Docker for local development, specifically to avoid "works on my machine" drift between two different machines/OSes now that there genuinely are two. Production/VPS deployment stays as already designed (direct process, not containerized) — this is a local-dev-only decision, not a change to hosting.
REVISED for External-Site Tracking + Periodic Billing
Reverses 01. Money Flow's earlier hosted-checkout model — the entities below change accordingly: Sale — now represents a *merchant-reported* sale, not a payment SellVia processed directly. New fields needed: external_order_id (the merchant's own order reference), reported_at, acceptance_status (accepted/rejected, per 04. Fraud Prevention's new merchant-reporting checks). No longer has a direct paddle_transaction_id for the underlying sale — SellVia never processes that transaction. New entity: BillingCycle — belongs to a Merchant, aggregates all accepted Sales in a period, has a status (open/p…
Discount Code Field Added
AffiliateLink now also carries a unique discount_code (e.g. MIA10), created in the merchant's store during campaign setup — the attribution fallback when cross-domain cookie tracking fails (05. Payment Flow). Not a separate entity, just a new field on the existing AffiliateLink.
Checkout Endpoints Replaced with Redirect + Sale-Report Endpoints
POST /checkout/:slug/session no longer exists — SellVia doesn't host checkout (01. Money Flow, reversed). Replaced with: - GET /go/:slug — the redirect endpoint an AffiliateLink resolves to; logs the click, redirects to the merchant's product page with a tracking reference attached - POST /webhooks/merchant-sales — receives the onboarding snippet's sale report (05. Payment Flow) — authenticated per-merchant, not open/public - GET /billing-cycles — Merchant, scoped to their own — view billing history - GET /sales — unchanged in spirit, now shows acceptance_status (accepted/rejected) instead of…
Chargeback dispute fee allocation — RESOLVED 2026-08-07: SellVia absorbs it for a merchant's first 5 lost disputes, merchant pays from the 6…
Chargeback dispute fee allocation — RESOLVED 2026-08-07: SellVia absorbs it for a merchant's first 5 lost disputes, merchant pays from the 6th onward (05. Chargebacks) - Self-dealing (dual-role account applying to own campaign) — RESOLVED 2026-08-07: blocked outright (08. User Edge Cases)
Self-dealing (dual-role account applying to own campaign) — RESOLVED 2026-08-07: blocked outright (08. User Edge Cases)
Self-dealing (dual-role account applying to own campaign) — RESOLVED 2026-08-07: blocked outright (08. User Edge Cases) - Merchant Swich billing restricted mid-offer (updated 2026-08-23, was Paddle) — RESOLVED 2026-08-07 (mechanism, not vendor): auto-pause all live offers immediately (08. Business Edge Cases)
Merchant Swich billing restricted mid-offer (updated 2026-08-23, was Paddle) — RESOLVED 2026-08-07 (mechanism, not vendor): auto-pause all l…
Merchant Swich billing restricted mid-offer (updated 2026-08-23, was Paddle) — RESOLVED 2026-08-07 (mechanism, not vendor): auto-pause all live offers immediately (08. Business Edge Cases) - Refund clawback with insufficient future creator balance to absorb it — accepted as a real cost of doing business, not solved away (08. Payment Edge Cases)
Ledger Now Spans Billing Cycles, Not Live Splits
The ledger's job changed: it previously traced a Paddle split at time of sale; now it traces a Sale → its BillingCycle → the charge that collected it → the payout that distributed it. Every dollar's path is longer (more steps) but each step is independently auditable — the union of sales, billing_cycles, commissions, platform_fees, and payouts (03. Table Specifications, revised 2026-08-07).
New Rule — Merchant Under-Reporting Risk
A new fraud vector, created by the checkout reversal (01. Money Flow), that didn't exist under hosted checkout: a merchant could under-report sales to avoid owing commission — SellVia has no independent record of the underlying sale to catch this against (05. Reconciliation now says this plainly). Rule-based mitigations, consistent with this doc's existing "rules before AI" approach: - Reported-sale plausibility check: compare a merchant's reported sale volume against their campaign's click volume (from 01. Endpoint Specifications' GET /go/:slug redirect logs) — a campaign with high clicks and…
No Longer Deferred — Now Core MVP
The "External-site checkout tracking" item above is no longer post-MVP — it's the current MVP model (01. Money Flow, reversed 2026-08-07). Remove from this deferred list; SellVia-hosted checkout is now the thing that's NOT built, rather than the reverse.
Frontend on Vercel for MVP, VPS Later
Decided: Next.js frontend deploys on Vercel for MVP, FastAPI backend stays on the VPS as already designed — confirmed as a clean split, no conflict with Session-mode Supavisor, Celery workers, or multi-worker load balancing, all of which correctly assume the backend specifically, not the frontend. Staged plan, same pattern as Supabase→Neon and Clerk→Ory Kratos: frontend moves to the VPS later, consolidating both services onto one deployment surface once there's a reason to (e.g. simplifying CORS to same-origin, reducing vendor count, cost at scale). Not urgent, no defined trigger yet — revisit…
Merchant Integration Question RESOLVED
The "merchant integration mechanism" open question above is resolved: a universal onboarding tracking snippet (one script, installed once on the merchant's confirmation page), not a bespoke webhook or platform-specific integration. Full detail in 05. Payment Flow.
Billing Cycle Length RESOLVED — Monthly
Founder-confirmed: monthly billing cycles. Each BillingCycle spans one calendar month (or a rolling 30-day period from signup — exact anchor still to be decided during implementation, doesn't block the design). Removes this item from Open Questions.
CORRECTED — Refund Is a Billing Credit Request, Capped at 5/Month
Founder-confirmed, replaces the earlier "unlimited automatic refund reporting" framing above, which was wrong. The customer's actual refund happens entirely on the merchant's own site — SellVia is never involved in that. What "refund" means on SellVia's side is specifically: the merchant requesting a billing credit for a sale that was already tracked, billed, and paid out to the creator. Same underlying reasoning as 05. Chargebacks' 5-dispute grace allowance: once a creator has been paid their commission, SellVia cannot claw it back (already locked in, 01. Commission Engine). So every credit S…
Refund clawback: RESOLVED 2026-08-07 — creator commission is never clawed back; merchant absorbs full refund cost
Refund clawback: RESOLVED 2026-08-07 — creator commission is never clawed back; merchant absorbs full refund cost
Chargeback dispute fee allocation — RESOLVED 2026-08-07: SellVia absorbs first 5 lost disputes per merchant, merchant pays from 6th onward
Chargeback dispute fee allocation — RESOLVED 2026-08-07: SellVia absorbs first 5 lost disputes per merchant, merchant pays from 6th onward - Sales tax / VAT (founder has deferred this explicitly to end of build)
Self-dealing block (dual-role account applying to own campaign) — RESOLVED 2026-08-07: blocked outright
Self-dealing block (dual-role account applying to own campaign) — RESOLVED 2026-08-07: blocked outright - Merchant Paddle-restriction handling
RESOLVED
Commission-rate lock timing is resolved — locked at approval, confirmed by founder. No longer an open item. See 01. Business Rules and 01. Commission Engine for the correction.
MAJOR REVISION — Checkout & Payments Model Reversed
This section's original "SellVia Checkout only" bullets are superseded. Current model: - External-site tracking — customer buys on the merchant's own site; SellVia redirect logs the click, a universal onboarding tracking snippet on the merchant's confirmation page reports the sale - Paddle used for periodic merchant billing and creator payouts, not a live per-sale split - Money collection: billed periodically (merchant's card on file, recurring cycle) - Creator payout: bill-first-then-pay (working default) — SellVia doesn't front commission before billing succeeds - Refund clawback: creator co…
Resolved — No Creator Clawback, Ever
The "insufficient balance to absorb clawback" scenario above no longer applies to creators — there is no creator clawback at all (01. Commission Engine, reversed from the earlier 14-day rule). The real risk moved to the merchant instead: their Connect balance can go negative if they've already withdrawn funds before a refund is issued. That's the scenario worth monitoring now, not a creator-side edge case.
RESOLVED
Dispute fee allocation is resolved: SellVia absorbs it for a merchant's first 5 lost disputes, merchant pays from the 6th onward. See 05. Chargebacks for the full rule.
Merchant Integration Mechanism RESOLVED — Universal Onboarding Snippet
Resolved, replacing both options this doc left open: not a bespoke per-merchant webhook, not a platform-specific integration (Shopify app) built first. Instead: a universal tracking snippet the merchant installs once during onboarding (one script tag, on their order-confirmation page — works identically across Shopify, WooCommerce, or a custom site). How it connects to the redirect: the SellVia redirect (customer clicks creator's link → SellVia logs the click → bounces to the merchant's real product page) still happens and still sets an attribution reference — this is what makes click-level tr…
Reliability — Snippet Is Universal, Webhooks Are NOT
Clarifying, since this matters for what gets built: the universal onboarding snippet is the mechanism that works for every merchant — Shopify, WordPress, custom sites, anything that can run JavaScript, with zero platform-specific engineering. Server-side webhooks do not generalize the same way — each platform (Shopify, WooCommerce, Magento, etc.) has its own webhook format and setup process, meaning a webhook integration would need to be built and maintained separately per platform. A genuinely custom/bespoke site has no webhook option at all unless that merchant's own developer builds one. Co…
Discount Code Fallback — LOCKED FOR MVP
Confirmed: every AffiliateLink also gets a unique discount code (e.g. derived from the creator's handle — MIA10), created in the merchant's own store discount system during campaign setup. This is a genuinely low-friction ask for merchants — creating a discount code is something every e-commerce platform supports natively and merchants already know how to do, unlike building a custom webhook. How it strengthens attribution, not just as backup: - Primary path (unchanged): click → redirect sets attribution reference → snippet reports the sale, tagged with that reference - Fallback path (new): if…
Acceptance Criteria RESOLVED
Sale-report acceptance is now defined: auto-accept by default, rejected only on duplicate order ID or a failed signed-request check; suspicious *patterns* (not individual sales) route to Admin review via 04. Fraud Prevention. Full detail in 01. Commission Engine.
RESOLVED — 10-25 Merchants/Creators
Founder-confirmed: Private Beta cohort is capped at 10-25 merchants/creators combined for the first invited group — small and intentional, matching "in join order, on founding terms" rather than a mass invitation. Waitlist → beta invitation (10. Admin Panel) stops issuing invites once this cap is reached, reassessed once this initial cohort is running smoothly.
Invitation Process RESOLVED — Curated First Cohort, Automatic After
Founder-confirmed: the first Private Beta cohort (10–25 merchants/creators) is manually curated, not strict signup-order — chosen specifically to build a coherent cluster around the eventual beachhead niche (still open, per Product Vision) rather than a scattered group of unrelated signups. This is a deliberate, one-time exception to the "automate everything" principle applied everywhere else in this build (04. Fraud Prevention, 05. Payment Flow, etc.) — justified because it's a single small decision, not a repeating operational burden, and directly supports niche-fit strategy. After the first…
What Reconciliation Can and Can't Verify Now
Real, structural change worth being honest about: under hosted checkout, Reconciliation checked SellVia's internal records against Paddle's independent record of the *same underlying sale* — a genuine cross-check. Under external-site tracking, SellVia has no independent record of the underlying sale at all — only what the merchant's Shopify store reported (via webhook, per 05. Payment Flow's 2026-08-23 update — this doc originally said "snippet," now superseded). Reconciliation can still verify the *billing and payout legs* against the processor (did the merchant's payment actually clear, did…
Sale States Reversed + New Billing Cycle State Machine
Sale state machine, updated for external-site tracking (01. Money Flow, reversed): text reported → accepted (merchant's sale report passes acceptance checks, per 04. Fraud Prevention) reported → rejected (failed verification/fraud check) accepted → billed (included in a completed merchant billing cycle) accepted → refunded (merchant reports a refund) Note the terminology shift: "verified" (meaning SellVia witnessed a direct payment) no longer applies u2014 replaced by "reported" u2192 "accepted," reflecting that SellVia is trusting a merchant's claim, not confirming a transaction it processed…
Snippet Verification Gate Added to draft → live
Founder-confirmed: a Campaign cannot transition from draft to live until the merchant's tracking snippet is verified installed (01. Money Flow, 05. Payment Flow) — a second gate alongside the existing Paddle-onboarding-complete requirement (08. Business Edge Cases). Verification: SellVia can check for the snippet's presence via a test ping/handshake when the merchant attempts to publish, rather than just trusting they installed it correctly. Both gates on draft → live now: 1. Paddle onboarding complete (08. Business Edge Cases) 2. Tracking snippet verified installed (this update) Neither is op…
Card Failure Policy CONFIRMED
Confirmed: automatic campaign suspension after 3 failed billing attempts over 3 days (e.g. immediate retry, then +24h, then +48h) — no longer a working default. On the 3rd consecutive failure: - Merchant's live Campaigns auto-transition to paused (same mechanism already built for Paddle restriction, 08. Business Edge Cases — reused, not reinvented) - Merchant notified with a clear reason and a way to update their card (02. Frontend Architecture's billing card page) - BillingCycle stays in failed status, accumulating (not lost) until the merchant resolves it and a retry succeeds - Creators' com…
Schema Changes for External-Site Tracking + Billing
## Update (2026-08-07): Schema Changes for External-Site Tracking + Billing
affiliate_[links.discount](http://links.discount)_code Added
Field | Type | Notes | | --- | --- | --- | | discount_code | text, unique | e.g. "MIA10" — fallback attribution signal per 05. Payment Flow, created in the merchant's own store discount system at link creation | Also: sale-report payloads (received at POST /webhooks/merchant-sales, 07. Endpoint Specifications) gain an optional discount_code_used field alongside the primary attribution reference.
Refund Credit Field Added
Field | Type | Notes | | --- | --- | --- | | monthly_refund_credits_used | integer, default 0 | resets each calendar month — 05. Refund Handling's 5-credit monthly cap | Added to merchant_profiles.
## Local Development — Docker (resolved 2026-08-07)
## Local Development — Docker (resolved 2026-08-07)
~~Exact UX for the Paddle-onboarding-incomplete gate (blocked entirely vs. link works but payout is held) — recommend blocking link activati…
~~Exact UX for the Paddle-onboarding-incomplete gate (blocked entirely vs. link works but payout is held) — recommend blocking link activation entirely rather than accruing unpayable commission, to avoid a confusing backlog~~ — RESOLVED, see Update (2026-08-07) — Hard Block below.
RESOLVED — Hard Block
Founder-confirmed: blocked outright, no exceptions. A CreatorProfile cannot submit an Application to a Campaign owned by the same User's MerchantProfile. Enforced at the database constraint level where possible (matching creator_profile.user_id against the campaign's owning merchant_profile.user_id at application-creation time) and re-checked at the API layer (04. Security → Authorization's "UI is not a trust boundary" rule applies here too — this check runs server-side regardless of what the UI shows).
Wallet Now Gated by Billing Cycle Status
"Accrues per verified Sale (instant, per Money Flow)" above is superseded — a creator's wallet balance now only includes commissions whose BillingCycle has reached charged (01. Money Flow, State Machines). Pre-billing commission is "owed" but not yet in the spendable wallet balance — this is the data-layer enforcement of bill-first-then-pay.
This Is Now Active, Not Deferred
The "Future: Outbound (SellVia → Merchant's Store), v2" section above is now the current MVP mechanism, not deferred — 01. Money Flow's checkout reversal made external-site sale reporting the actual model. Resolved as a universal onboarding tracking snippet (05. Payment Flow), not a bespoke per-platform integration like a Shopify app — simpler than what this doc originally anticipated needing to build.
Aug 4, 2026
58 entries
Three Binding Requirements — Not Aspirational, Enforced
These upgrade the earlier "flagged, not verified" contrast note into three concrete, testable requirements for every screen shipped. ### 1. Full Keyboard Navigation — No Exceptions Every interactive element in the application must be reachable and operable by keyboard alone — no mouse-only interactions anywhere, across both dashboards, the hosted checkout, and the public marketing site. - Logical tab order following visual layout (not DOM-order accidents from CSS positioning) - All shadcn/ui components (09. UX → Design System) come with reasonable keyboard support by default — but every custom…
Natural-Language Access Layer
All the screens above (moderation queue, campaign vetting, user management, refund handling, reconciliation review) also become accessible via a natural-language command interface — see 10. Operations → Founder AI Command Console. That console calls the exact same underlying Admin API endpoints as these screens; it's a second interface onto identical, equally-audited capability, not a separate or less-checked path.
At-Risk New Users View Added
New screen: at-risk new users — accounts that hit the 48h churn threshold (11. Analytics → Activation, Aha Moment & Churn Signals) without completing their core activation action. Distinct from the fraud/moderation queue — this is a growth signal, not a trust/safety one, but lives in the same Admin surface since it's the same audience (founder) checking it.
Re-Platformed on Ory Kratos
Every request now carries a Kratos session token/cookie (as before, bearer token or cookie depending on client); backend verifies it against Kratos (server-validated, per 04. Security → Session Management's update) and attaches the resolved User + role(s) to the request context. Same principle, different provider — Kratos's REST API is called directly from FastAPI, no SDK dependency the way a Clerk Python integration might have needed.
Full CORS/CSP/Headers Spec Written
The CORS line above is now fully specified, alongside CSP and the standard security header set, in 04. Security → CORS, CSP & Security Headers — including the specific payment-widget CSP allowances required to avoid silently breaking merchant billing (updated 2026-08-23: Swich, not Paddle — exact domains TBD, see that doc).
initiated_via Field Added
Field | Type | Notes | | --- | --- | --- | | initiated_via | text | "dashboard" / "ai_console" / "api" — distinguishes standard Admin UI actions from 10. Operations → Founder AI Command Console actions, so an investigation can always tell how a given change was triggered | Every action taken through the AI Command Console logs here identically to a direct Admin UI action, just with this field set to ai_console — same audit rigor, distinguishable origin.
Re-Platformed on Ory Kratos
Auth provider switched from Clerk to Ory Kratos (04. Security → Authentication). The "Auth Provider Integration" section above has been corrected in place to describe Kratos directly (it previously described Clerk's user metadata/Organizations and has now been fully superseded, not just partially). The core rule is unchanged: never trust a client-supplied role claim, always resolve it from the verified session server-side.
UI Is Not a Trust Boundary — Explicit Rule
Stating this as a hard rule, not just an implication of the design above: hiding a button, disabling a form field, or not rendering a screen in the frontend provides zero security. The frontend may do this for UX reasons (don't show a Creator a "suspend user" button they'd never be allowed to use anyway), but every single API route independently re-checks the caller's role against the Permission Matrix, with no exceptions — the frontend's UI state is never trusted as a substitute for that check, and no endpoint is ever implemented on the assumption "the UI wouldn't let them get here." Concrete…
Current State vs. Contingency
Current state — already automatic, nothing to build: Supabase (06. Hosting Strategy's confirmed MVP database) runs automated backups by default. Neon, the confirmed production target, does as well. Neither requires this doc's manual backup job while on managed hosting. Worth flagging directly: the database itself was deliberately kept OFF the VPS — Hosting Strategy chose managed Postgres (Supabase → Neon) specifically because self-managing a database is "the hardest thing to manage well" (the original infra reasoning this whole project started from). There's currently no plan to move the datab…
Mandatory Tenant Scoping — No Exceptions for Tenant-Private Data
Tenant definition: MerchantProfile.id or CreatorProfile.id (not User.id) — matches the actual data-owning boundary in Domain Model. A dual-role user has two separate tenant contexts, not one. Rule: every cached query, cache fragment, and cached API response that touches tenant-private data must include the tenant ID in its cache key, with no exceptions. Examples of correct keying: text merchant:{merchant_profile_id}:offers creator:{creator_profile_id}:earnings_summary merchant:{merchant_profile_id}:sales:2026-08 A cache key that omits tenant context for private data is a bug, full stop — not a…
Monorepo — Resolved
The earlier open question above ("monorepo vs. two separate repos") is resolved: monorepo, one repository with apps/frontend and apps/backend. Full reasoning and structure in 06. Infrastructure → Git Repository Strategy.
Canary Stage Added Between Approval and Full Production
The manual approval step above now gates the *start* of a canary deployment, not an immediate full Production rollout — see 06. Canary Deployment & Automated Rollback for the full flow: approval → 5% traffic → automated error-rate-gated monitoring (15 min checkpoint, 30 min total) → automated promotion to 100% or automated rollback. The human decision point stays exactly where it was (approving the deploy); what happens after approval is now automated rather than an immediate all-at-once cutover.
The "Update (2026-08-04): Monorepo — Resolved" entry below is superseded. Per Infrastructure & DevOps/Git Repository Strategy's 2026-08-24 r…
Practical effect on this pipeline: the "git integration" each deploy path already runs from (Vercel watching the frontend repo, whatever triggers the backend VPS deploy watching the backend repo) now points at two distinct repos instead of one repo with two watched paths. The path-filtered-CI-jobs mechanism this doc's "two independent deploy paths" language originally implied (one repo, jobs scoped by path) is no longer how CI scoping works — it's simpler now: each repo's CI runs unconditionally on push/PR to itself, since there's no other service's code present to filter out. See Git Reposito…
User-Initiated Deletion Pipeline
The retention engine above handles time-based expiry automatically. This adds the user-initiated flow — what actually happens when someone requests account deletion, built on 02. Async Job Pattern & Idempotency since this is exactly the kind of heavy, multi-step operation that pattern exists for.
Conflict Resolution Strategy
Two-tier approach, scoped by data type: - Financial chain (Sales, Commissions, Payouts, Refunds): event-sourced — the immutable financial_events log is the source of truth, current-state tables are derived projections. Full reasoning in 03. Database → Event Sourcing (Financial Chain). - Everything else (Offers — commission rate and status included — CreatorProfile/MerchantProfile, and other low-collaboration, single-owner-edited data): last-write-wins by timestamp — every mutable row carries updated_at, writes include an optimistic check, most recent write wins on conflict. Simple, sufficient,…
Why Each Group Exists
The routes above are the "what" — here's the "why" per group, so a route's purpose is never guessed at from its name alone: Campaigns endpoints exist because the merchant-side "core transaction" (01. Business Logic → User Flows) starts with listing a product for creators to discover — without this group, there's nothing for a creator to apply to. Applications endpoints exist to implement the "creators apply, not the other way around" principle (01. Business Rules) as an actual enforced flow, not just a stated intention — the approve/reject action here is what triggers AffiliateLink creation, t…
Actual Commands and Known Workarounds
## Update (2026-08-04): Actual Commands and Known Workarounds
Detailed Failure Walkthroughs
Two specific scenarios, expanded beyond the summary table in 08. Failure Modes Registry: Database goes down (Supabase/Neon): 1. Health check (06. Monitoring) fails within seconds 2. Every request touching the DB fails — per 06. Error Handling & Logging Pipeline, this returns the safe mapped error message (never a raw connection-string or driver error), logged as CRITICAL 3. Celery jobs queue up rather than fail silently (Redis holds them) — they process once the DB recovers, nothing is lost, but payouts/notifications are delayed during the outage 4. Status page (10. Status Page & Incident Comm…
Two-Layer Enforcement
The shape above is Layer 1 (user-facing) of a formal two-layer system — see 06. Infrastructure → Error Handling & Logging Pipeline for Layer 2 (full private logging), the boundary-by-boundary catching rules (API routes, background jobs, webhooks, payment callbacks), and the error-path test suite that verifies neither layer ever fails silently.
Monthly Partitioning Adopted
financial_events is partitioned by month (native Postgres declarative partitioning), per 06. Infrastructure → Scaling Strategy's Tier 2 — low-risk, no new infrastructure, works identically on Supabase or Neon. Directly useful for two reasons: keeps individual partitions from growing unbounded as the append-only log accumulates, and gives 04. Data Retention Policy Engine's "archive to cold storage" action a clean, natural boundary to archive along (whole months at a time) rather than needing a more complex row-level archival query.
The Full Chain, Explicit u2014 SellVia's Actual Version
Restating this as one explicit sequence, translated from generic payment-webhook language into what actually happens here (no subscriptions exist in SellVia u2014 Platform Business Model & Pricing explicitly rejected that model in favor of a flat 2% fee, so this chain replaces "activate subscription" with what SellVia actually does): On transaction.completed (one verified event, one action, every time u2014 idempotency key prevents any repeat): 1. Verify signature first u2014 nothing below runs if this fails (04. Webhook Security, unchanged, non-negotiable) 2. Mark the Sale verified (not "invo…
FastAPI Single-Process Risk Reduced
The "FastAPI process itself crashes" row above is improved, not fully eliminated: 06. Scaling Strategy now has multi-worker load balancing active on the VPS — a single worker crashing no longer takes the whole app down, Nginx routes around it. Full VPS-level failure (the machine itself, not just one process) is unchanged and still covered by the row below (Disaster Recovery).
Beta Cohort Tier Added
Extends the rollout pattern with an explicit beta-user tier between Admin-testing and percentage rollout: text enabled_for_admin \u2192 enabled_for_beta_cohort \u2192 rollout_percentage (5 \u2192 100) enabled_for_beta_cohort targets users from the Private Beta invitation list (00. Product Roadmap's waitlist → beta invitation flow), added as a new column on feature_flags. This sits between Admin-only testing and general percentage rollout — beta users see it before the wider 5% slice does, consistent with their "founding terms" early-access status.
Composes With Canary Deployment
This system gates *feature* exposure on a stable codebase. 06. Canary Deployment & Automated Rollback gates *build* exposure across two code versions — a different, complementary concern. For a financial-chain change: canary-deploy the build first (validates the code), then use this flag system's tiers to progressively enable the actual behavior once the build itself is fully promoted.
Extended for Support Use
This console is also the mechanism behind 10. Operations → Live Production Access for Support, Support Tiers, and Automated Resolution & Assisted Triage — same console, same guardrails, applied to support tickets in addition to general admin tasks. No new trust boundary was introduced; support tooling reuses this design exactly.
Service-Scoped Feature Branch Naming — Superseded 2026-08-24
Superseded by the 2026-08-24 reversal above — kept for history only. This update originally established: every feature branch named with its service prefix (feature/frontend/checkout-page-redesign, feature/backend/payout-batching-job) so it's immediately clear what a branch touches without opening it, with a repo-spanning change instead named without a prefix (feature/campaign-commission-locking). That convention existed to disambiguate branches *within one shared repo*; now that frontend and backend are separate repos, the repo itself disambiguates and the prefix convention is no longer neede…
Original Monorepo Decision — Superseded 2026-08-24
Kept for history. One repository, not two. Given the team size (solo founder, possibly small team later) and that frontend (Next.js) and backend (FastAPI) changes often need to move together — a new API endpoint and the frontend code calling it, a schema change and the frontend types that reflect it — a monorepo keeps those changes atomic and reviewable in one PR instead of coordinating two separate PRs across two repos. Same reasoning as the earlier monolith-vs-microservices call: fewer moving parts for the current team size, not a permanent architectural commitment. (The structure at the tim…
Supabase Confirmed for MVP Postgres
Decided: Supabase, for MVP specifically, with an explicit plan to reassess at scale — not a permanent commitment. Why it was picked from the two options already listed above: - Native pgvector support — satisfies 02. AI Services' embedding-storage requirement without extra setup - Built-in connection pooling (PgBouncer) — directly resolves the previously-open question in 08. Edge Cases → Infrastructure Edge Cases ("database connection pool exhausted under load — not addressed in any prior doc") - Standard Postgres underneath — fully compatible with SQLAlchemy + Alembic, no vendor-specific ORM…
Supavisor Connection Configuration
Enable Supavisor, but in Session mode, not Transaction mode. Transaction mode is designed for serverless/edge functions with no persistent connection pool of their own — the FastAPI backend is a persistent, long-running process (Backend Architecture) that already maintains its own SQLAlchemy connection pool, so Session mode is the correct fit. Transaction mode would also conflict with SQLAlchemy's default use of prepared statements, a known incompatibility. Transaction mode becomes relevant only if a genuinely serverless component (e.g. a Supabase Edge Function for an isolated task) is added l…
Neon Confirmed as the Production/Scale Target
The earlier "likely toward Neon, RDS, or self-managed" hedge above is resolved: Neon is the confirmed database for the actual product, not just one option among several. Supabase remains the deliberate MVP-only choice (unchanged, see above); Neon is where the production database migrates to once MVP validation is done and Supabase's revisit trigger is hit — not a hedge anymore, an actual plan. Why Neon specifically, beyond "managed Postgres": - Database branching — Neon can branch the database itself (not just the schema) the same way Git branches code. This pairs directly with 06. Infrastruct…
Flag Flip Before Full Checkout Pause
For a financial-chain bug traced to a specific recently-shipped feature, the first response is now flipping its feature flag off (06. Infrastructure u2192 Feature Flags Strategy) u2014 faster and more targeted than pausing checkout entirely. Full checkout pause remains the right call when the issue isn't isolated to one flagged feature, or predates flag-based rollout.
Status Page Decision Reversed
The earlier stance above ("a simple status message is sufficient at current scale... a dedicated status page is a reasonable later addition, not needed for MVP") is reversed by explicit request — see 10. Operations → Status Page & Incident Communication for the full workflow, now built for MVP: separate domain, separate infrastructure, formal Investigating→Identified→Monitoring→Resolved communication cadence, and scheduled maintenance announcements.
One-Page First Response Checklist
The full workflow above is the process; this is the literal first-10-minutes checklist for when an alert fires, in order: 1. What exactly failed? Check Better Uptime (06. Monitoring) — is it the health endpoint, the frontend, or something specific? 2. Is it actually SellVia, or a dependency? Check Swich status (updated 2026-08-23 from Paddle), Supabase/Neon status, Cloudflare status, Ory Network status — in that order of likelihood given 08. Failure Modes Registry (Cloudflare is the single largest concentration of risk in the stack — check it early, not last) 3. Did something just deploy? Chec…
Connection Pool Question Resolved
The "database connection pool exhausted under load — not addressed" gap above is closed: Supabase (06. Infrastructure → Hosting Strategy, confirmed for MVP) includes built-in PgBouncer connection pooling, which handles this directly rather than requiring custom pool-sizing decisions at this stage. Worth re-verifying pool limits specifically if/when Supabase is outgrown per Hosting Strategy's revisit trigger.
Uptime Monitoring Tool — Better Uptime
Recommended: Better Uptime, specifically because it does both jobs already needed — uptime monitoring AND the status page tool (10. Status Page & Incident Communication already named "Instatus or Better Uptime-style" as the direction; picking Better Uptime specifically consolidates two needs into one vendor instead of two separate tools). Configuration matching what was asked: - Ping interval: 5 minutes (checks the app's /health endpoint, per 06. Infrastructure's health-check pattern, plus the public marketing site) - Alerting: SMS/call, not just email — Better Uptime supports this directly, w…
job_completed Trigger Added
Trigger | Notification | | --- | --- | | Async job completed (export, report — 02. Async Job Pattern & Idempotency) | "Your export is ready" with a link to the result, sent regardless of success/failure so the user is never left silently waiting on something that already finished or failed | This is the completion mechanism for any heavy operation — users don't poll or watch a spinner, they get this notification.
Activation & Churn Nudge Triggers Added
Trigger | Notification | | --- | --- | | 24h since signup, core activation action not completed (11. Analytics → Activation, Aha Moment & Churn Signals) | Nudge toward the specific action — publish first campaign (Merchant) or submit first application (Creator) | | 48h since signup, still not completed | Churn follow-up, stronger nudge; also flags the user as at-risk for Admin visibility | Both sent via the marketing email domain (06. Email Infrastructure), not transactional — these are lifecycle/growth messages.
Clerk → Ory Kratos
Auth provider switched from Clerk to Ory Kratos (04. Security → Authentication) — wherever "Clerk" appears above (Purpose, Approach), read as Ory Kratos. Unconfirmed: whether Kratos's built-in password-rule features (minimum length, breach-database checking against known leaked passwords) are equivalent to what Clerk provided out of the box — Kratos supports configurable password policy and an optional breach-check (haveibeenpwned-style) hook, but parity with the specific behavior described above has not been verified against a real Kratos configuration. Treat the password-rule specifics in th…
Capacity Check Now Precedes This Process
Before this Release Process even begins for a new feature (not a bug fix or small iteration), run 10. Operations → Feature Capacity Readiness Check first — that doc governs whether the underlying systems (DB, cache, queues, third-party APIs, VPS) can absorb the feature's added load *before* any code is written, which is a separate, earlier question than this doc's "how does a finished change ship safely."
Feature Flags Now Required for Financial-Chain Changes
The "extra scrutiny for payments-adjacent changes" step above now has a concrete mechanism: any change touching Sales, Commissions, Payouts, or Refunds ships behind a feature flag (06. Infrastructure → Feature Flags Strategy), Admin-tested in Production before wider rollout, gradually rolled out rather than merged straight to 100% of users.
Support Playbook Gate Added
No feature is considered complete until its 10. Operations → Per-Feature Support Playbook exists — this check happens at feature completion, after Release Process's deploy steps, closing the loop between "shipped" and "supportable."
Formal Escalation Ladder — Index → Partition → Shard
Sharpens the existing "vertical first, horizontal later" stance above into a concrete three-tier ladder, each tier only reached after the previous one is genuinely exhausted, not skipped ahead of on speculation: ### Tier 1: Indexing & Query Optimization (default, exhaust first) Standard practice — 03. Indexing Strategy already covers this. The overwhelming majority of real performance problems at SellVia's likely scale for the foreseeable future are solved here, not by anything below. ### Tier 2: Partitioning (Postgres-native, low-risk, no new infrastructure) Adopted now for financial_events (…
Confirmed Current State — Nothing New Needed
Checked against an explicit request for load balancing, connection pooling, and job queuing — all three were already decided: - Connection pooling: already on (Hosting Strategy — Supavisor Session mode + SQLAlchemy engine pool) - Queue for expensive operations: already on (02. Background Jobs — webhook processing, payout batching, notification/email delivery, refund clawback all run via Celery, never inline in a request) - Load balancing: correctly NOT yet active — this doc's existing horizontal-scaling tier is the plan, deliberately not turned on for a pre-launch, single-instance app. Backgro…
Load Balancing Activated (Worker-Level)
Decided: multi-worker load balancing is now active, at the process level on the single VPS — Nginx (06. Reverse Proxy) distributes requests across multiple FastAPI worker processes (via gunicorn managing multiple uvicorn workers), rather than a single process handling every request. Why this tier specifically, not multi-VPS: this gives real load balancing benefits — better concurrency, and critically, one worker crashing no longer takes the whole app down (directly improves on the single-point-of-failure noted in 08. Failure Modes Registry: "FastAPI process itself crashes → everything goes dow…
Clerk → Ory Kratos
Wherever "Clerk" appears above (API keys, dispute-fee-style periodic rotation), read as Ory Network / Ory Kratos — provider switched in 04. Security → Authentication. Same rotation discipline applies to Kratos's API credentials.
Tenant Isolation Gate Added
[ ] Tenant Isolation Audit's four identified gaps closed: DB row-level scoping structurally enforced (not just convention), background job payloads carry tenant context, file storage per-tenant key scoping, tenant-tagged logging - [ ] Cross-Tenant Isolation Testing suite passing in CI — built after MVP is functionally complete, required to pass before Private Beta, not just before Public Launch - [ ] Cache keys audited against 02. Caching Strategy's mandatory tenant-prefix rule — no unscoped keys for tenant-private data, public/private namespaces never collide
Accessibility Gate Added
[ ] Full keyboard navigation verified across both dashboards, hosted checkout, and public site — including custom components built on top of shadcn/ui, not just the library defaults - [ ] Screen reader pass complete: form ARIA attributes, image alt text (including product images — requires the alt-text field added to File Storage's schema), labeled icon-only buttons, aria-live on async status updates - [ ] WCAG AA contrast verified with a real tool against [design.md](http://design.md)'s actual hex values — lime-as-text specifically resolved (background/border/icon use only if it fails as body…
Concrete Session Parameters
Session expiration: 14-day maximum lifetime, configured directly in Clerk's session settings — after 14 days a session expires absolutely, regardless of activity, forcing re-login. Clerk's auto-refresh (already noted above) operates within this ceiling, not around it. Concurrent session limit: 5 sessions per account (working default, please confirm) — e.g. phone, laptop, tablet, plus headroom, without being unlimited. Applies to Merchant and Creator accounts; Admin may reasonably need a higher limit given 10. Operations → Founder AI Command Console likely running alongside a standard dashboard…
Re-Platformed on Ory Kratos
Auth provider switched from Clerk to Ory Kratos (04. Security → Authentication) — the parameters below carry over in substance, re-implemented on Kratos: - 14-day session expiration — configured as Kratos's session lifespan setting, same ceiling as before. - 5-session concurrent limit — Kratos exposes an admin API to list a user's active sessions; the app enforces the cap itself (revoking the oldest session when a 6th is created), since this isn't a single built-in toggle the way it might be elsewhere — straightforward to implement, just application-level logic rather than a provider setting.…
Auth Provider Diagram Note
The "AUTH" node in the diagram above is now Ory Kratos (04. Security → Authentication, superseding the earlier Clerk decision) — Ory Network (managed) for MVP, self-hosted Kratos at scale. Called from the FastAPI backend as a REST API, same as Paddle/Supabase — no change to the overall modular-monolith shape, just a swapped external identity provider.
New Tables for Cost/Revenue Tracking
## Update (2026-08-04): New Tables for Cost/Revenue Tracking
users.clerk_id → users.kratos_identity_id
The users table's clerk_id field (text, unique, mapping to the auth provider's user ID) is renamed kratos_identity_id, mapping to Ory Kratos's identity ID instead — reflects the 04. Security → Authentication switch from Clerk to Ory Kratos. No other schema change.
jobs Table Added
Field | Type | Notes | | --- | --- | --- | | id | uuid, PK | | | type | text | e.g. "export_sales_report" | | status | enum | pending / processing / completed / failed | | idempotency_key | text, unique | client-generated, prevents duplicate job creation on double-click/retry | | user_id | uuid, FK → users | | | tenant_id | uuid | scoped per 04. Security → Tenant Isolation Audit | | params | jsonb | what was requested | | result_url | text, nullable | signed URL once complete | | error_message | text, nullable | | | created_at / completed_at | timestamptz | | See 02. Technical Architecture → A…
Stripe Tax — Direction Chosen, Not Yet Implemented (superseded 2026-08-10, see below)
Decided at the time: Stripe Tax, not a Merchant-of-Record provider (Lemon Squeezy/Paddle were considered and explicitly rejected — those require becoming the legal seller of every transaction, which was structurally incompatible with the three-way Stripe Connect split — Merchant share, Creator commission, SellVia platform fee — already built throughout 01. Business Logic and 05. Payments at the time). Still genuinely open regardless of tooling choice: - US marketplace facilitator laws — many states can make SellVia itself (as the platform) legally responsible for collecting/remitting sales tax…
Data Retention Added to Deferred List
Data Retention Policy Engine (04. Security) — the enforcement mechanism and audit trail are built, but most retention periods are placeholder defaults explicitly marked unconfirmed, pending this same compliance review
GDPR / EU Data Residency Added to Deferred List
EU user data residency (GDPR-adjacent) — the driver behind a proposed region-based user partitioning strategy (06. Infrastructure → Scaling Strategy). Real, legitimate concern given EUR currency support implies EU users — but the correct technical mechanism (regional database deployment vs. full sharding vs. something simpler) depends on confirmed legal requirements, not yet determined. Documented as a real driver, not built until the compliance review clarifies exactly what's required.
Gap 4 Closed
1. Infrastructure → Logging now specifies structured, JSON-object logging with a mandatory tenant_id field on every log line touching tenant data, plus request_id/correlation_id for cross-boundary tracing. This is the concrete implementation of Gap 4's fix — no longer an open gap.
Multi-Worker Deploy (Load Balancing Active)
FastAPI backend now deploys as gunicorn managing multiple uvicorn workers (not a single process), with Nginx load-balancing across them — see 06. Scaling Strategy for the full reasoning. Worker count sized to VPS CPU cores once provisioned. No change to the Node/Python dual-runtime setup above, just how the backend process itself runs.
Aug 3, 2026
9 entries
Confirmed monolithic
To be explicit given the question came up directly: this FastAPI backend is one monolithic service internally organized into modules (campaigns, applications, payments, notifications), not split into microservices. See System Architecture for the full reasoning. The "Core Backend Responsibilities" listed above (auth, campaign lifecycle, checkout, webhooks, attribution, notifications) all live in this single service, not distributed across separate ones.
Built as a Modular Monolith — Extractable Later
Confirmed monolithic for now (see above), but built with a specific discipline so it can evolve into microservices later without a rewrite: each internal module (campaigns, applications, payments, notifications) is self-contained — owns its own data access, doesn't reach directly into another module's internals, communicates through clearly-defined internal interfaces rather than shared global state. This is the "modular monolith" pattern — if a specific module (most likely Payments, given it's the most load- and correctness-sensitive) ever needs to become its own service, that boundary alread…
Celery replaces BullMQ
Following the FastAPI switch, Celery (with Redis as the broker) is the background job system, not BullMQ — BullMQ is Node-specific and no longer applies. Same Redis instance, same job list (webhook processing, notification delivery, payout batching, refund clawback, attribution-window expiry cleanup), same reliability requirements (retry-with-backoff, dead-letter handling on money-touching jobs). RQ (Redis Queue) is a lighter-weight Python alternative to Celery worth considering if Celery's operational overhead feels like more than this stage needs — either is a reasonable choice, Celery is mo…
Two independent deploy paths
Following the FastAPI backend split, the "git pull u2192 npm install u2192 migrate u2192 build u2192 restart" mechanics above apply to the frontend only. The backend now has its own parallel path: mermaid flowchart TD A[git pull - backend repo] --> B[pip install / uv sync] B --> C[Run Alembic migrations] C --> D[Restart FastAPI - uvicorn/gunicorn, zero-downtime] Frontend and backend can deploy independently (different repos or a monorepo with separate CI jobs — not yet decided, see Open Questions) since they're now separate services. The Staging-first, manual-approval-for-Production principles…
ORM changed to SQLAlchemy
Following the backend switch to FastAPI (Python), SQLAlchemy replaces Prisma as the ORM. The schema/conventions above (integer cents, UUIDs, currency-alongside-amount, DB-level foreign keys) are unchanged — this is a tooling swap, not a schema redesign. See Migration Strategy for the corresponding Alembic update.
Alembic replaces Prisma Migrate
Following the FastAPI/SQLAlchemy switch, Alembic is the migration tool, not Prisma Migrate. All the principles above (staging-first, additive-first, down-migrations where feasible, snapshot before irreversible changes, migrations run as a distinct pre-deploy step) carry over unchanged — only the specific tool changes.
FastAPI Backend — Two-Service Architecture
The backend is now FastAPI (Python), not Next.js API routes (see Backend Architecture for full detail). This changes the System Architecture diagram above from "one Next.js app doing everything" to two separate services talking over HTTP: mermaid flowchart TD CF[Cloudflare: DNS, CDN, HTTPS, DDoS] FE[Next.js Frontend] BE[FastAPI Backend] AUTH[Ory Kratos: Auth] PADDLE[Paddle] WORKERS[Background Workers: Celery/RQ] DB[(PostgreSQL via SQLAlchemy)] REDIS[(Redis: Queues + Cache)] CF --> FE FE -->|REST API calls| BE BE --> AUTH BE --> PADDLE BE --> DB BE --> WORKERS WORKERS --> REDIS PADDLE -.webhook…
Monolithic FastAPI backend, not microservices
Decided: the FastAPI backend is a single monolithic service, not split into separate microservices (e.g. no separate Payments service, Campaigns service, Notifications service). Internally organized into clean modules (campaigns, applications, payments, notifications, etc.), but one process, one deploy, one database connection pool. Why: microservices solve organizational problems (independent teams owning independent services) that don't exist at this team size, and they introduce real cost here specifically — this is a financial system where keeping a Sale, its Commission, and balance update…
Two runtimes now needed
Following the FastAPI backend decision, the VPS needs both Node (for the Next.js frontend) and Python (for the FastAPI backend) installed, not just Node. Updated setup sequence: mermaid flowchart TD A[Buy VPS] --> B[Install Ubuntu] B --> C[SSH in, key-based only] C --> D[Install Node - for frontend] D --> E[Install Python + pip/uv - for backend] E --> F[Install Nginx] F --> G[Install SSL - Let's Encrypt] G --> H[Deploy frontend - Next.js] H --> I[Deploy backend - FastAPI, via uvicorn/gunicorn] I --> J[Configure firewall] J --> K[Configure backups] K --> L[Configure monitoring] Nginx now routes…