Case study · Engineering · CrewM8.ai · Apr to Jul 2026

A three-person startup. The front end and the billing surface were mine.

CrewM8 is a managed deployment platform for AI agents. I joined Useful Ventures as an Agentic AI and Product intern, and by the end I owned everything a customer sees, plus the Stripe billing behind it.

101commits in four months
10merged pull requests
−3,054net lines, one migration
22commits, one rebuild PR

every number on this page traces to git history or a logged write-up

Context

A managed home for other people’s AI agents.

Bring your own model provider, connect a messaging channel, and CrewM8 provisions an isolated container that runs your agent continuously.

  • The team was three people. There is nowhere to hide on a team that size, and no one to hand things to.
  • I joined as a generalist and ended up owning the product’s entire presentation layer and its billing surface.
  • The tenure in numbers: 101 commits and 10 merged pull requests across four months.
crewm8.ai, the live marketing site
fig 01crewm8.ai, one part of the owned surface
APRjoined, team of three
APR–JUL101 commits, 10 PRs
JULfront end + billing, owned

What I owned · and what I did not

The front end, end to end. And a boundary I can defend.

  • The marketing site and the onboarding flow behind it.
  • The authenticated dashboard, rebuilt and shipped as one 22-commit pull request.
  • The design system it was all built on, through the migration that retired shadcn and Radix.
  • The Stripe integration: the billing surface, plus raw SQL and Drizzle queries against Postgres for account and credit resolution.

the boundaryBeing specific about it matters.

The infrastructure layer, the agent runtime, and most of the API belonged to another engineer. The CI/CD pipelines were written before my first commit.

My full claim there: I repaired the pipeline once, to unblock a merge. That is the extent of it.

Stack

Chosen to be reasoned about, not admired.

ApplicationNext.js, React and TypeScript. The in-house design system replaced shadcn and Radix once it reached v2, on our own tokens.
Data and billingPostgres through Drizzle ORM, with raw SQL where the query was easier to reason about directly. Stripe for billing.
Fly.io + Docker deploysDrizzle ORM and raw SQLStripe billing surface

Flagship one · the dashboard rebuild · one PR, 22 commits

Every screen used to refetch the world.

The authenticated dashboard fetched per screen. Each navigation requested the same data again and blocked the UI on it. The rebuild replaced the data layer underneath every screen.

01Mount-once context providers.

Data loads once when the dashboard mounts. Every screen reads from context instead of fetching for itself.

Replaced: a fresh fetch per screen, per visit.

02Stale-while-revalidate caching.

Screens render the cached copy immediately and revalidate in the background. The user never waits on data that has not changed.

Replaced: blocking spinners in front of stale-safe data.

03Progressive load with cleanup.

A blocking load became a progressive one: skeletons while data settles, and in-flight requests properly cancelled on navigation.

Replaced: one all-or-nothing load, with abandoned requests left running.

the performance claim here is deliberately withheld · see the honest caveat at the end

Flagship two · the design system migration

Minus 3,054 lines. Nothing the user saw changed.

I migrated the product onto the in-house design system and retired the shadcn and Radix primitives, rebuilding the dashboard primitives on our own tokens with no change to the shipped surface.

Deleting code without changing behavior is the strictest test of whether you understand it. The diff closed at 3,054 net lines removed.

shadcn and Radix retiredin-house tokens underneathno change to the shipped surface

How I work · the same gates, every merge

Three gates. No exceptions for small changes.

Small changes were where the regressions came from, so nothing merged without passing all three:

  1. 1
    tsc --noEmit at zero errors.Types are the first reviewer.
  2. 2
    A passing production build.Not the dev server. The thing that ships.
  3. 3
    Lint warnings held at the agreed baseline.Held across all 10 merged PRs. Never allowed to creep.
  • Every defect got a written entry: the root cause, two fix options with their costs, the gate that would verify the fix, and a logged RCA result.
  • That log compounds. It became the reference the next time something similar broke.
  • Before the largest release I read my own branch diff line by line and wrote up the findings. Items I chose not to fix were logged as tracked bugs, not quietly dropped.
P0 to P2, same protocoltwo costed options per fixRCA log per defectline-by-line pre-release diff read

One bug, close up · what the protocol catches

It looked like a server problem. It was a click.

draft deletion in the dashboard, root-caused and written up

Deleting a draft navigated away mid-request, which read like a backend failure. It was not. The click event was propagating from the delete control up to the enclosing card, which triggered a route change while the DELETE was still in flight. A React synthetic event bug, isolated on the client.

The write-up is the reason the same class of bug was easy to spot the next time it appeared.

One problem worth showing · sizing machines for headless Chrome

We assumed memory would be the ceiling. It was CPU.

We had to size the machines that would run headless browser workloads, and the working assumption was that memory would run out first. Instead of guessing, I stress-tested a single 1 vCPU instance to the point of failure.

  • Load average hit 51 on one vCPU while memory never became the constraint.
  • Per-tab memory fell from roughly 80MB to 30MB as the browser compacted itself under pressure.
  • Chrome raises its own renderers’ oom_score_adj, so the kernel kills renderers first and the process looks healthy while quietly losing its work.
25 to 30 tabsStable. The production operating range.
50 tabsThe instance can no longer recover on its own.
The callRelax CPU before memory. The inverse of what we had planned to buy.

the output was a production sizing table the machine purchase was made against

Impact

What four months left behind.

101commits, 10 merged PRs
−3,054net lines, zero visible change
0tsc errors, every merge
25–30tabs, the adopted sizing range

The billing surface shipped on Stripe. P0 to P2 defects closed with a logged RCA each. Lint stayed at baseline across every merged PR. And the sizing recommendation inverted what the company was about to buy.

The honest caveat

One claim I will not dress up.

The dashboard performance work is a structural argument. Mount-once providers, stale-while-revalidate, progressive loading: I believe in the shape of it. But I did not instrument it, so I have no before-and-after timing to show you.

I would measure it first if I did it again.

Get in touchSee more work