DEV Community

Isaiah Kim
Isaiah Kim

Posted on

How ParseRail turned features into capabilities

The endpoint catalog on ParseRail had a wrong number in it. Google Document AI was listed at a throughput that was off by 10x, and Mindee wasn't in the table at all. Fixing that meant editing one comparison row — a small thing that took an afternoon and changed how I think about the rest of the studio's work.

Here's why. That row isn't marketing copy. It's the published benchmark for a capability that a dozen of my own products call. When BenchFile parses a NYC energy-benchmarking filing, when CoverCheck reads a vendor's certificate of insurance, when Truing pulls figures out of a payroll register — they're all leaning on the same document-extraction path that the ParseRail catalog describes. Getting the number right on the marketing surface and getting it right in the routing logic are the same job, because there is one implementation underneath both.

The decision: solve it once, at the seam

I run Kynth Studios as a Turborepo monorepo on Next 16, Supabase and Stripe. Every product in it needs some intersection of the same five hard things:

  • capture a demo of itself without a human driving the browser
  • narrate that demo
  • render it to video
  • send a message to a real person
  • take a payment

For a while I built those per product. The honest reason was that each product's version felt different enough to justify it — BenchFile's demo walks a filing flow, CoverCheck's walks a COI upload, so surely the capture scripts want to be different. They don't. The route is different. The capability is identical: drive a real Chrome, wait until the page has actually settled, take the shot.

So the shape I settled on is capability-first. A capability is a thing with one implementation, one place it can break, and a contract narrow enough to write down:

// One contract. Products supply the route, not the mechanics.
export interface DemoCapture {
  capture(route: string, opts?: { settle?: SettleRule }): Promise<Shot[]>;
}

export interface Narrate {
  speak(script: string, voice: VoiceId): Promise<AudioTrack>;
}

export interface Notify {
  send(msg: OutboundMessage): Promise<Receipt>;
}
Enter fullscreen mode Exit fullscreen mode

Nothing clever there. The value is entirely in the fact that capture has exactly one body.

How ParseRail turned features into capabilities — code

What "one body" bought me

The demo capture runs Playwright against a real browser, no human in the loop. The interesting part isn't launching Chrome, it's deciding when the page is done. My first versions used timeouts, which is the obvious move and the wrong one — a timeout encodes your laptop's speed into your test suite. The version that stuck watches for a deterministic settle condition instead: the network is quiet, the animation frame budget is clean, the elements you named are present. That logic is fiddly. It took real work to get right.

It took real work once. Every product's demo recording inherits it. When I improved settle detection, every demo in the studio got better on the next run, including ones I hadn't touched in months.

Narration went the same way. The pipeline started on a paid TTS provider, which is fine at one product and a line item at many. Swapping in free edge-tts was a single-file change because narration was already behind Narrate — the products calling it don't know or care which voice engine answers. If it had been inlined in each product's video script, that swap would have been a week of tedium and a week of drift.

The animation engine, HyperFrames, is the same bet made at a harder problem. Video rendering wants to be seekable — you need frame 900 without playing frames 0 through 899, or your render is serial and slow and your preview is useless. That constraint shapes the whole engine: one paused timeline, deterministic at every frame, no animation state that depends on having been played through. Getting seek-safety right was the expensive part. Nobody writing a composition thinks about it anymore.

Where it shows up

The clearest signal came from the Framer capture-and-own pipeline. I'd port a template to owned Next code, and the part that used to be a whole project — landing page, checkout, demo, share card — became mostly configuration, because payments were already a capability and demo capture was already a capability. What was left was the actual product: the domain logic, the thing a buyer pays for.

FetchDue is the sharpest test of that. It went live the weekend of July 18th — an AI collections agent that chases overdue invoices from your own mailbox, in your voice, reads every reply, drafts the payment plan, and never sends anything without your approval. The novel work is all in the reading and the drafting and the approval gate. The email delivery, the Stripe billing at $29/mo, the one-click live demo with no signup form — those were assembled. That's the whole reason a one-person studio can ship something with an approval gate in it: the boring surface area was already finished.

I launched it on a Saturday on purpose. With no customers yet, timing is a story you tell yourself. The proof I offer instead of testimonials is the live product and a recorded real run — a real reply classified, a real answer drafted, a real email delivered — and I can record that run because recording runs is a capability.

parserail-landing

The trade-off, stated plainly

Capabilities cost you at the seam. Every one of those interfaces is a place where a product wants something the contract doesn't express, and you either widen the contract for everyone or you special-case and lose the single body. I've done both badly. The rule I use now: a capability earns its abstraction on the third product that needs it, not the first. Two implementations is a coincidence. Three is a shape.

And you inherit bugs as fast as you inherit fixes. When settle detection had an edge case, every demo had it.

The wrong-by-10x row got fixed, Mindee got added, and the same pass moved the accent color to the mark's orange and made the marketing surface read properly on a phone. Small edits. But the reason they were small edits and not a project is that ParseRail's catalog and the code it describes are two views of one thing, and I only had to change one of them.

Top comments (0)