FrameKraft / Jay's Frames
Order, payment and vendor-catalog system for a Houston frame shop, plus its storefront

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
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.
Edge/Delivery
Hosting
Railway, Docker, Nixpacks
A container build that bundles the Vite frontend and serves it from the Express server.
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.
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.
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
- 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.
- 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'.
- 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.
- 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.
- 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.
- 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