Skip to content
Pedro Santos
Go back

Predileto '#15 - Chapter I - System Design - Requirements Engineering

Edit page

In an early-stage startup or an unproven product it’s usually more important to be able to iterate quickly on product features than it is to scale some hypothetical future load.

This is Chapter I of the “How I built Predileto” series. Before writing a single line of code, the most leverage you’ll ever get is in requirements engineering — deciding what the system must do, and (just as important) what it must not do yet.

Table of contents

Open Table of contents

Why bother with requirements for a solo build?

It’s tempting to skip this when you’re moving fast. But requirements aren’t bureaucracy — they’re the contract you write with your future self. Every “should the reel be free?” or “what happens when a video fails halfway?” you answer here is a question you don’t have to re-litigate while you’re elbow-deep in the worker code.

The trick for an unproven product is scoping ruthlessly. Predileto can technically run a handful of generation pipelines today (room staging, enhancement, single-call prompts), but for the requirements that follow I’m deliberately pinning the product to two flagship flows. Everything else is noise until these two earn their keep.

I split requirements into two buckets, which is the standard system-design framing:

Functional Requirements

FR#01 — Authentication & Authorization

Users sign in and sign up with Google OAuth, brokered by Supabase. The backend never sees a password — it validates the Supabase-issued JWT (ES256, verified against the project’s published JWKS) on every request.

The product is multi-tenant: a user can only ever see and act on their own generations, credits, and billing. There is no “team” or “agency” sharing model yet — one human, one tenant. Every data access is scoped by the authenticated user’s id.

Users can also permanently delete their account and all associated data (generations, credit ledger, billing) — a hard requirement for operating under GDPR.

FR#02 — Timelapse Building Video

The hero feature. A user uploads a single photo of a property — an empty plot of land, an existing house, or a derelict ruin — and the system generates a cinematic construction timelapse: the camera holds steady while the structure rises (or is renovated/rebuilt) from the input frame to a finished, magazine-cover building.

FR#03 — Reels from Pictures

The second flow. A user uploads multiple photos of a property (up to 15), arranges them in the order they want, and the system turns each photo into a short cinematic clip and concatenates them into a single vertical reel.

Note the asymmetry: FR#02 charges, FR#03 doesn’t. That’s a deliberate product decision encoded as data, not a code branch — both flows resolve their cost from the same pricing table.

FR#04 — Users Subscribe to Generate

Generation is funded entirely through a monthly subscription — there’s no à la carte credit purchase. A subscription is the only paid path into the product, billed through Stripe, with two tiers:

TierPriceCredits
Pro€24 / month100 credits refreshed each period
Unlimited€99 / monthNo metering — generate without spending credits

So credits still exist as the internal metering unit for Pro users (100 credits ≈ 20 timelapse videos a month), while Unlimited users bypass metering entirely. Subscription state is driven entirely by Stripe webhooks (checkout completed, invoice paid, subscription updated/deleted), and users manage, upgrade, or cancel through the Stripe Customer Portal.

An earlier design let users buy one-off credit packs (pay-as-you-go). I dropped it: a single subscription model is simpler to reason about, gives predictable monthly revenue, and removes a whole class of “I bought credits but…” edge cases. Fewer ways to pay is a feature when you’re one person.

FR#05 — Users Can See Their Balance & Plan

At any time a user can see how many credits they have left this period, whether they’re on an unlimited plan, and when their current billing period ends. This is what the UI reads to decide whether to let them hit “Generate” or nudge them to upgrade.

FR#06 — Users Can See Their Generation History

Every generation — draft, in-progress, succeeded, or failed — is listed for the user, newest first, with its status, thumbnail, and (when ready) a link to the output video. A user can open any past generation to download it again or delete it.

Non-Functional Requirements

NFR#01 — Generation is asynchronous

Rendering a timelapse or a reel takes minutes, not milliseconds — far longer than any sane HTTP request. So generation is fire-and-poll: the API accepts the request (responding 202 Accepted), enqueues the work to a background worker, and the client polls the generation’s status until it succeeds or fails. The request thread is never blocked on a model call.

NFR#02 — Tenant isolation

A user must never be able to read or mutate another user’s generations, images, credits, or billing — not via a guessed id, not via a stale token. Authorization is enforced on every query, not just at the UI layer.

NFR#03 — Never charge for work we didn’t deliver

Credits are spent up-front when a generation is accepted, but if the job fails to dispatch or fails to render, the credits are automatically refunded. A user paying 5 credits for a video that never arrives is a bug, not an edge case — the happy path and the refund path are designed together.

NFR#04 — Billing must be idempotent and exact

Stripe delivers webhooks at least once, sometimes out of order, sometimes replayed. Credit grants and subscription changes must therefore be idempotent: processing the same event twice grants credits once. Every webhook is recorded and de-duplicated before it can touch a balance, and a failed handler rolls back cleanly so Stripe’s retry can re-apply it.

NFR#05 — Adapt to the upstream model’s rate limits

The actual video generation runs on a third-party model API (Runway) with its own concurrency and rate limits. The system must stay within those limits — e.g. capping how many clips of a single reel render in parallel — rather than firing everything at once and getting throttled or banned.

NFR#06 — Right-sized capacity

This is an unproven product, so the target is deliberately modest: comfortably handle ~100 daily active users averaging ~5 generations/day. Designing for that — instead of an imaginary million-user load — keeps the infrastructure cheap and the iteration loop fast. Scaling is a problem I’d be lucky to have.

What’s next

With the requirements pinned down, the rest of Chapter I builds on them: capacity estimation (turning “100 DAU × 5 videos” into storage and throughput numbers), the data model, the API design, and the high-level system design — followed by the trade-offs I made along the way.

See you in the next one.


Edit page
Share this post on:

Next Post
Davin '#0 - Changing Digital Marketing for Real Estate Agencies