Back to Splitify

Technical Specifications

Engineering deep-dive into what powers Splitify under the hood.

At a Glance

8,000+
Lines of TypeScript
82
Source Modules
17
Routes (SSR + Static)
49
Unit Tests
3
Redux Slices
70+
Regex Patterns
14
Type Interfaces
5
Security Layers

Architecture: Server Shell + Client Islands

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 result

Tech Stack

Framework
Next.js 16 (App Router, Turbopack)
React Server Components, streaming SSR, file-based routing
Styling
Tailwind CSS v4.2
JIT compilation, @theme inline tokens, custom variant system
State
Redux Toolkit
Multiple slices, memoized selectors, client-only state
AI Engine
Google Multimodal AI
Vision model with structured schema-constrained output
Animations
Framer Motion
Layout animations, whileInView triggers, variant orchestration
UI Primitives
Radix UI + shadcn/ui
Accessible Dialog, Sheet, Slider — WAI-ARIA compliant
Typography
Syne + DM Sans + JetBrains Mono
3-font system: display, body, monospace
Testing
Vitest
49 tests, 8 receipt fixtures, parser edge cases

AI Extraction Pipeline

Receipt 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 state

Legacy Local Receipt Parser (700+ LOC)

The 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 locales

Image Preprocessing Pipeline

Before 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

Integer Arithmetic Money System

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)

Split Computation Engine

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 person

Security Architecture

The 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

State Management: Redux Toolkit

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 guard

Canvas 2D Share Card Generator

The 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 fallback

Design System: Aurora Ledger

Always-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.

Background
Surface
Mint
Iris
Error
Warning
Text
Border
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

Confidence Scoring & Validation

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 ceiling

Performance Optimizations

Every optimization targets perceived performance — the time between user action and visual feedback.

Streaming SSR + Progressive Hydration
Server-rendered HTML streams immediately. Client islands hydrate independently — navigation between split steps is near-instant because the shell is already painted.
Route Transition Skeletons
loading.tsx at /split/result shows shimmer cards during transition. Next.js auto-wraps the page in a Suspense boundary. Zero blank screen time.
Integral Image Preprocessing
Sauvola thresholding uses integral images for O(1) per-pixel mean/variance. Processing a 2048×2048 image (~4.2M pixels) takes ~200ms — fast enough to run before any network call.
Module-Level Worker Reuse
The Tesseract.js OCR worker is created once and reused across extractions. No worker teardown/recreation overhead on repeat scans.
Memoized Redux Selectors
Derived state (subtotals, per-person splits, assignment counts) is computed only when dependencies change. Avoids O(n²) recomputation on every render cycle.
Client Bundle Minimization
Presentation-only components never get "use client". Even when imported by a client component, the explicit absence documents intent and keeps them server-compatible for future extraction.

Type System & Data Contracts

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

Test Suite

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.

Test Framework
Vitest (ESM-native)
Test Cases
49 across 8 fixtures
Coverage Area
Receipt parser (pure functions)
Fixture Source
Real Tesseract.js OCR output

User Flow State Machine

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 notifications

Build & Deployment

Build Tool
Turbopack (Rust-based bundler)
Output
17 routes — all statically prerendered
Rendering
SSR streaming + static generation
Font Loading
next/font/google (zero layout shift)
Image Formats
JPEG, PNG, WebP, HEIC, HEIF
Deployment
Vercel Edge Network

Privacy by Architecture

Splitify 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.