Work

Case studies

From appointment scheduling and calendar synchronization to estimate workflows and operational reporting, I build systems people rely on every day. Most of this work is internal business software for a single client, so these write-ups describe the problems and the approach without exposing data, screenshots, or proprietary code.

  1. 01Scheduling and messaging platformTwo-way Outlook sync, admin calendar, automated messaging
  2. 02Cross-system estimate and survey platformMulti-step forms across React, NestJS, and .NET
  3. 03Service operations data integrationSix-tenant sync and classification pipeline
  4. 04Production reliability and performanceConnection pools, build times, wasted retries

01 · Contract engineer, Canter Power Systems · May 2025 – present

Scheduling and messaging platform

Appointment booking and administration that stays in sync with the calendars staff already use.

  • React
  • TypeScript
  • NestJS
  • PostgreSQL
  • Prisma
  • Cronofy
  • Outlook
  • FullCalendar
My role
Built the scheduling service, its calendar integration, and its admin tools from the first version in May 2025, building on business rules from the earlier scheduler. I still maintain them.
Scope
React admin UI, NestJS API, PostgreSQL, two-way Outlook sync through Cronofy, SMS and email.
What changed
Bookable availability reflects staff calendars in real time, and staff manage schedules from one admin calendar.

Problem

Customers and staff both book appointments, but the people doing the visits manage their days in Outlook. Availability has to reflect both the published schedule and whatever is already on someone's calendar. A change made on either side has to show up on the other quickly and correctly, across time zones and for both virtual and in-home appointments.

Approach

  • Modeled locations, services, schedules, and recurring hours, including time zone rules for virtual and in-home services, and generated bookable slots from them, with tests around the edge cases.
  • Connected staff calendars through Cronofy (OAuth, calendar connections, webhook channels), so calendar events create, update, and remove appointments.
  • Invalidated cached availability whenever an appointment changes, so customers aren't offered a slot that was just taken.
  • Built an admin calendar with drag-to-reschedule, drive times, conflict checks, and role-aware permissions.
  • Added an audit trail, appointment confirmations, and automated SMS and email touchpoints.
Simplified architecture: an admin calendar and customer booking links call a NestJS scheduling API backed by PostgreSQL. The API syncs with staff Outlook calendars through Cronofy and sends SMS and email through messaging providers. Admin calendar React · FullCalendar Booking links customer-facing Scheduling API NestJS slots · sync PostgreSQL schedules · events Cronofy calendar API Outlook staff calendars SMS & email messaging providers webhooks 2-way sync
Simplified architecture. Dashed boxes are third-party services.

Constraints and decisions

  • The calendar is the source of truth for whether a person is busy. The scheduler reacts to calendar webhooks instead of asking staff to double-enter.
  • Time zones are part of the data model, not a display detail. Virtual and in-home services have different requirements.
  • Invalid states, like an active schedule with no linked calendar, are blocked in both the API and the UI so they can't reach production.

02 · Contract engineer, Canter Power Systems · 2025 – 2026

Cross-system estimate and survey platform

An estimate starts as a customer inquiry and goes through a site survey, equipment sizing, pricing, and a payment plan. Each step is captured by different people, at different times, in different services.

  • React
  • TypeScript
  • NestJS
  • C#/.NET
  • ASP.NET Core
  • SQL Server
My role
On a team building the platform, built features across the React UI, NestJS API, and .NET API, and delivered the October 2026 form redesign.
Scope
Four repositories: customer and sales-rep forms, site surveys and photos, load sizing, pricebook, payment schedules.
What changed
Estimates can be saved and resumed, photos can be taken on a phone mid-form, and drafts and records from older form versions keep working.

Problem

Quoting a home standby generator needs a lot of structured information: what the customer wants to power, what the site looks like, which unit fits the load, current pricing, and how payment will be scheduled. Customers and sales reps both fill parts of it in, often over several sessions. The data is spread across a TypeScript integration layer and a .NET core API, and it has to stay consistent as forms change.

Approach

  • Built multi-step customer and sales-rep estimate forms with saved drafts that stay compatible across form versions.
  • Implemented load calculations that feed generator selection.
  • Added structured site surveys with photos, including a "text me the link" handoff so someone on a computer can take the photos on their phone.
  • Built pricebook and payment-schedule administration in the React admin console, with role-restricted pricebook editing.
Simplified architecture: estimate forms, an admin console, and a phone photo handoff connect to a NestJS integration API and a C#/.NET core API, which persists to SQL Server. Estimate forms customer · rep Admin console pricebook · surveys Phone photo handoff Integration API NestJS Core API C#/.NET SQL Server relational data drafts · estimates persistence
Simplified architecture. Service names are generalized.

Constraints and decisions

  • A form redesign is also a data migration. Old drafts and older records had to keep working, which meant versioning both the inputs and the reports built on them.
  • Changes had to ship together across four repositories (client, integration API, core API, and admin UI), so contracts between services were settled before implementation.
  • The phone handoff created a new failure mode: the customer's computer, still open on an older copy, could overwrite the photos they had just added from their phone. Every save carries the revision it was based on, and the server refuses saves based on an outdated revision.
  • Uploaded photos are scoped to the lead they belong to and validated server-side, so the public form can't be used to reach other files.

03 · Contract engineer, Canter Power Systems · 2026

Service operations data integration

Synchronizing customer, equipment, and maintenance data from a field-service platform into internal reporting.

  • ServiceTitan API
  • NestJS
  • TypeScript
  • PostgreSQL
  • Background jobs
  • React
My role
Built the integration end to end, from the first API connection to the reporting UI. Ported a colleague's equipment and membership classification rules into the pipeline.
Scope
Six tenants: incremental sync, backfills, a daily classification pipeline, query APIs, reporting, and CSV exports.
What changed
Previously missing inactive customers now sync with their equipment history, and equipment and memberships are classified for reporting.

Problem

Service history lived in ServiceTitan across six tenants. Internal reporting was missing customers and couldn't reliably tell which equipment was a generator, which service visits were maintenance, or which customers were covered by a maintenance plan or warranty.

Approach

  • Built incremental synchronization across all six tenants, with a watermark that only moves forward so an interrupted run can't skip records.
  • Tracked down why customers were missing. The platform's list endpoint silently leaves out inactive customers unless asked for them explicitly, even though those customers still own real equipment and service history.
  • Combined the classification steps (equipment, service activity, membership types, warranty status) into one ordered daily pipeline, with a shared lock and a "last updated" freshness indicator in the admin UI.
  • Added chunked seeding so large backfills could run on a smaller staging database without dropping connections, leaving production behavior unchanged.
  • Built reporting UI, CSV exports with stable IDs for reconciliation, and a generator-unit browser for internal users.
Data flow: ServiceTitan across six tenants feeds NestJS sync and backfill jobs, which run classification and write to PostgreSQL, which powers reporting views and exports. ServiceTitan 6 tenants Sync & backfill NestJS jobs Classification equipment · plans PostgreSQL reporting data Reports UI · exports
Simplified data flow. Dashed box is a third-party platform.

Constraints and decisions

  • Multi-tenant source data means the same concept can be configured differently in each tenant, so classification has to account for that.
  • A third-party API's defaults are part of the contract. "List all customers" didn't actually mean all customers, and the same pattern had already appeared with another resource.
  • Large backfills run in staging first and are checked before anything touches production.

04 · Contract engineer, Canter Power Systems · 2025 – 2026

Production reliability and performance

Three problems that each had an obvious workaround and a less obvious root cause. The method was the same each time.

My role
Diagnosed and fixed each issue, from reproduction to the deployed fix.
Scope
NestJS and Prisma backend, React and Tailwind build pipeline, service-to-service error handling.
What changed
One shared connection pool per instance, a build that skips unused CSS, and invalid requests that fail once instead of retrying.
  1. ReproduceMake it happen on demand.
  2. MeasureFind where it actually breaks.
  3. TraceFollow it to the root cause.
  4. FixChange the cause, not the symptom.
  5. VerifyConfirm it in production.

Database connection exhaustion

Symptom
A NestJS service intermittently failed with PostgreSQL connection-limit errors (Prisma P2037).
Root cause
Seven feature modules each listed the Prisma client as their own provider. In NestJS that creates a separate instance per module, and each instance opened its own connection pool.
Fix
Made the existing database module global and removed the duplicate providers, so the whole app shares one client.
Result
Only the global database module provides the client now, so each instance runs a single shared connection pool.

Slow frontend builds

Symptom
Frontend builds were slow enough to hold up every deploy and review cycle.
Root cause
A safelist-generator plugin inherited from the original UI template wrote out every color and variant combination on each build, and Tailwind then had to compile all of it.
Fix
Removed the generator and let Tailwind's JIT scan the real source, then safelisted only the few classes actually built dynamically.
Result
The build now compiles only the classes the app uses. Follow-up changes restored the few dynamically built classes the scan couldn't see.

Retries that couldn't succeed

Symptom
A calling service kept retrying appointment-create requests that could never succeed.
Root cause
A request referencing a record that didn't exist hit an unhandled foreign-key error and came back as a 500. The caller correctly treats 500s as transient, so it retried.
Fix
Mapped foreign-key violations to a 400 that names the failed reference, and left other errors unchanged.
Result
Unit tests cover both paths. Invalid requests now fail once, with a clear error.

Earlier projects

From the teaching years

Smaller projects I built while studying and teaching, before my current production work. They're kept here for reference. Source is on GitHub, and the hosted demos have been retired.

  • Course Admin Panel

    A .NET MVC application built to demonstrate strongly typed ViewModels instead of ViewBag, with session management and Entity Framework Core.

    • C#
    • .NET MVC
    • EF Core
    • SQL Server
  • Bookstore Inventory Management

    Inventory control, order processing, and role-based employee access for a bookstore.

    • Python
    • Flask
    • MySQL
    • bcrypt
  • Flask Casino

    Blackjack, video poker, and roulette under one persistent account, with a timed chip allowance.

    • Python
    • Flask
    • MySQL