Engineering deep-dive into what powers Splitify under the hood.
Splitify uses a hybrid server/client rendering architecture. Every page.tsx and layout.tsx is a React Server Component — enabling streaming HTML, zero-JS metadata exports for SEO, and smaller client bundles. Interactive UI is isolated into "client islands" — dedicated components marked with "use client" that hydrate independently.
This means the initial page load delivers fully-rendered HTML before a single byte of JavaScript executes. Interactive regions (form inputs, animations, state management) hydrate progressively — the user sees content immediately while interaction layers bootstrap in parallel.
Server Component Tree
─────────────────────
Root Layout (Server)
├─ <html> + <body> + font preloads + JSON-LD structured data
├─ State Provider (Client boundary)
│ └─ Scoped to subtree — server components above are unaffected
│
└─ Split Review Page (Server)
├─ export const metadata ← SEO, zero JS cost
└─ <ReviewFlow /> ← Client island
├─ State subscription → Redux store
├─ Item cards → Framer Motion + touch handlers
├─ Edit modal → Radix Dialog + form state
└─ Confidence banner → conditional render from extraction resultReceipt extraction is powered by a multi-stage AI vision pipeline that converts a raw photograph into structured financial data. The image goes through client-side validation, server-side inference via a multimodal AI vision model, response normalization, and confidence-scored dispatch into the client state tree.
Receipt Extraction Pipeline
══════════════════════════
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Client │ │ Server │ │ Multimodal │
│ Upload UI │────▶│ API Layer │────▶│ AI Vision │
└──────┬──────┘ └──────┬───────┘ └────────┬────────┘
│ │ │
File validation Multi-layer Structured JSON
Type & size security + Schema enforcement
checks rate limiting Multi-currency
Encoding + validation Discount detection
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────┐
│ Response Normalization │
│ ├─ Converts to integer minor-unit representation │
│ ├─ Normalizes charges (tax, tip, discounts) │
│ ├─ Reconciles totals against receipt values │
│ └─ Handles currency-specific edge cases │
└──────────────────────────────────────────────────────────┘
│
▼
Dispatch to client stateThe app also includes a complete local OCR fallback — a deterministic, regex-based receipt parser that operates entirely in the browser with zero network dependency. It uses a multi-pass architecture to classify, extract, and validate receipt data with proprietary heuristics tuned on hundreds of real-world receipts across multiple countries and formats.
Local Receipt Parser — Multi-Pass Architecture
═══════════════════════════════════════════════
Pre-pass: Line normalization & wrapping recovery
Pass 1: Line classification (70+ regex patterns)
Categories: item, subtotal, total, tax, discount, noise
Pass 2: Field extraction (price, quantity, name)
Multi-strategy quantity detection
Currency-aware price parsing
Pass 3: Confidence scoring & cross-validation
Per-item multi-factor penalty model
Statistical outlier detection
Subtotal reconciliation
Supports: 10+ tax systems (GST, CGST/SGST, VAT, HST,
PST, Sales Tax, Service Charge) across multiple localesBefore OCR, every receipt image goes through a 5-stage preprocessing pipeline that runs entirely in the browser using the Canvas API. The goal: normalize wildly different input conditions (shadows, flash glare, crumpled paper, variable lighting) into clean black-and-white text.
Image Preprocessing Pipeline (~200ms on modern hardware) ═══════════════════════════════════════════════════════ Stage 1: EXIF Orientation Correction └─ Auto-rotates based on camera metadata Stage 2: Aspect-Preserving Downscale └─ Capped resolution to prevent mobile GPU pressure Stage 3: Weighted Luminosity Grayscale └─ Industry-standard perceptual weighting (ITU-R BT.601) Stage 4: Adaptive Thresholding ├─ Proprietary parameter tuning for receipt conditions ├─ Handles: shadows, flash glare, uneven lighting, crumpled paper ├─ Integral image construction for O(1) per-pixel computation └─ Total complexity: O(w×h) — linear in pixel count Stage 5: Export via OffscreenCanvas └─ Web Worker compatible — no main thread blocking
Splitify never uses floating-point for money. All financial values are stored as integers in the currency's minor unit (cents for USD, paise for INR, yen for JPY). This eliminates the infamous 0.1 + 0.2 ≠ 0.3 problem entirely. Every calculation is performed in integer space with explicit rounding control.
Integer Money Arithmetic ═══════════════════════ Representation: $18.50 → 1850 (minor units) ₹320.00 → 32000 ¥1500 → 1500 Tax rates stored in basis points (1 bps = 0.01%) Even Split — Remainder-safe distribution: split(1000, 3) → [334, 333, 333] // sums to exactly 1000 Distributes remainder pennies fairly across people Handles negative amounts (discounts) with sign-aware logic Proportional Allocation: Weighted distribution based on each person's item subtotals Uses integer-safe allocation that guarantees exact sum No pennies lost — every cent is accounted for Percentage Charges: Tax/tip amounts computed with controlled rounding Basis-point precision avoids floating-point drift Currency-aware formatting: Supports currencies with 0–4 decimal places Examples: USD (2), JPY (0), BHD (3), CLF (4)
The split engine supports two modes — even split and item-based split — each handling an arbitrary number of dynamic charges (taxes, service charges, tips, discounts) with per-charge split strategy selection.
Item-Based Split Engine
══════════════════════
Three-phase computation:
1. Compute per-person item subtotals from assignments
(shared items split fairly with remainder handling)
2. Distribute each charge across people
(supports both equal and proportional strategies)
3. Assemble final per-person totals with full breakdown
Invariant: Σ(all person totals) === grand total
No rounding errors. No floating point. No pennies lost.
Charge types supported:
CGST, SGST, IGST, HST, PST, GST, VAT, Sales Tax,
Service Charge, Gratuity, Processing Fee, Surcharge,
Cess, Discounts, Coupons, Promos, Rounding adjustments
Each charge can be:
• Positive (tax, tip) or negative (discount, coupon)
• AI-extracted from receipt or manually added by user
• Split proportionally by item value or equally per personThe API route is hardened with 5 layers of defense to prevent unauthorized use of the AI extraction endpoint and protect against abuse.
API Security (5 defense layers) ═══════════════════════════════ Layer 1: Origin Validation ├─ Validates request origin against allowlist └─ Rejects cross-origin requests in production Layer 2: IP-Based Rate Limiting ├─ Sliding-window rate limiter per client IP ├─ Automatic stale entry cleanup └─ Throttled responses with retry headers Layer 3: Client-Side Credit System ├─ Per-device usage quota (sliding window) ├─ Credit badge in UI shows remaining count └─ Exhausted → dedicated screen, no API call made Layer 4: Payload Validation ├─ Request size limits enforced ├─ MIME type whitelist for image uploads ├─ Input format validation └─ Oversized payload rejection Layer 5: Schema Enforcement ├─ AI response constrained by strict JSON schema ├─ Required fields validated at runtime └─ Type-checked response mapping before dispatch
Three Redux slices manage the entire application state with 25+ actions and memoized selectors that derive computed values on demand — never stored in state.
Redux Store Architecture
═══════════════════════
store
├─ Bill Data Slice (persistent across navigation)
│ ├─ Selected currency
│ ├─ People list (name, assigned color)
│ ├─ Extracted line items (name, quantity, price)
│ ├─ Item-to-person assignment map
│ ├─ Dynamic charges array (tax, tip, discounts)
│ └─ Split mode selection
│
├─ UI Slice (ephemeral state)
│ ├─ Current step index
│ ├─ Modal open/close flags
│ └─ Active editing context
│
└─ OCR Slice (extraction lifecycle)
├─ Pipeline status (multi-phase state machine)
├─ Progress tracking (phase, percent, label)
├─ Full extraction result with metadata
└─ Error state
Derived State (never stored — computed by memoized selectors):
Subtotal → Σ item prices
Charges total → Σ charge amounts (incl. negative discounts)
Grand total → subtotal + charges
Per-person even split → remainder-safe distribution
Per-person item split → full breakdown with charge allocation
Assignment progress → unassigned count, completion guardThe share card is rendered entirely with the Canvas 2D API — not html2canvas (unmaintained since 2022, breaks with CSS variables and hashed font names). The generator produces a retina-resolution PNG with pixel-perfect typography, brand colors, and dynamic height based on party size.
Share Card Rendering Pipeline
════════════════════════════
Resolution: 420px × dynamic height, 2× retina scale (840px canvas)
Font loading: waits for web fonts to be fully available
Font resolution: resolves hashed font names from CSS at runtime
Layout engine:
├─ Header: brand + receipt icon + currency chip
├─ Micro-line: date + person count
├─ Person rows: colored dot + name (truncated) + monospace total
├─ Gradient divider: 4-stop linear gradient
├─ Total row: display font label + mono total
└─ Watermark: centered brand at 50% opacity
Cross-platform fixes:
├─ SVG via data: URL (avoids blob: URL issues on mobile Safari)
├─ roundRect() polyfill for older WebKit
└─ Text truncation with measureText() + ellipsis
Export: canvas.toBlob("image/png")
Share: Web Share API → clipboard fallback → auto-download fallbackAlways-dark theme with a custom design token system. Two accent colors — Mint for primary actions and success, Iris for AI features and secondary interactions. 8 person colors rotate for multi-person assignment.
Typography System ═════════════════ Display/Headings: Syne (variable weight) Body/UI text: DM Sans (400/500/600) Numbers/Code: JetBrains Mono Custom CSS Effects: • Glassmorphism surfaces with backdrop blur • Gradient text fills (mint + iris spectrums) • Glowing ring box-shadows for focus states • Radial gradient background mesh • SVG noise texture overlay • Animated loading shimmers • Pulsing glow animations • Floating keyframe effects • Multi-tier shadow elevation system
Every extracted item receives an individual confidence score, and the overall extraction is cross-validated against the receipt's subtotal. The scoring system uses a multi-factor penalty model with 7 independent deduction criteria.
Confidence Scoring Model
═══════════════════════
Per-Item Score (multi-factor penalty model):
┌──────────────────────────────────────────┐
│ 7 independent deduction criteria: │
│ • Invalid or suspiciously high price │
│ • Abnormally short item name │
│ • High numeric-to-alpha ratio in name │
│ • Multiple ambiguous price candidates │
│ • OCR artifact characters detected │
│ • Implausible quantity values │
└──────────────────────────────────────────┘
Weighted penalties produce: HIGH / MEDIUM / LOW
Overall Confidence:
1. If no items extracted → LOW
2. Cross-validate item sum vs. extracted subtotal
(deviation thresholds determine confidence band)
3. If majority of items are individually LOW → overall LOW
4. Presence of any LOW items caps overall at MEDIUM
Price Outlier Detection:
Statistical filter using median-based deviation
Removes extreme outliers from the item set
Tax Rate Inference:
When rate is missing but subtotal + tax amount exist,
the rate is back-computed and sanity-checked
against a maximum ceilingEvery optimization targets perceived performance — the time between user action and visual feedback.
End-to-end TypeScript with strict mode. 14 core type interfaces enforce data contracts across the entire pipeline — from Gemini API response to Redux state to Canvas rendering.
Core Type Categories (14 interfaces) ════════════════════════════════════ Financial Layer: Money type → branded integer (never float) Currency → code, display name, symbol, decimal precision Line item → identity, name, quantity, price Charge → identity, label, amount, rate, origin, split strategy Person result → subtotal, charge breakdown, total, item list AI / Extraction Layer: API response → items, charges, subtotal, total, currency hint Extraction result → items, per-item metadata, charges, confidence Item metadata → confidence level, ambiguity flags Progress → phase, percent, display label UI Layer: Person → identity, display name, color assignment Split mode → even or item-based (union type) Confidence → 3-tier enum (high / medium / low) Pipeline status → 6-state machine (idle → done | error) Share card data → people, totals, currency, date
49 Vitest unit tests cover the receipt parser across 8 real-world receipt fixtures (captured from Tesseract.js output). Tests verify item extraction, price parsing, quantity detection, noise filtering, subtotal/tax/total extraction, confidence scoring, modifier code handling, and edge cases like wrapped lines and zero-decimal currencies.
User Flow — 10 routes, 5 logical steps
═══════════════════════════════════════
Landing (/)
└─ "Start Split" → Currency Picker Sheet
└─ Select currency → store in state
└─ Step 1: Add people (2–20)
└─ Step 2: Capture/upload receipt photo
│ ├─ File validation (type, size)
│ ├─ Credit quota check
│ └─ Preview thumbnail
└─ Auto: AI extraction pipeline
│ ├─ Progress bar (0–100%)
│ ├─ Rotating hint labels
│ └─ Error state → retry or manual entry
└─ Step 3: Review extracted items
│ ├─ Confidence badges (HIGH/MEDIUM/LOW)
│ ├─ Subtotal cross-check banner
│ ├─ Edit/delete items inline
│ └─ Add items manually
└─ Step 4: Choose split mode
├─ 5a: Even split
│ └─ Tax/tip adjustment
│ └─ Results
└─ 5b: Item assignment
└─ Toggle people per item
└─ Tax/tip config
├─ Extracted charges (read-only)
├─ User tip (editable slider)
└─ Per-charge split strategy toggle
└─ Results
├─ Per-person breakdown
├─ Share card (Canvas PNG)
├─ Copy text summary
└─ Toast notificationsSplitify is designed so that it physically cannot store your data. There is no database, no user accounts, no server-side session. All bill data lives exclusively in the browser's state store — closing the tab destroys everything. The receipt image is sent to our AI service for real-time extraction and is not retained beyond the API call.
The receipt image exists only in volatile memory — not persisted to any storage layer. It is discarded automatically when the session ends. This is by design: your data is ephemeral.
Built with obsessive attention to detail.