A production SaaS platform replacing 5+ fragmented tools for a multi-vertical creative business
T Creative Studio LLC is a multi-vertical creative business in San Jose, CA spanning lash extensions and skin treatments, permanent jewelry welding, handmade crochet goods and 3D printing, and HR/business consulting. The owner (Trini Lam, Kevin Lam's sister) was running operations across a patchwork of disconnected tools—multiple Instagram accounts for DMs, Excel spreadsheets for client tracking, Zoho for CRM, Square for payments, and marketplace apps (GlossGenius, Vagaro, Booksy) for booking. No single platform supported her multi-vertical pricing models: duration-based lash appointments with deposits, walk-in jewelry welding, e-commerce with shipping, and hourly consulting invoicing. I designed and engineered a production SaaS platform from scratch to replace all of it.
Design and engineer a single unified application to consolidate booking, payments, loyalty, messaging, e-commerce, CRM, and compliance for T Creative Studio LLC — replacing GlossGenius, Vagaro, Square marketplace apps, Zoho, and Instagram DMs with one owned system that serves real clients and processes real payments.
Designed a 40-table Postgres schema organized by business domain (booking, payments, loyalty, messaging, commerce, CRM, compliance, operations) using Drizzle ORM with 35 tracked migrations — all monetary values stored in cents to avoid floating-point errors
Built a multi-vertical booking system supporting duration-based lash appointments with deposits, walk-in jewelry welding, e-commerce with EasyPost multi-carrier shipping, and hourly consulting invoices — each vertical with its own pricing model and booking flow
Implemented a double-entry loyalty points ledger (balance = SUM of transactions, never a denormalized counter) with admin-configurable reward catalog and a waitlist system using one-time UUID claim tokens with 24-hour auto-expiry
Integrated Square SDK v44 for webhook-driven payment reconciliation: order created at booking confirmation, Square payment.completed webhook auto-links payment, awards loyalty points, sends receipt via Resend, records in Zoho Books, and purchases EasyPost shipping label — all in one HMAC-verified handler
Built a unified inbox with Supabase Realtime for multi-channel messaging (internal, email, SMS via Twilio), replacing fragmented DMs across multiple Instagram accounts
Designed a consistent integration pattern across 9 external services: singleton client module, isXConfigured() guard for graceful degradation, sync_log table for audit trailing — app boots fully even when individual integrations are unconfigured
Implemented defense-in-depth security: Supabase RLS at the database layer, CSP + HSTS + X-Frame-Options headers, HMAC-SHA256 webhook signature verification, Cloudflare Turnstile bot protection on all public forms, JWT tokens for waiver signing links
Shipped 39 transactional email templates as React components (Resend + React Email), 128 test files across Vitest and Playwright E2E, and 6 engineering reference documents including a disaster recovery runbook
40-table Postgres schema with 35 migrations, organized by domain: booking, payments, loyalty, messaging, commerce, CRM, compliance, and operations
9 external integrations built to a consistent pattern (graceful degradation, sync_log audit trail): Square, Zoho CRM/Books/Campaigns, Resend, Twilio, EasyPost, Instagram Graph API, PostHog, Sentry
Double-entry loyalty points ledger, waitlist with one-time claim tokens (24hr expiry), full gift card transaction tracking—none of which any marketplace platform offered
542 commits | 997 TypeScript files | ~192K lines | 128 test files (Vitest + Playwright) | 35+ dashboard views (admin/assistant/client)
Quick answers to 'Why did you choose X?' questions
Drizzle generates type-safe queries with zero runtime overhead and no binary engine. Prisma requires a binary engine that adds cold-start latency on Vercel serverless. For complex analytics queries — multi-join aggregations, domain-level reports — Drizzle lets you drop to raw SQL with type inference intact. Prisma forces you to use its client or lose type safety. The migration workflow (schema-as-code with drizzle-kit generate) was also cleaner for a 40-table schema.
Managed Postgres with Row-Level Security, Realtime subscriptions, and built-in Auth meant zero DevOps overhead for a solo engineer. Self-hosting Postgres would have required managing backups, replication, and failover. Supabase handles all of that. An additional S3 backup cron job supplements Supabase's managed backups for full recovery options.
Trini was already processing in-person POS transactions through Square. Migrating to Stripe would have required purchasing a new physical terminal and retraining staff on a different in-person payment flow. The Square SDK integration kept existing hardware and workflows intact while adding online payment capabilities. Webhook-driven reconciliation made linking online bookings to in-studio payments clean even with Square's smaller developer ecosystem.
A denormalized balance counter is an audit nightmare. If a bug causes a double-award or missed deduction, you have no way to reconstruct how the balance reached its current state. A double-entry ledger records every transaction (earn, redeem, adjustment, expiry) with a signed amount. The balance is computed as SUM(amount), so the full history is always auditable and any discrepancy can be traced to its origin. The same pattern applies to gift card transaction tracking.
If you process inline and any step fails — loyalty award, Zoho sync, shipping label purchase — the whole handler throws and the external service (Square, EasyPost) retries. Retries can cause double-processing. By storing the raw event first and always returning 200, failures are recorded on the event row and can be manually replayed from the admin dashboard. This design means webhook processing failures never cause aggressive external retries.
Every decision has costs — here's how I thought through them
For a solo engineer building a business application, shipping production-grade auth without owning security-critical code is the right tradeoff. Would revisit for SAML/SSO if the business needed enterprise clients.
Drizzle has no binary engine overhead, generates migrations cleanly, and lets you write raw SQL without losing type safety. For 40 tables and complex multi-join reports, this was clearly the right call.
A landing page that loads fast beats a beautiful one that makes users wait. Procedural geometry delivered the 3D experience without the download cost.
tRPC adds real value with multiple frontends or a public API. For a single Next.js app with one frontend, Server Actions give equivalent type safety with significantly less ceremony.
Trini was already processing in-person payments with Square. Migrating to Stripe would have required replacing physical hardware and retraining staff. Operational continuity outweighed ecosystem size.
Microservices are justified by team scale and independent deployment needs. For a solo engineer on a single-tenant SaaS, the monolith's refactoring freedom and operational simplicity win. Would extract webhook processing and cron jobs first if horizontal scaling became necessary.
Retrofitting a multi-tenant RBAC system and commission engine into an existing schema is significantly more expensive than building it right the first time. The forward investment paid off when Trini expanded the business.
The hardest problems I solved on this project
When Square sends payment.completed, the handler needs to: find the booking via reference_id, create a payment record, award loyalty points, send a receipt email, record in Zoho Books, and if it's a shipped order, purchase an EasyPost label. Any step could fail without the others knowing, and Square retries on non-200 responses.
All inbound webhooks are stored raw in a webhook_events table immediately upon receipt, and always return 200 to the sender. The event row tracks attempts count and last error message. Processing failures are recorded on the event row so they can be replayed from the admin dashboard without triggering external retries.
Lesson: Store the raw event first, process second. Never let processing failures affect the HTTP response to the external service.
GlossGenius, Vagaro, and Booksy assume one business type per account. T Creative needs: duration-based lash appointments with deposits, walk-in jewelry welding, e-commerce with shipping, and hourly consulting invoicing. Each model has different deposit rules, duration logic, and payment flows.
Designed the bookings table with a flexible service reference and snapshotted the price at booking time — not the current service catalog price. Each vertical has its own booking flow on the frontend, but the underlying data model is unified. Price snapshots ensure historical records remain accurate even when the service catalog changes.
Lesson: Price snapshots at booking time are non-negotiable. Never recalculate historical prices from the current catalog — price changes will corrupt your revenue reporting.
When a cancellation opens a slot, the next waitlisted client needs a one-time link to claim the slot within 24 hours. Two clients could potentially claim the same slot simultaneously if the token validation and booking creation aren't atomic.
UUID claim token is validated and booking is created in the same database transaction — if the token is expired or already used, the transaction rolls back. A cron job handles token expiry and offers the slot to the next person in the queue. The token is marked used atomically with booking creation, preventing race conditions.
Lesson: Token validation and booking creation must happen in the same database transaction. Checking then acting in separate queries creates a race window that will eventually be exploited.
Want to discuss this project?