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 (35 tables), the destination-charge payment architecture for made-to-order art via Stripe Connect, print fulfilment, the 30-day damage and defect claim flow, webhook handling, admin tooling, and the daily cron jobs. 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 an artist sell original work as framed prints 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 | Stripe Connect (destination charges) | The buyer's charge is created on the platform account and the seller's share is sent to their connected account as a transfer at the same moment. The platform keeps a $5 buyer fee, pays Stripe's processing fee, pays the print supplier, and remits sales tax as merchant of record. The seller sets their price and receives it less the cost of producing and delivering the piece. |
| Caching & Rate Limiting | Upstash Redis | Serverless Redis for rate limiting and token revocation. Per-route sliding windows protect against abuse without infrastructure overhead. |
| Logistics | Lumaprints | The production supplier prints, frames, packs and ships every order direct to the buyer. The platform owns the order and the customer, not the parcel. |
| 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. Public previews live in the asset bucket; full-resolution print masters live in a separate private bucket and are uploaded directly from the browser via short-lived signed URLs — the server never handles file bytes, and a master is never served publicly. |
| 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[64 API Routes]
RateLimit[Rate Limiter\nPer-route sliding windows]
Cron[Vercel Cron\nDaily cleanup + reconciliation]
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))
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 <-->|Destination charges + seller transfers| Stripe
Stripe -->|Webhooks: payment events| 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 the edge proxy 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 proxy.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 a cryptographic Stripe signature, 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 — A request ID is generated on every request and forwarded as a request header:
x-request-id: the first 8 characters of a UUID, echoed on the response. This single ID correlates a user action across proxy logs, route handler logs, database logs, and Sentry traces.
The request also carries the Content-Security-Policy header, not only the response — Next.js reads the policy from the request while it renders, so a policy the server never receives is a policy it cannot act on.
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 'self' 'unsafe-inline'; frame-src …stripe.com | Allows same-origin and inline scripts — Next.js emits its own hydration scripts inline — plus the Stripe domains embedded checkout needs. Set on the request as well as the response. |
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. |
The policy deliberately names no script nonce, which is worth stating because the stricter-looking option is the broken one here. A nonce reaches only dynamically rendered pages — a statically prerendered page is built with no request to read it from — and while a nonce is present in script-src, 'unsafe-inline' is ignored by specification. So a nonce on this policy left Next.js's own un-nonced inline scripts refused on every static page: the HTML rendered, React aborted hydration, and no client component mounted. A nonce becomes the right answer the moment every route renders dynamically, and not before.
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 28 application tables. A framed artwork is one row with a JSON specification, so there are no option, colour or stock tables: the merchandise model they belonged to was retired, and its tables were dropped. Earlier listings stay readable through framed_print and their read-only attributes. The migration ledger, sql_migrations, is infrastructure rather than application state and is not drawn.
Diagram 1 of 3 — Entity relationship map (all application tables, no attribute detail):
Diagram source (Mermaid)
erDiagram
CREATORS ||--o{ PRODUCTS : creates
CREATORS ||--o{ ORDERS : receives
CREATORS ||--o{ SELLER_RECOMMENDATIONS : earns
CREATORS ||--o{ CONVERSATIONS : participates
CREATORS ||--o{ PRODUCT_CATEGORIES : organizes
PRODUCTS ||--o{ PRODUCT_IMAGES : has
PRODUCTS ||--o{ PRODUCT_MATERIALS : "legacy read only"
PRODUCTS |o--|| PRODUCT_CATEGORIES : "filed under"
ORDERS ||--|{ ORDER_ITEMS : contains
ORDERS ||--o| ORDER_RETURNS : has
ORDERS ||--o| CONVERSATIONS : has
ORDERS ||--o| SELLER_RECOMMENDATIONS : triggers
CONVERSATIONS ||--o{ CHAT_MESSAGES : hasDiagram 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_IMAGES : has
PRODUCTS |o--|| PRODUCT_CATEGORIES : "filed under"
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
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 product_type
jsonb framed_print "Print size, frame finishes + private master"
string status
boolean is_active
boolean admin_hidden
}
PRODUCT_CATEGORIES {
integer id PK
integer creator_id FK
string name
integer position
}
PRODUCT_IMAGES {
integer id PK
integer product_id FK
string url
integer position
}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_connect_id
integer amount
string status
string funds_status
integer refunded_amount
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
jsonb framed_print "Purchased format + artwork"
integer price
integer quantity
boolean is_refunded
}
CHECKOUT_ARTWORKS {
string stripe_session_id PK
jsonb items "Frozen before payment"
timestamp created_at
}
ORDER_RETURNS {
integer id PK
integer order_id FK
string claim_type "damaged|defective|wrong_item|not_as_described"
string reason
string status
string resolution "replacement|refund|denied"
string fault_party "supplier|artist|platform|buyer"
string supplier_claim_status
integer refund_amount_cents
timestamp decided_at
timestamp created_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 the artwork at purchase time. A framed listing freezes its name, price, preview, format and private master intocheckout_artworksbefore the payment session exists, and the order item stores that snapshot. Editing or deleting the listing afterwards cannot change what was bought, what was printed, or what was refunded.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_statusdescribes where the money went, independently ofstatus(the fulfilment state). An order paid under the current model isdirect— the historical column value now meaning the charge lives on the platform account, with the seller's share transferred at payment — whilerefunded,disputedandreverseddescribe what later happened to it.seller_amount,platform_fee_amountandplatform_retained_amountrecord the split computed at checkout. The valuesheld,releasedandfrozensurvive only so orders placed before destination charges still render correctly. The escrow-era columns those values depended on are likewise still present but no longer written: they describe real historical rows, and dropping a column reinterprets data rather than removing it. 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 & Order Lifecycle
This is the most important part of the platform. Getting money right — who holds it, when it moves, what happens when things go wrong — is what makes or breaks a commerce platform. Here is every step of the money flow, from the checkout button to the artist's transfer, under a destination-charge model: the buyer pays Limn, Limn pays the print supplier, and the artist receives their share at the same moment.
5.1 Checkout Flow & Price Integrity
When a buyer clicks checkout, we don't trust anything from the browser. Prices, the framed-print specification, and seller account status are all re-verified server-side before a Stripe session is created. The artwork is then frozen into checkout_artworks, so an edit or a deletion after the payment page opens cannot change what is bought.
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: Artwork items (product IDs + quantities)
rect rgb(230, 240, 255)
Note over Server,DB: Server-Side Integrity Verification
Server->>DB: Fetch products, framed_print specs + seller (vacation mode, suspension, Stripe account)
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 — client-submitted prices are ignored
Server->>DB: A framed listing must be publishable: verified master, matching ratio/PPI, price at or above the format floor
Server->>DB: Freeze name, price, preview, format and private master into checkout_artworks
Server->>Server: charge = artwork prices + $5 buyer fee + sales tax\ntransfer = artwork prices − supplier fulfilment
Server->>Stripe: Confirm seller's Stripe account can accept charges
end
Note over Server,Stripe: Destination charge — session created on the PLATFORM account\nNo application fee; the split is transfer_data.amount\nSession TTL: 30 minutes
Server->>Stripe: Create checkout session + transfer_data { destination: seller, amount: seller share }
Stripe-->>Server: client_secret
Server->>DB: Attach the real session id to checkout_artworks
Server-->>Frontend: client_secret → mount 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: a $5 buyer fee, and no commission on the artwork. Limn.land charges the buyer a $5 platform fee once per fulfilled order. It is not deducted from the artist's price: the artist sets one delivered price, which includes printing, framing and delivery, and receives that price minus the supplier's fulfilment cost. The platform keeps the $5 fee, the fulfilment money and sales tax; it pays the supplier and remits the tax. There is no monthly fee, no listing fee and no percentage taken from the artwork price.
What the platform is in the money flow: the merchant of record. The charge is created on the platform account, so the platform holds the money long enough to pay the print supplier and transfer the artist their share. There is no escrow schedule and no held seller balance: the transfer happens at payment, and a refund reverses it proportionally. That is also why the platform, not the seller, calculates and remits sales tax on the order.
Session and reservation TTL: Checkout sessions expire after 30 minutes (expires_at is set explicitly on the Stripe session at creation). A framed artwork has no stock to reserve — each piece is produced to order from a private master — so the reservation path belongs to legacy merchandise only. Those 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. A daily cron purges any stale reservation as a safety net.
Seller account check: A seller who started Stripe onboarding but did not complete identity or banking verification has a stripe_connect_id but charges_enabled: false; their storefront refuses checkout with a message naming the incomplete setup, and their account record is cleared outright when Stripe no longer recognises it, so Settings can prompt them to reconnect. The answer is served from a mirror of the account's charges_enabled status kept up to date by Stripe's account webhook, not from a fresh API call on every checkout — so an ordinary sale costs no extra Stripe request, and a Stripe outage cannot close a storefront that is otherwise fine. When the status cannot be determined at all, checkout proceeds and Stripe itself refuses the payment if the account genuinely cannot take one: a platform that reads "we don't know" as "no" takes revenue from sellers who did nothing wrong. Development does not block on this check, but logs the decision rather than skipping it, so the path stays visible while working locally.
Payment methods that need a registered domain: Apple Pay, Google Pay, Link, Klarna, Amazon Pay and PayPal render inside our checkout only once the domain the buyer is on is registered on the platform's Stripe account. Stripe's rule is specific: a platform that creates destination charges must manage payment-method domains for its own account, not rely on the Dashboard defaults. The platform checks the registration daily, registers anything missing, and lists any payment method Stripe reports as inactive. It is handled in code because ignoring it fails silently: nothing errors, those payment methods are simply absent from the checkout form, and the cost is a lower conversion rate rather than a bug report.
Per-order reconciliation: Each order records the transfer destination (orders.stripe_connect_id) and the split computed at checkout (seller_amount, platform_fee_amount, platform_retained_amount). The charge itself lives on the platform account, so a refund is issued unscoped with reverse_transfer: true. Recording the destination matters because a seller can replace their Stripe account — re-onboarding creates a new one — while historical transfers stay attached to the old account.
5.2 Destination Charges: Where the Money Actually Goes
Every checkout session is created on the platform account, and the artist's share is sent to their connected account as a transfer_data transfer at the same moment. This is Stripe's destination charges model, and it exists for one reason: Limn has to hold the money long enough to pay the print supplier.
Three consequences follow from that decision:
- The platform is the merchant of record. The buyer pays Limn. The platform keeps the $5 buyer fee, the fulfilment money and sales tax, pays the supplier, remits the tax, and transfers the artist their listed price minus fulfilment. The artist never funds production or delivery out of a separate balance.
- The artist is paid at payment, not on a payout schedule we control. Their connected account receives the transfer immediately; Stripe's payout schedule and bank details remain theirs, managed in their own dashboard. There is no escrow, no release cron, and no held seller balance for the platform to withhold.
- Liability sits with the platform, because the money does. Card processing, chargebacks and the supplier bill are the platform's; a lost dispute is the platform's loss, and the seller's transfer is reversed proportionally. That is the honest trade for being able to pay the supplier from the same charge.
Sellers still get a Standard-equivalent connected account via Stripe's controller properties — requirement_collection: stripe, stripe_dashboard.type: full — so identity verification, payouts and the seller's own dashboard stay with Stripe. stripe_dashboard.type is immutable after creation, so an account created as Express can never become Standard-equivalent; earlier sellers need a fresh account rather than a migration.
Diagram source (Mermaid)
sequenceDiagram
autonumber
actor Buyer
participant Server as /api/billing/create-checkout
participant Stripe as Stripe (platform account)
participant Seller as Seller's connected account
participant Supplier as Print supplier
Buyer->>Server: Clicks checkout
Server->>Server: Re-verify prices, format, seller status server-side
Server->>Server: Freeze artwork + format into checkout_artworks
Server->>Stripe: Create session on the platform + transfer_data { destination: seller, amount: price − fulfilment }
Stripe-->>Server: client_secret
Server-->>Buyer: Mount Payment Element
Buyer->>Stripe: Pays price + $5 fee + tax
Stripe->>Seller: Transfer of the artist's share
Note over Stripe,Seller: The artist is paid immediately.<br/>The platform keeps the fee, fulfilment and tax.
Stripe->>Server: checkout.session.completed
Server->>Supplier: Submit the framed order (after the response)
Server->>Server: Create order, notify buyer and seller
Note over Server,Supplier: The platform pays the supplier from its retained balance.An order's funds_status is direct from the moment it is created — the historical column value now records that the charge lives on the platform account, with the seller's share transferred. The values held, released and frozen are retained only so orders placed before this model existed still read correctly; nothing writes them any more.
When something goes wrong. A refund is created on the platform charge with reverse_transfer: true, so the artist's transfer is reversed proportionally and the platform recovers the supplier's share where it can. A chargeback debits the platform and the platform answers it using the order, the supplier's tracking and the buyer thread as evidence; the seller is told the deadline but is not the respondent. A lost dispute reverses the seller's transfer through lib/disputeRecovery.ts, idempotently and with a daily audit, because Stripe debits the platform and leaves the transfer in place. Suspending a seller stops them from trading — checkout is refused and the store is hidden — and does not move money already transferred.
Legacy orders are handled explicitly, not silently. Orders placed before the charge model changed were created on the seller's connected account. The daily reconciliation job reports any live order that does not match the current model, so a refund or a dispute on one is a known case rather than an unexplained Stripe error.
Inventory: a framed artwork has no stock. Each piece is produced to order from a verified private master, so there is nothing to decrement and no oversell to detect. The stock reservation and decrement paths were removed with the merchandise tables; a framed listing leaves the storefront when its seller archives it.
Recommendation scheduling: When an order is marked delivered, the platform 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 Damage and Defect Claims
Every store carries the same 30-day guarantee from delivery. A buyer opens a claim in the order conversation for one of four covered reasons: damage in transit, a print or frame defect, the wrong artwork/frame/size, or a piece materially different from its listing. Change of mind is not covered; all other sales are final.
The platform decides the claim, not the seller. The print supplier ships directly to the buyer and the seller never receives the parcel, so the seller has nothing to inspect or restock. An admin approves a replacement or a refund, or declines with a note. The decision, the refund and the supplier claim record are one action. Supplier defects do not reverse the artist's transfer; artist fault does. Lumaprints has no return/refund API, so the supplier side is recorded from its support response rather than polled.
Diagram source (Mermaid)
sequenceDiagram
actor Buyer
actor Admin
participant Chat as Order Conversation
participant API
participant DB
participant Stripe
Buyer->>Chat: Opens a claim within 30 days of delivery
Chat->>API: POST /api/conversation/[token]/return-request
API->>DB: Create claim + selected line items (status: requested)
API->>DB: Post a system message and alert the platform operator
Note over Buyer,API: Covered: damage, defect, wrong item, material misdescription.\nChange of mind is not offered.
Note over Admin,DB: The platform decides; the seller does not handle the parcel.
Admin->>DB: Decide replacement, refund or decline
Admin->>Stripe: Refund the platform charge; reverse the seller transfer only on artist fault
Admin->>DB: Mark the claim, items and order totals refunded, or record the replacement
Admin->>DB: Post the decision in the thread and email the buyer5.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.
Refunds and disputes cost the platform first. A refund is created on the platform charge. A seller refund, or a damage claim where the artist was at fault, sets reverse_transfer: true, so Stripe reverses the artist's share proportionally. A supplier defect does not reverse the artist's transfer: the platform pays, then recovers the cost through the supplier's replacement or credit. A chargeback debits the platform, which answers it using the order, the supplier's tracking and the buyer thread as evidence. If the dispute is lost, lib/disputeRecovery.ts reverses the artist's share of the transfer — idempotently, and retried by the daily audit when it failed because the seller's balance was short. Limn records both against the order and alerts an admin when a dispute opens, because an unanswered dispute is lost automatically.
Diagram source (Mermaid)
flowchart TD
SHook[POST /api/webhooks/stripe\nPlatform account — every charge lives here\nSignature verified; 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=direct,\ntransfer destination + split recorded)\nON CONFLICT: skip if already created"]
O1 --> O2[Create order items from the frozen artwork\nQueue the framed order for the supplier]
O2 --> O3[Delete the frozen artwork snapshot\nNo stock to release — framed art is made to order]
O3 --> O4[Send emails via Resend:\nBuyer confirmation\nArtist notification]
E2 --> S1[Delete the frozen artwork snapshot\nfor the expired session]
E3 --> R1["Update order status + refunded_amount\nRecord in refunds ledger"]
R1 --> R2["reverse_transfer: true — the artist's\ntransfer is reversed proportionally"]
E4 --> D1["Update order\nfunds_status = disputed"]
D1 --> D2[Alert email to platform admin\nStripe debits the platform]
E5 -->|status = won| DW["Update order\nfunds_status = direct, status = paid"]
E5 -->|status = lost| DL["Update order funds_status = reversed\nReverse the artist's share of the transfer"]
E6 --> L1[Log only — no order created\nOrders only created on checkout.session.completed]
SHook --> E7[charge.refund.updated]
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]
E9 --> DF1["Log the balance movement on the platform account\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 "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 their storefront is refused at checkout so no new orders can be placed. Suspension stops them from trading; it deliberately does not move money. The artist's share has already been transferred, and the platform's remaining exposure is the supplier bill and any dispute a buyer opens. Freezing funds the platform cannot reach would be theatre, so that code was removed rather than preserved.
7. Scheduled Jobs & Data Retention
Six daily cron jobs, none of which initiates a transfer. The transfer happens at checkout; the job that used to release escrowed funds was deleted with the escrow model it served.
Diagram source (Mermaid)
flowchart TB
Rec[Cron — daily 00:00\nGET /api/recommendations/send-pending]
Clean[Cron — daily 02:00\nGET /api/cron/cleanup]
Recon[Cron — daily 03:00\nGET /api/cron/reconcile]
Images[Cron — daily 05:00\nGET /api/cron/derive-images]
Notify[Cron — daily 06:00\nGET /api/cron/notifications]
Supplier[Cron — daily 07:00\nGET /api/cron/supplier-orders]
Rec --> R1[Send due buyer-recommendation emails\nOne per order, idempotent]
Clean --> D1[Delete old processed webhook records]
Clean --> D2[Delete expired email verification codes]
Clean --> D3[Delete abandoned checkout snapshots]
Clean --> D4[Delete unclaimed provisional accounts\nthat were never activated]
Images --> I1[Derive the responsive image ladder\nand purge superseded objects]
Notify --> N1[Retry undelivered notification emails\nwith backoff]
Supplier --> S1[Resubmit failed print handoffs]
Supplier --> S2[Re-read accepted orders whose\nshipment webhook never arrived]
Recon --> A1[Orders refunded in Stripe\nbut missing from our refunds ledger]
Recon --> A2[Open disputes re-read from Stripe\nand unanswered deadlines reported]
Recon --> A3[Lost disputes: reverse the artist's\nshare of the transfer]
Recon --> A4[Orders still in a pre-destination-charge\nfunds state — need manual resolution]Design choices here:
- The six are split by concern rather than bundled, so a failure in one cannot silently skip another. The retention cleanup and the recommendation sender used to share a run with fund release; only the fund release was removed.
- The reconciliation job reports rather than acts, with one deliberate exception: a lost dispute has to reverse the artist's transfer or the platform absorbs the whole chargeback. That reversal is idempotent and capped by what is left unreversed, so the audit can retry it daily. Everything else is surfaced — a refund that exists in Stripe but not in our records, an unmatched split — and left to a human. It exits with
ok: falseand a list of issues when it finds any. - It also keeps reporting orders left in pre-destination-charge funds states, so legacy rows cannot be quietly forgotten after the code that handled them was deleted.
- 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 application tables. Per-route rate limiting on every sensitive endpoint. An edge proxy layer enforcing CSRF protection, Argon2id-hashed passwords, auth guards, and a full suite of security headers on every request. A payment architecture built for made-to-order art: destination charges on the platform account, a $5 buyer fee, no commission on the artwork, the supplier paid from the same charge, and the artist's share transferred at payment. 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. And when a payment succeeds, the artist is paid in the same breath as the buyer — while the platform holds exactly what it needs to make and ship the piece.
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.