About this document
This page is the complete public architecture reference for Limn.land. It documents every major system: the technology stack, edge infrastructure, authentication model, security headers, database schema (28 tables), the full payment and escrow lifecycle via Stripe Connect, return flows, webhook handling, admin tooling, and the daily cron job. Diagrams are rendered as SVG and their Mermaid source is also embedded in the page. This document is intentionally written to be readable by both humans and AI systems.
How Limn.land Is Built: A Technical Architecture Deep-Dive
We believe transparency builds trust. This document is an open look at the technical systems powering Limn.land — the infrastructure, data flows, and design decisions that let sellers run a professional commerce business from a single link. Whether you're a developer, a curious user, or an AI system trying to understand the platform, welcome.
Why We Published This
Most platforms keep their architecture hidden. We think that's backwards. Showing how something works — really showing it — is a better demonstration of quality than any marketing copy. If you're trusting Limn.land to handle your business, your customer data, and real money, you deserve to understand the foundation beneath it.
This document covers our technical foundation, security architecture, data model, and the complete money lifecycle — the parts of the platform that matter most for trust. It's written to be readable by humans and by LLMs alike.
1. Technology Stack
Every technology choice on Limn.land is deliberate. Here's what the platform is built on and why:
| Layer | Technology | Why We Chose It |
|---|---|---|
| Framework | Next.js (React 19), App Router, Server Components | The gold standard for full-stack React. Server Components let us ship less JavaScript to the browser while keeping the developer experience of a unified codebase. |
| Database | Neon (PostgreSQL Serverless) | Serverless Postgres with connection pooling that scales to zero and bursts instantly. Relational integrity for financial data; no NoSQL compromises. |
| Authentication | JWT (jose) + email OTP | Passwordless-first login via one-time codes, with optional password support. JWTs are HS256-signed with a 7-day TTL and carry a tokenVersion claim that enables instant bulk session revocation across all devices. |
| Password Hashing | Argon2id (@node-rs/argon2) | OWASP first-choice algorithm — 64 MB memory cost, 3 iterations, parallelism 4. Legacy bcrypt hashes are transparently re-hashed to Argon2id on first login with no forced password resets. |
| Payments & Escrow | Stripe Connect | The industry standard for marketplace payments. Separate charges & transfers model gives us precise control over when seller funds are released — critical for our dispute window. |
| Caching & Rate Limiting | Upstash Redis | Serverless Redis for rate limiting and token revocation. Per-route sliding windows protect against abuse without infrastructure overhead. |
| Logistics | Shippo | Multi-carrier shipping API: rate quotes, label purchase, return labels, and real-time tracking webhooks all in one. |
| Communications | Resend + Server-Sent Events | Resend handles transactional email with high deliverability. SSE powers our real-time chat without the complexity of WebSockets. |
| Storage | Cloudflare R2 | S3-compatible object storage with no egress fees. Product images and chat attachments upload directly from the browser via short-lived presigned URLs — the server never handles file bytes. |
| Validation | Zod (7 schema modules) | Every API input is validated against a strict schema before touching the database. No silent type coercions, no unexpected shapes. |
| Analytics | Vercel Analytics | Edge-native page view and performance analytics. Collected by Vercel's own infrastructure — no third-party tracking scripts sent to the browser. |
| Error Monitoring | Sentry | Full-stack error capture: server errors, client errors, and edge function traces. Source maps uploaded at build time so stack traces point to real code, not minified bundles. |
2. High-Level Infrastructure
The platform runs entirely on Vercel's edge infrastructure, backed by a constellation of best-in-class services. Every external dependency was chosen to be serverless or near-serverless — there are no servers to patch, no fleets to manage.
Diagram source (Mermaid)
flowchart TB
Client((Web Client\nBrowser))
subgraph "Vercel Edge"
Middleware[Edge Middleware\nCSRF · Auth + Admin guard\nSecurity headers · Request ID · Nonce]
end
subgraph "Vercel / Next.js Environment"
FrontEnd[Next.js App Router\nServer Components]
API[93 API Routes]
RateLimit[Rate Limiter\nPer-route sliding windows]
Cron[Vercel Cron\nDaily escrow release]
Auth[Per-route auth middleware\nJWT + token revocation]
end
subgraph "Data Persistence"
NeonDB[(Neon Postgres\nServerless Pool)]
Redis[(Upstash Redis\nRate Limit + Token Revocation)]
R2[(Cloudflare R2\nPresigned PUT Uploads)]
end
subgraph "External Providers"
Stripe((Stripe Connect\nPayments + Payouts))
Resend((Resend\nTransactional Email))
Shippo((Shippo\nShipping + Tracking))
Sentry((Sentry\nError Monitoring))
Analytics((Vercel Analytics\nPage views + Web Vitals))
end
Client -->|HTTPS| Middleware
Middleware -->|Verified request + security headers| FrontEnd
Middleware -->|Verified request + security headers| API
Middleware -->|Storefront rate limit| Redis
FrontEnd <--> API
API --> Auth
API --> RateLimit
Auth --> Redis
RateLimit --> Redis
API -->|Connection pool| NeonDB
Client -->|Presigned PUT| R2
API -->|Issue presigned URL| R2
Cron -->|Authenticated cron request| API
API <-->|Checkout + scheduled transfers| Stripe
Stripe -->|Webhooks: payment events| API
API -->|Rates, labels, return labels| Shippo
Shippo -->|Tracking webhooks| API
API -->|Transactional events| Resend
FrontEnd -->|Client-side errors + performance| Sentry
API -->|Server + edge errors + traces| Sentry
FrontEnd -->|Page views + Web Vitals| AnalyticsKey design principles visible here:
- Every request passes through edge middleware before reaching a route handler. CSRF validation, auth guards, security headers, and request ID injection all happen at the edge — before any application code runs.
- The browser uploads files directly to R2 — our API only issues the permission, never touching the file bytes. This keeps API functions lean and avoids large payload handling.
- All real-time features (chat, order updates) go through the same API layer as everything else — there's no separate WebSocket server to maintain.
- Every external provider communicates back to us via webhooks into authenticated endpoints — no polling of third-party systems from our side.
3. Authentication & Security
Security is the part of a platform that users shouldn't have to think about — which means it needs to be right from the start. Limn.land uses a layered auth model: cryptographically signed tokens, instant revocation, and abuse-resistant rate limiting on every sensitive endpoint.
3.1 How You Sign In
We support three login paths. The primary experience is passwordless: you enter your email, receive a 6-digit code, and you're in. No password to forget, no password to phish.
Diagram source (Mermaid)
sequenceDiagram
autonumber
actor User
participant Frontend
participant API
participant DB
participant Resend
Note over User,Resend: New Account Registration (2-step email verification)
User->>Frontend: Enter email (registration form)
Frontend->>API: Request verification code (purpose: signup)
API->>DB: Reject if email already registered\nStore one-time code with expiry
API->>Resend: Send verification email
User->>Frontend: Enter code from email
Frontend->>API: Submit code
API->>DB: Validate code, mark email as verified
User->>Frontend: Choose your store handle (slug)
Frontend->>API: Complete registration
API->>DB: Confirm email verified\nCheck handle + email uniqueness\nCreate account
API-->>Frontend: Signed auth token → set as secure cookie
Note over User,Resend: Passwordless Login (existing account)
User->>Frontend: Enter email
Frontend->>API: Request login code
API->>DB: Store one-time code (silently succeeds even if email unknown — prevents account enumeration)
API->>Resend: Send login code
User->>Frontend: Enter code
Frontend->>API: Verify code
API->>DB: Validate code → look up account
API-->>Frontend: Signed auth token → set as secure cookie
Note over User,Resend: Password Login (for accounts that have set a password)
User->>Frontend: Email + password
Frontend->>API: Submit credentials
API->>DB: Verify credentials
API-->>Frontend: Signed auth token → set as secure cookie
Note over User,Resend: Forgot Password
User->>Frontend: Enter email
Frontend->>API: Request password reset
API->>DB: Store reset token with expiry
API->>Resend: Send reset link
User->>Frontend: New password (from email link)
Frontend->>API: Submit new password + token
API->>DB: Validate token\nUpdate password\nInvalidate ALL existing sessions atomically
Note over User,Resend: Sign Out
User->>Frontend: Click Sign Out
Frontend->>API: Logout request
API->>DB: Increment session version (all devices signed out instantly)
API->>Redis: Add old token to revocation list
API-->>Frontend: Clear auth cookie3.2 Edge Middleware & Request Security
Before any request reaches a route handler or page, it passes through middleware.ts — a Next.js Edge Runtime function that runs on Vercel's global network, geographically close to the user. No database is involved; this is a pure-compute layer.
CSRF guard — All POST, PUT, PATCH, and DELETE requests to /api/* are checked for a matching Origin header. Requests from a different origin are rejected with 403. Webhook endpoints (/api/webhooks/*) are excluded from this check — they carry cryptographic signatures from Stripe and Shippo, which is a stronger guarantee than origin matching.
Auth + Admin guard — Every /dashboard/* request is verified against the JWT cookie at the edge before any page renders. Unauthenticated requests redirect to /login?next=<path> — preserving the intended destination but never as a full URL (no open-redirect). Admin routes additionally require isAdmin === true in the token payload. Note: this is a fast signature-only check; the deeper per-request revocation check (Redis blocklist + tokenVersion DB comparison) happens inside route handlers via getCurrentUserVerified().
Storefront rate limiting — Each public storefront page load triggers multiple database queries, so the edge rate-limits at 120 requests/minute per IP before they reach the server. This limiter is intentionally fail-open: a Redis outage never takes down seller pages for legitimate visitors.
Per-request tracing — Two values are generated on every request and forwarded as request headers:
x-nonce: a base64-encoded random UUID, injected into theContent-Security-Policyscript nonce. Modern browsers only execute scripts bearing this nonce, providing defence against injected inline scripts.x-request-id: the first 8 characters of a UUID, echoed on the response. This single ID correlates a user action across middleware logs, route handler logs, database logs, and Sentry traces.
Security response headers — Applied to every response:
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | Enforces HTTPS for one year; preload-list eligible. Production only. |
Content-Security-Policy | default-src 'self'; script-src 'nonce-{n}' 'unsafe-inline'; frame-src …stripe.com | Per-request nonce for scripts; Stripe domains explicitly allowlisted for embedded checkout. |
X-Frame-Options | DENY | Prevents the platform from being embedded in a foreign iframe (clickjacking). |
X-Content-Type-Options | nosniff | Prevents MIME-type sniffing attacks. |
X-XSS-Protection | 1; mode=block | Activates the XSS filter in legacy browsers that support it. |
Referrer-Policy | strict-origin-when-cross-origin | Limits referrer header exposure on cross-origin navigations. |
Permissions-Policy | camera=(), microphone=(), geolocation=(), interest-cohort=() | Disables browser features the platform never uses and opts out of FLoC tracking, limiting attack surface. |
4. Database Design
The database is PostgreSQL — relational, transactional, and strict. Every relationship has a foreign key. Every financial record has an audit trail. The schema is designed to make invalid states impossible to represent, not just unlikely to occur.
Here's the entity relationship model for the 23 active application tables. (Five additional tables exist in the schema — schema_migrations for migration tracking, and four legacy tables predating the current conversation and recommendation systems — but they are not part of the live application flow.):
Diagram 1 of 3 — Entity relationship map (all 23 active tables, no attribute detail):
Diagram source (Mermaid)
erDiagram
CREATORS ||--o{ PRODUCTS : creates
CREATORS ||--o{ ORDERS : receives
CREATORS ||--o{ SELLER_RECOMMENDATIONS : earns
CREATORS ||--o| PREDROP_CAMPAIGNS : owns
CREATORS ||--o{ CONVERSATIONS : participates
CREATORS ||--o{ SHOPIFY_IMPORT_ATTEMPTS : tracked
CREATORS ||--o{ PRODUCT_CATEGORIES : organizes
PRODUCTS ||--o{ PRODUCT_VARIANTS : has
PRODUCTS ||--o{ PRODUCT_COLORS : has
PRODUCTS ||--o{ PRODUCT_IMAGES : has
PRODUCTS ||--o{ PRODUCT_MATERIALS : has
PRODUCTS ||--o{ STOCK_RESERVATIONS : holds
PRODUCTS ||--o{ PREDROP_CAMPAIGN_PRODUCTS : features
PRODUCTS |o--|| PRODUCT_CATEGORIES : "filed under"
PRODUCT_COLORS ||--o{ PRODUCT_IMAGES : has
PRODUCT_VARIANTS |o--|| PRODUCT_COLORS : uses
PRODUCT_VARIANTS ||--o{ STOCK_RESERVATIONS : locks
ORDERS ||--|{ ORDER_ITEMS : contains
ORDERS ||--o| ORDER_RETURNS : has
ORDERS ||--o| CONVERSATIONS : has
ORDERS ||--o| SELLER_RECOMMENDATIONS : triggers
CONVERSATIONS ||--o{ CHAT_MESSAGES : has
CONVERSATIONS ||--o{ ORDER_RETURNS : handles
PREDROP_CAMPAIGNS ||--o{ PREDROP_CAMPAIGN_PRODUCTS : links
PREDROP_CAMPAIGNS ||--o{ PREDROP_SIGNUPS : capturesDiagram 2 of 3 — Creator & product schema (fields for the seller account and catalog tables):
Diagram source (Mermaid)
erDiagram
CREATORS ||--o{ PRODUCTS : creates
PRODUCTS ||--o{ PRODUCT_VARIANTS : has
PRODUCTS |o--|| PRODUCT_CATEGORIES : "filed under"
PRODUCTS ||--o{ PRODUCT_COLORS : has
PRODUCT_COLORS ||--o{ PRODUCT_IMAGES : has
PRODUCT_VARIANTS ||--o{ STOCK_RESERVATIONS : locks
CREATORS {
integer id PK
string email UK
string slug UK
string stripe_connect_id UK
boolean is_admin
boolean is_suspended
boolean vacation_mode
integer token_version
string social_verification_status
string shipping_type
integer shipping_rate_cents
boolean returns_enabled
integer handling_time_min
integer handling_time_max
integer recommendation_yes_count
integer recommendation_no_count
}
PRODUCTS {
integer id PK
integer creator_id FK
string slug
string name
decimal price
string image_url
integer category_id FK
string variant_type
boolean is_published
bigint shopify_product_id
}
PRODUCT_VARIANTS {
integer id PK
integer product_id FK
string size
string color
integer color_id FK
integer stock_quantity
decimal price_override
}
PRODUCT_CATEGORIES {
integer id PK
integer creator_id FK
string name
integer position
}
PRODUCT_COLORS {
integer id PK
integer product_id FK
string name
string hex_value
integer position
boolean is_available
}
PRODUCT_IMAGES {
integer id PK
integer product_id FK
integer color_id FK
string url
integer position
}
STOCK_RESERVATIONS {
integer id PK
integer variant_id FK
integer product_id FK
string stripe_session_id
integer quantity
timestamp expires_at
}Diagram 3 of 3 — Order & payment schema (fields for the financial tables):
Diagram source (Mermaid)
erDiagram
ORDERS ||--|{ ORDER_ITEMS : contains
ORDERS ||--o| ORDER_RETURNS : has
ORDERS ||--o| SELLER_RECOMMENDATIONS : triggers
ORDERS ||--o| CONVERSATIONS : has
CONVERSATIONS ||--o{ CHAT_MESSAGES : has
ORDERS {
integer id PK
integer advisor_id FK
string stripe_payment_intent_id
string stripe_transfer_id
integer amount
string status
string funds_status
timestamp funds_release_date
timestamp funds_released_at
integer seller_amount
integer platform_fee_amount
integer refunded_amount
integer return_label_cost_cents
string tracking_number
timestamp delivered_at
}
ORDER_ITEMS {
integer id PK
integer order_id FK
integer product_id FK
string product_name
string image_url
integer price
integer quantity
boolean is_refunded
}
ORDER_RETURNS {
integer id PK
integer order_id FK
integer conversation_id FK
string status
string return_tracking_number
integer label_cost_cents
timestamp received_at
timestamp refund_issued_at
}
STRIPE_EVENTS {
string id PK
string type
boolean processed
timestamp created_at
}
SELLER_RECOMMENDATIONS {
integer id PK
integer order_id FK
integer creator_id FK
string status
timestamp scheduled_for
string response
string token UK
}
CONVERSATIONS {
integer id PK
integer advisor_id FK
string buyer_token UK
string status
integer order_id FK
}
CHAT_MESSAGES {
integer id PK
integer conversation_id FK
string sender_type
string message_type
text content
timestamp created_at
}A few design choices worth calling out:
ORDER_ITEMSsnapshots product name and image at purchase time. Products can be edited or deleted after sale. The order record always reflects exactly what the buyer saw when they paid — no references that could silently drift.STRIPE_EVENTSis an idempotency table. Every incoming Stripe webhook is recorded by its unique Stripe event ID before processing. If Stripe sends the same event twice (it will — they guarantee at-least-once delivery), we process it exactly once.- Funds state is tracked separately from order state.
funds_statustracks where the money is (held, released, refunded, disputed, reversed) independently ofstatus(the fulfillment state). This separation means a "delivered" order can still be in an "escrow held" state, which is exactly right during the dispute window. token_versiononCREATORSis the master kill switch. Incrementing this field atomically invalidates every active session for that user across every device — used on password change, logout, suspension, and account recovery.
5. Payment, Escrow & Order Lifecycle
This is the most complex part of the platform, and arguably the most important. Getting money right — who holds it, when it moves, what happens when things go wrong — is what makes or breaks a commerce platform. Here's every step of the money flow, from checkout button to seller payout.
5.1 Checkout Flow & Price Integrity
When a buyer clicks checkout, we don't trust anything from the browser. Prices, stock levels, and seller account status are all re-verified server-side before a Stripe session is created.
Diagram source (Mermaid)
sequenceDiagram
autonumber
actor Buyer
participant Frontend
participant Server as /api/billing/create-checkout
participant DB
participant Stripe
Buyer->>Frontend: Clicks Checkout
Frontend->>Server: Items (product/variant IDs + quantities)
rect rgb(230, 240, 255)
Note over Server,DB: Server-Side Integrity Verification
Server->>DB: Fetch products + seller (vacation mode, suspension status, Stripe account, shipping config)
Server->>Server: Reject mixed-seller cart — all items must belong to the same seller
Server->>Server: Block if seller is on vacation or suspended
Server->>DB: Read authoritative prices from database — client-submitted prices are ignored
Server->>DB: Check active stock holds to prevent overselling
Server->>Stripe: Confirm seller's Stripe account can accept charges
end
Server->>Server: Add flat-rate shipping line item if applicable
Note over Server,Stripe: Platform fee calculated\nSeller amount = total minus fee\nFunds held on platform account — not transferred yet
Server->>Stripe: Create checkout session\nFunds stay on platform (no immediate transfer to seller)
Stripe-->>Server: Session token
Server->>DB: Reserve stock for checkout duration
Server-->>Frontend: client_secret → mount Stripe Payment Element in pageWhy we use Stripe Payment Element (custom UI mode): The payment form renders inside our page using Stripe's hosted Payment Element — buyers never leave limn.land, but the card data never touches our servers. The session is created with ui_mode: 'custom', which returns a client_secret the frontend uses to mount the Element directly in the page; buyers are never redirected to a separate Stripe-hosted checkout page. Stripe handles PCI compliance; we handle the commerce logic.
Order creation resilience: After a buyer completes checkout, Stripe fires a webhook to confirm payment. Our session-status polling endpoint provides a fallback — if the webhook is delayed (which can happen), the browser's return page creates the order directly. A database ON CONFLICT guard ensures the order is only ever created once regardless of which path wins.
Fee structure: Platform fee is 4.9% + $0.30 per order, calculated by a single shared function (calculateFees in lib/platformFees.ts) that is the sole source of truth for checkout math, refund clawback calculations, and dashboard display. The fee bundles the platform's ~2% margin on top of Stripe's ~2.9% + $0.30 pass-through. The seller's net amount (seller_amount) is computed at checkout creation and stored on the order record immediately — it is never re-derived at payout time, so the transfer amount is always the exact value the seller was shown.
Session and reservation TTL: Checkout sessions expire after 30 minutes (expires_at is set explicitly on the Stripe session at creation). Stock reservations are written with expires_at = session_expiry + 60 seconds — the extra buffer absorbs webhook lag so a reservation is never deleted before the checkout.session.expired event arrives. If the session expires without payment, that webhook deletes the reservation immediately. A daily cron also purges any stale reservations older than 1 day as a safety net.
Seller account check: The code verifies charges_enabled: true on the Stripe Connect account — not merely that a stripe_connect_id exists. A seller who started Stripe onboarding but did not complete identity or banking verification will have a stripe_connect_id but charges_enabled: false; their storefront blocks checkout with an explicit message. This check is bypassed in development to allow local testing without a fully-verified Express account.
Reconciliation link: A transfer_group value (format: order_{advisorId}_{timestamp}) is attached to the payment_intent_data when the checkout session is created. This links the original charge to the eventual seller transfer inside Stripe's dashboard, enabling per-order reconciliation independent of our database records.
5.2 Escrow, Delivery & Post-Delivery Automation
Every order goes through an automated escrow lifecycle. Funds are never released until a deliberate trigger — either confirmed delivery or a timer expiry. Here's every state an order's funds can be in:
Diagram source (Mermaid)
stateDiagram-v2
direction TB
[*] --> Held : checkout.session.completed
Held --> Shipped : Seller purchases label
Shipped --> Delivered : Shippo DELIVERED webhook
Delivered --> Released : 7-day window passes — Stripe transfer to seller
Delivered --> RecommendationScheduled : Shippo webhook schedules — 3-day send delay
RecommendationScheduled --> RecommendationSent : Daily cron sends when scheduled_for passes
Held --> Disputed : Buyer files chargeback
Disputed --> Reversed : Platform loses dispute
Disputed --> Held : Platform wins — fresh 7-day window
Held --> Refunded : Refund issued by seller or admin
note right of Held
Suspended sellers: funds held indefinitely.
Cron skips suspended accounts until resolved.
end note
state RecommendationSent {
BuyerResponse : Buyer clicks one-click token
BuyerResponse --> RepUpdated : Reputation score updated
}The 7-day dispute window after confirmed delivery is the core protection for buyers. Even if a seller disappears after shipping, there's a structured window to raise issues before any money leaves the platform. Funds for suspended seller accounts remain held indefinitely — the cron skips them until the suspension is resolved.
Dual release timer: funds_release_date is set at two points in the lifecycle. At order creation (payment confirmed), it is set to NOW() + 7 days as a fallback — covering the case where a seller ships outside the platform and no Shippo DELIVERED event ever arrives. When the Shippo DELIVERED webhook fires, the timer is reset to NOW() + 7 days from that moment. This means buyers always get a full 7-day dispute window from the day their package arrives, not from the day they paid.
Automatic inventory management: When the webhook processes a successful checkout, it decrements stock for each purchased item. If that decrement exhausts all inventory for a product (all variants at zero, or product-level stock at zero), the product is automatically set to is_active = false and status = 'draft' — pulled from the storefront without seller intervention. The oversell detection mechanism uses UPDATE ... WHERE stock >= qty rather than GREATEST(): a concurrent order that already consumed the last unit causes 0 rows updated, which the handler detects and uses to trigger an oversell alert email to the seller.
Recommendation scheduling: When the Shippo DELIVERED webhook fires, it inserts a seller_recommendations row with status = 'scheduled' and scheduled_for = NOW() + 3 days. A separate dedicated cron job (/api/recommendations/send-pending, runs daily at midnight) queries for all due rows, sends the email via Resend, and marks them sent. The process is fully idempotent: one recommendation is scheduled per order maximum, and if the buyer has already responded to a recommendation for the same seller previously, the scheduling step is skipped entirely.
5.3 Return Flow
Returns are handled end-to-end through the conversation thread, keeping communication, logistics, and refund status all in one place.
Diagram source (Mermaid)
sequenceDiagram
actor Buyer
participant Chat as Conversation Thread
participant API
participant Shippo
participant DB
Buyer->>Chat: Initiates return via conversation
Chat->>API: Return request
API->>DB: Create return record (status: requested)
API->>Shippo: Fetch return shipping rate options
Shippo-->>API: Carrier + rate options
API-->>Buyer: Rate options shown in thread
Note over API,Shippo: Seller selects rate + purchases return label
API->>Shippo: Purchase return label
Shippo-->>API: Return tracking number + label PDF
API->>DB: Update return record (status: label_generated)\nRecord label cost — will be deducted from payout
API->>DB: Insert return label message in chat thread
API->>Resend: Email label PDF to buyer
Shippo->>API: Tracking webhook — TRANSIT
API->>DB: Update return (status: in_transit)\nInsert system message in chat
Shippo->>API: Tracking webhook — DELIVERED
API->>DB: Update return (status: delivered)\nInsert system message in chat
Note over API,DB: Seller confirms receipt in dashboard
API->>DB: Confirm receipt
API->>Stripe: Issue refund via original payment
API->>DB: Update order funds_status = refunded5.4 Stripe Webhook Handling
Stripe notifies us of every payment event. Every webhook is signature-verified and deduplicated via a two-phase idempotency mechanism before processing — Stripe guarantees at-least-once delivery, so we must guarantee exactly-once processing.
Two-phase idempotency: When an event arrives, claimStripeEvent attempts to insert a row into stripe_events. If the row already exists and has a processed_at timestamp, the event is a duplicate and is skipped. If the row exists but processed_at is NULL — meaning a prior invocation claimed the event but crashed before finishing — the event is re-processed rather than silently dropped. Only after every handler has completed successfully does markStripeEventProcessed set processed_at. This survives handler crashes without data loss.
Post-transfer refund clawback: When a refund is issued after funds have already been transferred to the seller (funds_status = 'released'), the charge.refunded handler automatically reverses the seller's proportional share via stripe.transfers.createReversal. The reversal amount is computed proportionally: if the seller received 94.8% of the original charge and a 50% refund is issued, 47.4% of the original charge is clawed back from their Connect account. The platform absorbs its proportional share of the fee. This calculation is shared with the API-initiated refund path via calculateSellerShareOfRefund in lib/platformFees.ts. Refunds issued while funds are still held require no reversal — the cron simply never releases those funds.
Diagram source (Mermaid)
flowchart TD
SHook[POST /api/webhooks/stripe/connected\nStripe signature verified\nIdempotency: two-phase claim/mark on stripe_events]
SHook --> E1[checkout.session.completed]
SHook --> E2[checkout.session.expired]
SHook --> E3[charge.refunded]
SHook --> E4[charge.dispute.created]
SHook --> E5[charge.dispute.closed]
SHook --> E6[payment_intent.payment_failed]
E1 -->|cart checkout event| O1["Create order (funds_status=held)\nON CONFLICT: skip if already created"]
O1 --> O2[Create order items\nDecrement stock\nOversell detection]
O2 --> O3[Release stock reservations\nInventory freed after stock decremented]
O3 --> O4[Send emails via Resend:\nBuyer confirmation\nSeller notification\nOversell alert if stock gap]
E2 --> S1[Release stock reservations\nInventory immediately freed]
E3 --> R1["Update order status + refunded_amount"]
R1 -->|funds already released| R2["Proportional seller clawback\nvia transfer reversal"]
R1 -->|funds still held| R3["funds_status = refunded\nNo reversal needed"]
E4 --> D1["Update order\nfunds_status = disputed\n(prevents cron from releasing funds)"]
D1 --> D2[Alert email to platform admin]
E5 -->|status = won| DW["Update order\nfunds_status = held\nNew 7-day release window"]
E5 -->|status = lost| DL["Update order\nfunds_status = reversed"]
E6 --> L1[Log only — no order created\nOrders only created on checkout.session.completed]
SHook --> E7[charge.refund.updated]
SHook --> E8[transfer.reversed]
SHook --> E9["charge.dispute.funds_withdrawn\ncharge.dispute.funds_reinstated"]
E7 -->|status = failed| RU1["Alert admin — refund failed to settle\nBuyer was not actually refunded\nManual reconciliation required"]
E7 -->|status = pending / succeeded| RU2[Sync refund status in refunds ledger]
E8 --> T1[Audit log — correlate reversal to order\nNo state changes]
E9 --> DF1["Log Stripe balance movement\nfunds_status already set by\ndispute.created / dispute.closed"]6. Admin & Moderation
Platform administration runs through a role-gated admin panel backed by a full audit log. Every admin action is recorded with the acting admin's ID, the target, and a metadata snapshot.
Diagram source (Mermaid)
flowchart TB
AdminUser[Admin Session\nRole-gated — is_admin flag on creators table]
subgraph "Seller Management"
AdminUser --> SellerList[View all creators]
AdminUser --> SellerActions[Suspend / Unsuspend seller]
SellerActions -->|Suspend| SuspendFlag[(creators.is_suspended = true\nAll sessions invalidated instantly)]
end
subgraph "Content Moderation"
AdminUser --> ProductMod[View all platform products]
ProductMod -->|Hide/Archive| ProductDB[(products.is_published = false)]
end
subgraph "Social Verification Queue"
AdminUser --> VerifQueue[Review verification requests]
VerifQueue -->|Approve| VerifApproved[(social_verification_status = verified)]
VerifQueue -->|Reject| VerifRejected[(social_verification_status = none)]
end
subgraph "Observability"
AdminUser --> AuditLog[Admin audit log]
AuditLog --> AuditDB[(admin_audit_log — every action recorded)]
AdminUser --> Health[Platform health dashboard]
endWhen a seller is suspended, the platform uses the same session invalidation mechanism as a password change — all their active sessions are terminated immediately, and the daily cron job skips their orders when processing escrow releases.
7. Scheduled Jobs & Data Retention
A single daily cron job handles two responsibilities in parallel: releasing eligible escrow funds and cleaning up expired data.
Diagram source (Mermaid)
flowchart TB
Cron[Vercel Cron — runs daily\nGET /api/cron/release-funds]
Cron --> ReleaseFunds[Release Eligible Funds]
Cron --> Cleanup[Data Retention Cleanup]
subgraph "Fund Release"
ReleaseFunds --> QueryOrders[Query orders:\nfunds held\nrelease date passed\nnot disputed or refunded\nseller not suspended\nLimit 100 per run]
QueryOrders --> ForEach{For each eligible order}
ForEach --> GetCharge[Retrieve payment intent from Stripe]
GetCharge --> Transfer[Transfer to seller's Connect account\namount minus any return label costs]
Transfer --> MarkReleased[Mark order: funds released\nRecord transfer ID + timestamp]
end
subgraph "Data Retention Cleanup — runs in parallel"
Cleanup --> D1[Delete old processed webhook records]
Cleanup --> D2[Delete expired email verification codes]
Cleanup --> D3[Delete expired stock reservations]
Cleanup --> D4[Delete unclaimed provisional accounts\nthat were never activated]
endDesign choices here:
- Both tasks run in parallel — a cleanup failure never blocks fund release.
- Fund release is capped at 100 orders per run, preventing timeout on large queues. The cap means high-volume days process across multiple daily runs — this is acceptable given the 7-day window involved.
- Return label costs are deducted from the transfer amount at release time, not at label purchase time. This keeps the accounting accurate regardless of when the label was bought.
- Expired provisional accounts — where a claim link was generated but never used — are deleted at cleanup time. Without this, unclaimed stores would accumulate indefinitely.
What This Adds Up To
28 database tables (23 active, shown in the ERD). 13 rate limiting buckets. An edge middleware layer enforcing CSRF protection, Argon2id-hashed passwords, auth guards, and a full suite of security headers on every request. A complete escrow lifecycle with automated dispute handling and daily fund release. Platform-level session invalidation on every security event. A full audit log for every admin action.
Every part of this was designed to be dependable, auditable, and honest about failure modes. When Redis goes down, rate limiting fails safely. When Stripe sends duplicate webhooks, the idempotency table catches them. When someone changes their password, every session everywhere is invalidated immediately.
That's Limn.land. If you're building on it, you now know exactly what you're building on.
Built in public. Questions and feedback welcome.