The MVP trap
You shipped an MVP. Users love it. Now investors want you to scale, onboard enterprise customers, and add features faster. But every change feels harder than the last. The codebase that got you here is now holding you back.
This is the MVP trap: patterns that work for 10 users become liabilities at 1,000. The question isn't whether to refactor—it's what to get right from the beginning so you don't have to.
Multi-tenancy: Get it right first
Multi-tenancy is how you separate one customer's data from another's. Get this wrong and you'll face:
- Data leaks between customers (career-ending bugs)
- Noisy neighbor problems where one customer slows down everyone
- Painful migrations when enterprise customers demand isolation
Multi-tenancy Strategies
- Shared database, shared schema: Simplest. Add tenant_id to every table. Works until you hit scale or compliance requirements.
- Shared database, separate schemas: Better isolation. More complex migrations. Good middle ground.
- Separate databases: Maximum isolation. Required for some enterprise deals. Higher operational overhead.
Most products should start with tenant_id everywhere, but design so you can migrate to stronger isolation later. This means abstracting your data access layer properly.
Authentication: Don't roll your own
Authentication looks simple until you need:
- Password reset flows that don't expose user existence
- Rate limiting that stops credential stuffing
- MFA that doesn't break your mobile app
- SSO for enterprise customers (SAML, OIDC)
- Session management across multiple devices
Use an auth provider (Auth0, Clerk, Supabase Auth, Firebase Auth) from day one. The $100/month you spend now saves months of security engineering later—and reduces the surface area for credential breaches.
Authorization: RBAC from the start
Most MVPs start with admin/user roles and hardcode permissions in the application. This falls apart when you need:
- Enterprise customers with custom role configurations
- Team hierarchies (admins can manage their team, not others)
- Resource-level permissions (user can edit Document A but not Document B)
Design a proper role-based access control (RBAC) system early. Define permissions as actions on resources. Let roles be collections of permissions. Store assignments at the user and team level.
RBAC Design Pattern
Permission: "documents:edit"
Role: "editor" = ["documents:read", "documents:edit"]
Assignment: User X has role "editor" on Team Y
Check: canUserDo("documents:edit", docId)
→ resolve team → check role → check permissionDatabase design: Think about queries, not just storage
Normalized schemas are great for consistency. But production systems are read-heavy, and your most common queries will dictate performance.
- Index your actual queries: Look at your slow query logs. Add indexes for real access patterns, not theoretical ones.
- Denormalize where it matters: Store computed values when the computation is expensive and data changes rarely.
- Choose the right database: PostgreSQL handles most workloads. Don't add MongoDB just because you have some JSON—Postgres has jsonb.
API design: URLs aren't forever (but they should be)
Once external developers build on your API, breaking changes become very expensive. Think ahead:
- Version from day one: /api/v1/resources. You'll need v2 eventually.
- Use proper HTTP status codes: 400 for client errors, 500 for server errors, 401 for auth, 403 for authz.
- Return consistent error formats: Always include an error code and message. Avoid leaking stack traces.
- Document as you build: OpenAPI specs make integrations easier for everyone, including your frontend team.
Billing and subscriptions
Billing code is deceptively complex. Proration, upgrades, downgrades, trials, refunds, invoices, taxation—each has edge cases that break naive implementations.
Use Stripe. Let them handle the complexity. Your job is to:
- Sync subscription state to your database (via webhooks)
- Enforce feature access based on subscription tier
- Handle dunning (failed payments) gracefully
- Provide clear billing history to customers
Observability: If you can't see it, you can't fix it
Production issues at 2 AM are inevitable. What matters is how quickly you can diagnose them.
- Structured logging: JSON logs with consistent fields (timestamp, request_id, user_id, action).
- Request tracing: Trace IDs that follow requests across services.
- Metrics and dashboards: Error rates, latency percentiles, business metrics.
- Alerting: PagerDuty or Opsgenie. Alerts that actually require action.
Set this up before you need it. Debugging production issues without observability is archaeology.
Trade-offs to acknowledge
Not everything needs enterprise engineering on day one. Be intentional about where you cut corners:
- Skip now, document later: If you knowingly take a shortcut, write it down. Future you will thank present you.
- Monolith is fine: Don't start with microservices. A well-structured monolith scales further than you think.
- Perfect is the enemy of shipped: Some technical debt is acceptable. The goal is to know what debt you're taking on.
Conclusion
The decisions that matter most are the ones hardest to change later: data architecture, authentication patterns, multi-tenancy strategy. Get these right early, be intentional about trade-offs, and you'll build on a foundation that supports growth rather than fighting it.
Speed matters for MVPs, but so does knowing which corners not to cut. The best engineering teams ship fast and think ahead—not one or the other.