Skip to content
HL
Available
Case study / saaslive

FrameKraft / Jay's Frames

Order, payment and vendor-catalog system for a Houston frame shop, plus its storefront

Jay's Frames homepage with a photo of the shop interior, Get Instant Quote, Book Free Consultation and Track My Order buttons, and a contact bar with phone, address and hours
jaysframes.com, the shop's public storefront, where customers get a quote, book a consultation or track an order.

At a glance

Engagement
Contract full-stack developer on client-owned codebases
Timeline
Aug 2025 to Dec 2025
Commits
163 across both repos (103 in FrameKraft)
Platforms
Web: staff back-office app and public storefront
Status
Live at jaysframes.com and framekraft.cloud

Overview

FrameKraft (the app's UI says FrameCraft) is the back-office system for Jay's Frames, a custom framing shop in Houston Heights. It covers orders, customers, pricing, wholesaler catalogs, invoices, payments and finance. jaysframes.com is the shop's public storefront. I worked on both client-owned codebases as a contract developer from August to December 2025. Most of my work went into the payment flow, invoice email, the MongoDB data and auth layer, wholesaler imports and production deployment.

The challenge

The app began as a Replit prototype on PostgreSQL/Drizzle with Replit's OpenID login; moving it to Railway needed its own data and login layer, and the Docker/Railway deploy kept failing on TypeScript runtime, Vite config and module-export errors. Payments caused the most trouble. Framing orders are usually paid in parts: a deposit at the counter, sometimes by card and sometimes in cash, then the balance at pickup. The Stripe webhook, though, overwrote the order's paid amount instead of adding to it, and a deposit could be counted twice. Later, slow database connections could stall the order screens.

My role

Between 22 August and 6 December 2025 I made 103 of the commits in the FrameKraft repo. Those commits added the MongoDB layer: the connection module, the Mongoose models (25 collections) and the storage functions. I also wrote the MongoDB session auth routes, the Stripe reconciliation service, the payment-link UI, the unified email service with its Resend, AWS SES and Gmail providers, the wholesaler CSV import utility, the finance routes and the Dockerfile. The order form and kanban board were built by the owner and other contributors, and so was the later multi-vendor import CLI. On the order form, my changes were the deposit payment-method step and the dimension handling. On the storefront I worked on the AR/3D frame viewer (glTF models) and on SEO: Search Console data in the SEO dashboard, canonical and sitemap fixes, IndexNow and internal linking.

Architecture

  1. Clients

    • FrameKraft staff app

      React 18, Vite, TanStack Query, shadcn/ui, Tailwind

      Staff-only single-page app for orders, customers, pricing, wholesalers, invoices, receivables and finance.

    • jaysframes.com storefront

      React 18, Express, @google/model-viewer, glTF

      Public site with instant quotes, consultation booking, order tracking and a frame visualizer that ends in an AR view.

  2. Edge/Delivery

    • Hosting

      Railway, Docker, Nixpacks

      A container build that bundles the Vite frontend and serves it from the Express server.

  3. Services

    • Express API

      Express, TypeScript (run with tsx)

      REST API behind session auth, with rate limiting and a Helmet CSP that allows Stripe.js and Cloudflare.

    • Payment and reconciliation

      Stripe Checkout, webhooks, Checkout Sessions and PaymentIntents list APIs

      Creates a Checkout session for each order (full or partial payment), applies webhook events to the order, and re-syncs any payment a webhook missed.

    • Unified email service

      Resend, AWS SES, Gmail API

      Picks the email provider from configuration (Resend first, then SES, then Gmail, then a development fallback) and sends invoices, receipts and payment notifications.

    • Wholesaler CSV import

      csv-parse, csv-stringify, multer

      Checks uploaded wholesaler price lists against a fixed schema (category, unit type, stock status) before importing products, and serves a downloadable template.

  4. Data

    • Operational store

      MongoDB Atlas, Mongoose

      25 collections, including users, sessions, customers, orders, invoices, payments, wholesaler products, expenses and transactions.

    • Relational schema

      PostgreSQL, Drizzle ORM

      The original Drizzle schema from the Replit version, which still runs alongside the MongoDB layer.

  5. Third-party

    • Stripe

      Stripe Checkout, Stripe.js

      Hosted card payments. Each session carries order and payment-type metadata that links it back to its order or invoice.

Key decisions

  1. 01

    Payment links instead of an embedded card form

    I replaced the embedded Stripe card form with a Stripe Checkout link for each order. Staff can show the link as a QR code at the counter or send it by email or SMS. Card data goes through Stripe's hosted page, and the same link works in person and remotely. The cost is a redirect out of the app, and payments come back asynchronously, so they have to be reconciled.

  2. 02

    Payments added to the order, not overwritten

    The webhook used to overwrite the order's paidAmount, so a second payment replaced the first. Now each completed Checkout session adds its amount to paidAmount and appends an entry to a per-order payment history (amount, method, session ID, balance after). The order is set to 'paid' and 'confirmed' only when the balance due reaches zero, within a one-cent tolerance. Until then it stays 'partial'.

  3. 03

    Cash and card deposits handled separately

    The deposit step asks whether the deposit was paid in cash or by card. A cash deposit is recorded straight away. A card deposit creates a payment link once the order is saved and is tracked through paidAmount. Keeping the two paths apart fixed deposits being counted twice.

  4. 04

    Reconciliation as a backstop for webhooks

    A reconciliation service lists completed Stripe Checkout sessions and PaymentIntents from the last N days. Any that the webhook missed are applied to the matching order or invoice, using the same balance logic as the webhook. It runs every 15 minutes, and staff can also trigger it with a Sync Payments button. The order and dashboard screens refetch every 30–60 seconds, so webhook payments appear without a manual reload.

  5. 05

    Fail fast and retry on MongoDB

    To fix the Atlas timeouts of 90+ seconds, I cut server selection and connect timeouts to 5 s and the socket timeout to 10 s. I also capped the connection pool at 5, set primaryPreferred reads with retryReads, and wrapped the order queries in a retry helper. The helper retries only network and timeout errors, with exponential backoff starting at 500 ms. A stalled connection now fails in seconds instead of hanging the order screens.

  6. 06

    Invoice email that matches the printed invoice

    Customer invoice emails, sent through Resend, use the same sections as the printable invoice: bill-to, order lines, tax, deposit/paid and balance due. The artwork photo was not displaying when embedded as a base64 data URI. It is now sent as an inline CID attachment instead.

Results

  • Order payments run through Stripe Checkout links, including deposits and partial payments. Each order keeps a payment history, and order and invoice status update automatically.
  • Invoices and payment notifications go out by email through a single service that can use Resend, AWS SES or Gmail.
  • A MongoDB data layer (Mongoose models, storage functions, session-based auth with bcrypt) and finance endpoints for expenses, transactions and financial summaries.
  • Wholesaler price lists can be imported from CSV, with validation and a downloadable template.
  • The Railway/Docker production build was fixed, and MongoDB connection stalls now fail fast and retry instead of hanging the order screens.
  • On jaysframes.com: the AR/3D frame viewer, Search Console data in the SEO dashboard, canonical and sitemap fixes, IndexNow and internal linking.

Stack

Frontend
TypeScriptReact 18ViteTanStack Queryshadcn/ui (Radix)Tailwind CSSreact-hook-form + Zodqrcode.react
Backend
Node.jsExpresstsxHelmetexpress-rate-limitbcryptjsmultercsv-parse
Data
MongoDB AtlasMongoosePostgreSQLDrizzle ORM
Payments & email
Stripe CheckoutStripe webhooksResendAWS SESGmail API
Storefront
glTF / @google/model-viewerGoogle Search Console APIIndexNow
Infrastructure
RailwayDockerNixpacks

FrameKraft / Jay's Frames · screenshots

Jay's Frames homepage on a phone, with quote, consultation and order-tracking buttons stacked vertically
1 / 4The storefront on mobile.