Your startup has 37 users.
You don't need Kubernetes.
You need 38.
That sounds like a dunk. It isn't. It is the most expensive confusion I keep watching early teams walk into: treating "build it right" as a reason to build the future before the present exists.
The product has twenty users. The engineer is thinking about two million.
The product has one background job. The engineer is thinking about Kafka.
The product has one database. The engineer is thinking about sharding.
The product does not know whether anyone wants it. The engineer is designing the architecture for a company that does not exist yet.
That is backwards.
Most early-stage startups don't have a scalability problem. They have a validation problem.
I am not arguing for bad engineering. I am arguing against solving problems you don't have yet.
"Not scalable yet" is not the same thing as "bad engineering."
The goal is not sloppy code. The goal is intentional simplicity: the simplest trustworthy architecture that satisfies today's requirements and is cheap to change when tomorrow's requirements become real. Not theoretical maximum throughput.
Your users are not coming
This is the uncomfortable part, so I will say it plainly.
Most early products do not fail because PostgreSQL couldn't handle the traffic.
They usually fail because nobody wants the product badly enough. The workflow is wrong. The target customer is wrong. The founder does not understand the problem. Distribution does not work. Users do not come back.
I have watched teams spend a month looking enterprise-ready and still not know if anyone wants the product. Microservices before revenue. An event bus before a second consumer exists. A custom platform for a load pattern that has never happened. The architecture review photographs well. The week-three demo does not.
When I built DocPilot, the bet was smaller than the documentation company in my head: ingest a source, generate structured docs, open them at a URL. Managed auth, a typed boundary, and enough logging to debug a failed generation went in on week one. Collaboration, version history, and a full admin panel waited.
FinCrew started as a six-agent finance OS. The first build that could be trusted was smaller: ingest transactions, flag risk, compute runway, answer a grounded question. Bank connectivity was built end to end, then paused: the per-account cost did not make sense before there were users to justify it. CSV kept the loop alive. That was not a retreat from seriousness. It was a refusal to pay for infrastructure the product could not carry yet.
You can make Postgres, Kubernetes, and a queue topology very impressive.
None of that tells you whether a finance team will import a CSV twice.
You cannot architecture your way out of product-market fit.
That is not cynicism. It is sequencing. First you need a product people return to. Then you get to have interesting load.
Scale is a problem you earn
There is a massive difference between these two questions:
Can this architecture handle 10 million users?
What does the next 10x actually look like?
If you have fifty users, designing for five hundred is often useful. You will feel the seams: a slow query, a job that overlaps itself, a page that does too much on request. Those are real. They have evidence.
Designing for ten million is usually fiction. You do not know the access pattern. You do not know which feature survived. You do not know whether the bottleneck will be reads, writes, a third-party API, or the fact that nobody signed up.
The useful goal is not predicting the final architecture on day one. It is a codebase that can evolve.
The architecture should make the next architecture possible.
That is a much better standard than "will this still look clever at Series B."
I learned this the expensive way on an outbound recruiting engine. The first ranking implementation was the obvious one: loop through applicants, await each LLM call, pick a winner. It worked in development with eight test resumes. In staging, with a real pool, one company took four minutes. Five companies sequential meant twenty minutes of ranking before discovery, enrichment, or Instantly setup.
That was a scale problem I had actually earned. The pool was real. The cron had a schedule. Sequential await could not finish the work. Fan-out with batchTriggerAndWait was the next architecture, not a Kafka cluster I designed in week one because someday this might be a marketplace.
The production run still targets about five companies, not five hundred. That was a product decision: signal over volume. The engine did not need to "scale outbound." It needed to send a small number of emails a human could stand behind, twice a day, without rediscovering the same founders.
When the next 10x showed up, it showed up as wall-clock time and rate limits. Not as a thought experiment about millions of users.
Simplicity is not laziness
People hear "one Next.js app, Postgres, a cron" and think you are being cheap. Sometimes you are. Often you are protecting the only scarce resource an MVP has: the ability to change your mind next week without a migration ceremony.
These are not universal rules. They are the shape I keep shipping because they match the actual requirements:
| Actual requirement | Simplest architecture that usually satisfies it | Hypothetical-scale substitute |
|---|---|---|
| Durable records, relations, constraints | Postgres | A database cluster |
| A job that must run on a schedule | Trigger.dev cron / a background task | A distributed scheduler |
| A web product with a dashboard and an API | One Next.js application | Microservices |
| Filter and look up your own data | A database query | Elasticsearch |
The stack is a product decision. Boring defaults buy change. They do not prove you are unserious.
DocPilot is a full-stack Next.js app on Neon Postgres. Docs are rows, not a static site farm. GitHub webhooks, OpenAPI upload, generation, and the hosted doc page live in the same deployable unit. I did not need a documentation microservice. I needed a loop I could ship in three weeks.
The outbound engine went into the hiring platform that already existed. No new repo. A type-safe Trigger.dev workflow, a PostgreSQL resume pool, and a dashboard toggle so ops could turn the schedule on without a redeploy. FinCrew keeps financial logic in a domain package and the app thin, with import and fraud scans as background jobs so the request path stays responsive.
Peergrowth Pulse is the same instinct later. One Next.js app. Clerk. Postgres. Trigger.dev for lead sync. Live Meta for ads KPIs, durable Postgres for CRM state, freeze-once snapshots for client reports. The Copilot is eleven read-only tools over the same analytics primitives the dashboard already uses. Not three backends. Not a "real-time insights mesh." One system with two clocks of freshness, because ads and leads are not the same kind of data.
The in-memory rate limit and brief cache in Pulse are fine for current scale. The code already admits they are not the multi-instance story. I did not invent Redis because a blog post said I would need it. I left a seam.
Choose the simplest architecture that satisfies the actual requirements.
Not: always use the simplest technology.
If the product is the infrastructure, skip this advice. If sequential work cannot finish before the next cron, you have a real problem, and you should solve that problem.
Every extra moving part is a thing that can be down, drifted, or misunderstood during a customer call. Early products already have enough existential risk. Do not invent more.
The MVP can be ugly
This is the distinction I wish more engineering advice would make.
An MVP can legitimately have:
- a rough UI
- manual admin workflows
- hardcoded configuration
- one database
- limited integrations
- simple cron jobs
- a small number of supported workflows
- some operational work done by a human
That does not automatically make it badly engineered.
DocPilot shipped without collaboration, version history, or a proper admin. When something needed a fix, I used direct database access. Full regen on every push to main is wasteful on a large repo. I knew that during the build. I left it. Incremental diffing was the next architecture, not the first one.
FinCrew shipped two agents instead of six. No live activity feed. No AR/AP. No payroll. CSV as the default ingest path. "Coming soon" in the information architecture for the adjacent products. Ugly relative to the vision. Honest relative to the bet.
Pulse is Meta-only on purpose. HubSpot-style connectors are deferred, not faked. Clients get a frozen public link instead of an account. Sales-goal ad accounts do not get the hourly Instant Form pull. Those look incomplete if you are shopping for a platform. They look precise if you are replacing a real lead-passing and reporting loop.
Ugly product decisions are how you keep a sprint a sprint.
These are not ugly product decisions:
- broken data integrity
- insecure authentication
- missing authorization
- destructive operations without safeguards
- no recovery path
- jobs that duplicate side effects
- failures you cannot debug
- architecture that makes every change dangerous
Those are not "MVP shortcuts." They are technical debt, or they are negligence.
A rough settings screen is a shortcut. A missing WHERE on tenant data is a breach waiting for a demo.
A hardcoded config value is a shortcut. A job that double-sends because nobody asked what happens on retry is a production incident.
A manual ops step for the first ten customers is a shortcut. A Copilot that can mutate a lead because a sentence sounded like a follow-up is a product you cannot trust.
Pulse's Copilot suggests. It does not write lead state. ORACLE can only reach FinCrew's own services. Numbers it does not track stay unavailable rather than estimated. DocPilot does not ask the model to invent routes when extraction finds no handler.
Those constraints are not scale. They are honesty.
Move quickly on scope. Don't move carelessly on invariants.
If the core loop is flaky, if you cannot tell who is allowed to see a record, or if every deploy feels like a coin flip, you are not moving fast. You are borrowing time from the week after launch, at a terrible interest rate.
I wrote the positive version of this in The Anatomy of a Production-Ready MVP: the smallest system you can trust with real users. This post is the other edge. Trustworthy is required. Hypothetical scale is not.
The things I refuse to cut
Even when moving quickly, some foundations are worth getting right. Not because they look senior. Because they are the difference between a product you can iterate on and a product you are afraid to touch.
Data integrity
Use database constraints where they matter. A unique constraint is still a unique constraint. If two rows must not exist, the database should refuse the second write. Application checks are how you get a race, a retry, and a duplicate payment alert that is itself a duplicate.
Pulse puts organizations, memberships, and per-ad-account ACL in Postgres, then adds row-level security as defense in depth. Request-path access goes through withUserDb. Application asserts remain necessary. RLS is a second lock, not a substitute for product logic.
FinCrew's fraud rules live in code as predicates for the cases that are already knowable: duplicate payments, weekend spend, new vendors. The model only sees the ambiguous remainder. The invariant is not "the LLM will probably notice." The invariant is "this row cannot silently become two truths."
You do not need a perfect schema on launch. You do need a source of truth for each entity, a story for a write that fails halfway, and a list of fields the core loop cannot live without.
Auth and permissions
An MVP can have three screens. It still needs to know who is allowed to see those screens.
I do not roll custom session handling on a 2–4 week sprint. Managed auth is the default because authentication is a liability, not a differentiator.
Authorization is different. The product has to own it.
Pulse is invite-gated. Clerk session, then org and ad-account ACL, then the workspace owner's Facebook token for Meta. Public report links are unguessable tokens over frozen snapshots, not a logged-in dashboard with the filters stripped. Copilot tools compact PII before the model ever sees a lead.
FinCrew has multi-tenant organizations, invites, and Stripe entitlements. ORACLE's tool surface is narrow on purpose. A finance assistant that can see another tenant's runway is not an MVP. It is a lawsuit with a chat UI.
Skip five roles. Do not skip "this user can see this row."
Idempotency
If a job can run twice, understand what happens the second time.
A twelve-hour outbound cron will eventually rediscover the same founders. After each run, contacted domains go on a Fiber exclusion list, even when outreach was skipped. Discovery already spent budget. The domain should not show up tomorrow.
Pulse's hourly Meta lead sync cannot be "just run again." Overlapping syncs fighting over the same leads is how you wipe CRM notes. Trigger.dev tasks use idempotency keys per account-hour and skip-if-in-flight tags. Upserts omit workflow fields, notes, and custom attributes from conflict updates so operator work survives the next Meta fetch.
FinCrew's ingest and fraud scans retry in the background. A failed scan should recover without losing the import. Retries belong on the unit of work that is safe to repeat. Side effects stay after the gates.
Idempotency is unglamorous. It is also the difference between a pipeline and a spam cannon.
Observability you can actually use
You do not need a full APM suite on day one. You need to answer "what happened?" without guessing.
The outbound parent used to log leadsQueued: 3 and go green. Then ops asked why we emailed a founder about a mobile engineer when the company was hiring backend. Every child had returned valid JSON. The run was not red. The product was wrong. Skip reasons, batch health, and a breakdown of why a run queued zero leads are what made the cron operable.
Pulse surfaces sync health in the UI. Stale or failed lead state is visible instead of silent. Sentry sits on the critical paths.
If a job can skip, you should be able to see why. If a user can get stuck, you should hear about it before they email you.
A path to change
This is the foundation that looks like architecture and is actually mercy for future you.
Clear boundaries. Versioned migrations. A schema you can evolve. One place where the domain lives. Previews and a rollback that does not require folklore.
You will be wrong about the product. That is the job. The codebase has to survive being wrong.
FinCrew's domain package exists so fraud, forecasting, KPIs, and ORACLE tools share one model. Pulse's analytics primitives exist so the dashboard, the Copilot, and freeze-once reports cannot drift into three different truths. DocPilot stores docs as structured rows so regen is an update, not a new static site pipeline.
Those are change investments. They make the next architecture possible without requiring you to guess what it is.
When the next 10x actually shows up
You will know.
It will not arrive as a whiteboard question. It will arrive as a symptom: ranking that cannot finish before the next cron, a request-path sync that hangs the leads page, a vendor bill that does not make sense before launch, a cache that is fine on one instance and will lie on two.
Respond to the symptom you have.
| What I actually felt | What I shipped next | What I did not ship |
|---|---|---|
| Ranking could not finish in staging | Child tasks and batchTriggerAndWait | A queue platform for its own sake |
| Lead sync did not fit a page load | Trigger.dev, hourly, idempotent per account | Syncing everything live, or nothing |
| Bank APIs cost real money before users existed | CSV-first; rails built, then paused | Making bank sync the onboarding path |
| Full doc regen is wasteful on large repos | Left it, with a known follow-up | A distributed codegen farm |
| In-memory rate limits are not multi-instance safe | Noted in the code, not rebuilt in advance | Redis "just in case" |
Notice the pattern. The next architecture is small, local, and justified by a failure I could point to.
Scope you kill on purpose is the product version of this. Hypothetical scale is the infrastructure version.
When you do grow, grow the part that is actually tight. That is not an excuse to design the final form of the system before a stranger has completed the core loop twice.
Build for the users you have
I still want the codebase to look like an adult wrote it.
Auth that holds. Constraints that hold. Jobs that can run twice without corrupting state. Logs that explain a quiet failure. Boundaries that let you extract a service later if you ever earn one.
That is production-ready. It is also enough.
What I do not want is a system optimized for a traffic graph that is still imaginary. Kubernetes for 37 users. Kafka for one job. Sharding for one database. Microservices for one Next.js app. Elasticsearch for a query Postgres would have been happy to run.
The startup does not have a scalability problem.
It has a validation problem.
Ship the smallest architecture that can tell you the truth, survive being used, and still be easy to change when the truth arrives.
Related: The Anatomy of a Production-Ready MVP · The Stack Is a Product Decision · Scope You Kill on Purpose · How to Brief an Engineer for a 2–4 Week MVP · The Hard Part of AI Products Isn't the AI · DocPilot · FinCrew · Peergrowth Pulse · Automated Recruiting Outreach