Skip to content
М

Boston, MA

AI & software engineer

Matt Serdukoff

Case study · 2024–Present · AI Engineer / Full-Stack Developer at Hime

Wheelbase

Dealership software built at the pace of a live auction floor. One Turborepo monorepo ships a web app, an Electron desktop app, an Expo field app, a Go backend, and an AI agent runtime.

Deployment topologyNginx edge · Docker · Dokploy
  • /→ FrontendTanStack Start SSR · React 19 · tRPC
  • /landing→ LandingVite static · waitlist + onboarding
  • /api/backend/*→ Go APIGin · CGO + 2GB SQLite · MinIO · Remotion
  • /_agent→ Agent proxyBun → OpenClaw runtime + plugins

Supabase Postgres underneath everything: RLS, pgvector, Realtime, Vault, Edge Functions. GitHub Actions deploys only the services whose paths changed.

01 · Retrieval

Retrieval that makes decisions

The RAG layer here does not chat with documents. It embeds vehicles, retrieves the ones that fill a dealer's real inventory gaps, and feeds that signal into ranking that buyers use on the auction floor.

  • Every car becomes canonical text ("2020 Toyota Sienna LE Minivan") with a 768-dim embedding under an HNSW index and a weighted tsvector under GIN.
  • Embeddings come from a provider chain: Gemini, then OpenAI at 768 dimensions, then a local sparse-feature fallback, so ranking degrades instead of failing.
  • Semantic and lexical results are fused with Reciprocal Rank Fusion, which handles conceptual queries ("work truck") and exact ones ("F-150") alike.
  • Retrieval runs over the whole demand matrix, not one query. Each underfilled category is embedded and searched, weighted by its gap ratio, and aggregated.
  • Vehicle archetypes are embedded once by year, make, model, and body style, then reused across every matching car to cut embedding cost.
gap_weight = (target - current) / target
fit        = Σ gap_weight × rrf(vector, full_text)
raw_imx    = 0.7 × fit + 0.2 × mileage + 0.1 × age
imx        = normalize(raw_imx) within the runlist

IMX, the Inventory Match Index. Retrieval supplies relevance; deterministic rules for pricing bands, recency, aging, and wholesale risk align it with how a dealership actually operates.

02 · Governance

An assistant that cannot leave its tenant

Operators ask questions in plain English and get answers from their own data. Letting a model write SQL against a multi-tenant database only works if the model physically cannot see anyone else's rows.

  • Prompts become a tenant-scoped, SELECT-only query DSL executed through ai_execute_query, a hardened security-definer Postgres function tightened over several migrations.
  • Writes are risk-classified. Destructive operations require an approval token from a human before they run.
  • Every query and write lands in telemetry tables, so any agent action can be audited after the fact.
  • Context documents for the user, dealership, and team go through a draft and publish step before they can change agent behavior.

03 · Offline

VIN decoding without the network

A paid third-party API became a self-contained Go service over a ~2GB NHTSA SQLite database: about 1.6M pattern rows and 8.7M valid-character rows, zero external calls per decode.

  1. 01Model yearPosition 10, resolved with 30-year cycle logic.
  2. 02ManufacturerWMI lookup for make and manufacturer.
  3. 03Schema discoveryFind the VIN schemas that apply to this WMI and year.
  4. 04Multi-pass VDS matchPositions 4–8 for body style, engine, drive type, and model.
  5. 05Trim refinementVehicle spec pattern rules narrow to a trim.
  6. 06CorrectionCheck-digit validation, single-character auto-correction, ranked candidates.

A custom pattern parser instead of regex, in-memory caches for elements and error codes, and streaming row reads throughout. The container pulls the database from MinIO at startup.

04 · Performance

Python to Go, with nothing broken

A core import that populated thousands of rows took up to two minutes. Fixing the FastAPI path got it to 5–25 seconds. Rewriting the services in Go with Gin, plus caching and data-model changes, cut database load by more than 80% and brought responses under a second, with zero breaking changes for the clients.

05 · Decisions

Other calls worth explaining

  • Streaming ETL that cleans up after itself

    Auction runlists stream row by row through csv.Reader at constant memory. Column mappings are per-auction configs in Supabase, so dealers add a new auction house without a code change. Inserts go in 500-row batches with client-side UUIDs; if linking cars to the runlist fails, the just-inserted cars are deleted so nothing is orphaned.

  • One API, two transports

    tRPC procedures are split into a shared router, safe over both HTTP and Electron IPC, and a cloud-only router for embeddings and heavy IMX compute. The desktop app runs the full shared API in-process with zero duplicated business logic.

  • A desktop app that supervises its own runtime

    The Electron main process spawns the bundled Go binary, launches the OpenClaw agent with loopback-only trusted-proxy auth, relays OpenRouter credentials locally, and manages Chrome for Testing for browser automation.

  • Collaboration without a collaboration server

    The document vault uses TipTap with Yjs CRDTs, relayed over Supabase Realtime channels. Concurrent edits merge conflict-free, with debounced autosave, version history, and point-in-time restore.

  • Secrets outside application tables

    Each user's OpenRouter key is encrypted in Supabase Vault. The Go backend mints and revokes keys through Vault RPCs, so keys never appear in an application query.

  • Isolation enforced by the database

    Every core table carries tenant_id, and RLS binds auth.uid() to the user's tenant inside Postgres. The mobile app talks to Supabase directly with RLS as its only boundary, which only works because the policies are the real security layer.