Skip to main content
Alvin QuachFull Stack Developer
HomeProjectsExperienceBlog
HomeProjectsExperienceBlog
alvinquach

Full Stack Developer building systems that respect complexity.

Open to opportunities

AQ

Projects

  • All Projects
  • Hoparc Physical Therapy
  • OpportunIQ
  • Hoop Almanac
  • SculptQL

Knowledge

  • Blog
  • Experience
  • Interview Prep

Connect

  • Contact
  • LinkedIn
  • GitHub
  • X

Resources

  • Resume
© 2026All rights reserved.
Back to Projects

T Creative Studio

A production SaaS platform replacing 5+ fragmented tools for a multi-vertical creative business

Solo Engineer
Team of 1
Jul 2024 – Jan 2025
Source

The Problem

Situation

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.

My Goal

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.

My Approach

1

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

2

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

3

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

4

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

5

Built a unified inbox with Supabase Realtime for multi-channel messaging (internal, email, SMS via Twilio), replacing fragmented DMs across multiple Instagram accounts

6

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

7

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

8

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

The Outcome

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)

Technical Decisions

Quick answers to 'Why did you choose X?' questions

Q1Why Drizzle ORM over Prisma?

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.

Q2Why Supabase over self-hosted Postgres?

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.

Q3Why Square over Stripe?

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.

Q4Why a double-entry ledger for loyalty points instead of a balance counter?

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.

Q5Why store webhook events before processing instead of processing inline?

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.

Key Trade-offs

Every decision has costs — here's how I thought through them

Supabase Auth over custom auth

Gained

  • +Production-grade auth with RLS integration out of the box
  • +Google OAuth, magic link, and email verification without writing security-critical code
  • +No custom session management to maintain or audit

Gave Up

  • −Coupled to Supabase's auth flow and token format
  • −Cannot customize session management as deeply as a custom implementation

Why Worth It

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 ORM over Prisma or raw SQL

Gained

  • +Type-safe SQL with zero runtime overhead and no binary engine
  • +Schema-as-code with drizzle-kit migration generation
  • +Can drop to raw SQL with type inference intact for complex analytics queries

Gave Up

  • −Smaller ecosystem and fewer tutorials than Prisma
  • −More initial setup than Prisma's auto-generated client

Why Worth It

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.

Procedural Three.js geometry over GLTF models for the 3D landing page

Gained

  • +Zero network cost — geometry generated on GPU
  • +Loads fast on mobile without a loading bar
  • +No binary assets to version control

Gave Up

  • −Lower visual fidelity than hand-modeled GLTF scenes

Why Worth It

A landing page that loads fast beats a beautiful one that makes users wait. Procedural geometry delivered the 3D experience without the download cost.

Next.js Server Actions over tRPC or REST for mutations

Gained

  • +Type safety between client and server with zero boilerplate
  • +No additional abstraction layer to maintain
  • +Webhook endpoints still use API Routes where needed

Gave Up

  • −Less explicit API contract than tRPC
  • −Next.js-specific, reducing portability if frontend ever changes

Why Worth It

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.

Square SDK over Stripe

Gained

  • +No hardware migration — existing in-studio POS terminal kept
  • +No staff retraining required
  • +Webhook-driven reconciliation links online and in-person payments seamlessly

Gave Up

  • −Square's developer ecosystem is smaller than Stripe's
  • −SDK documentation is less comprehensive

Why Worth It

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.

Monolith on Vercel over microservices

Gained

  • +Free to refactor across the entire codebase
  • +Shared types between all modules with no cross-service contracts
  • +Atomic deployments with zero inter-service communication overhead

Gave Up

  • −Cannot scale individual services independently
  • −Webhook processing and cron jobs share the same deployment unit as the UI

Why Worth It

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.

Building the staff/RBAC system before Trini hired any staff

Gained

  • +Infrastructure ready when business growth required it
  • +No costly schema migration after launch
  • +Assistant portal and commission tracking shipped as first-class features

Gave Up

  • −Engineering time spent on features not immediately needed

Why Worth It

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.

Challenges & Solutions

The hardest problems I solved on this project

1Multi-step webhook reconciliation without lost or double-processed events

Approach

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.

Solution

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.

2Supporting four incompatible pricing models in a single booking system

Approach

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.

Solution

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.

3Waitlist claim tokens with race condition prevention

Approach

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.

Solution

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.

What I Learned

  • →Build-vs-buy analysis pays off when a business has multi-vertical needs marketplace platforms can't address without costly workarounds
  • →Double-entry ledger patterns apply beyond accounting — loyalty points, gift cards, any auditable balance benefits from a transaction log over a denormalized counter
  • →Building the staff/RBAC system before staff are hired costs less than retrofitting multi-tenant access control into an existing schema after launch
  • →Consistent integration patterns (singleton client, isXConfigured() guard, sync_log) make the difference between a system that fails gracefully and one that breaks silently
  • →Defense-in-depth is worth the setup cost: application-level auth checks can have bugs; database-level RLS cannot be bypassed by application code

Future Plans

  • +Native mobile app (React Native + Expo) so Trini can manage bookings and view the dashboard on-device without a browser
  • +Multi-location support for studio expansion — current schema is single-tenant but designed to extend with a locations table and location-scoped RLS policies
  • +AI-powered scheduling optimization to suggest optimal appointment slots based on historical booking patterns and service duration data
  • +SAML/SSO integration for enterprise consulting clients who need to authenticate via their own identity provider

Want to discuss this project?

Get in TouchView More Projects