We build every SaaS MVP on the same core stack: Next.js with TypeScript on the frontend, Tailwind for styling, Python or Node.js on the backend, Vercel for deployment, and Mastra as our agent framework for AI features. This post walks through every choice, what we tried before, what we rejected, and why we stopped debating stack decisions and started shipping.
Why we standardized on one stack
Early on, we let each project dictate its own stack. A client wanted Vue, so we built in Vue. Another wanted Django templates, so we did that. A third wanted a separate React SPA with a Go API.
Every one of those projects took longer than it needed to.
The problem was not the technologies. They are all fine. The problem was context switching. Our build loop runs on a tight cycle of drafts, reviews, and corrections, and that cycle gets faster the more consistent the patterns are. Consistent patterns mean cleaner first drafts, fewer regenerations, less rework. Faster delivery is the entire point.
So we standardized. One stack, deeply understood, with every pattern dialed in. The result: SaaS MVPs in roughly 5 days, and the code quality is higher than when we were “flexible.”
Here is every layer of that stack and why it is there.
Frontend: Next.js + TypeScript
Next.js is our default for every SaaS product. Not because it is popular: because it solves the exact problems SaaS products have.
Server-side rendering and static generation. Marketing pages and landing screens render at build time. Dashboard and authenticated routes render on the server. This means fast initial loads, good SEO for public pages, and no flash of loading states when a user logs in.
API routes built in. For an MVP, we do not need a separate backend service for simple CRUD operations. Next.js API routes handle authentication callbacks, webhook endpoints, and data mutations without deploying a second server. When the product outgrows API routes, we split into a dedicated backend, but most MVPs never need to.
File-based routing. Route structures generate cleanly when the pattern is “create a file, get a route.” No router configuration, no route registration, no mapping files. This is a small thing that compounds across an entire MVP build.
TypeScript everywhere. Every file is TypeScript. No exceptions. Type safety catches entire categories of bugs before they reach production. It also tightens the loop: drafts come back cleaner when our toolchain has type context to work against. The overhead of TypeScript is maybe 10% more keystrokes. The payoff is fewer runtime errors, better autocomplete, and code that new engineers can read and modify without guessing what a function returns.
What we considered instead: Remix (good, but the ecosystem is thinner), Astro (we use it for static sites like this blog, but it is not ideal for app-heavy SaaS dashboards), and plain React SPAs (no SSR, worse SEO, more infrastructure to manage).
Styling: Tailwind CSS
We have tried component libraries, CSS modules, styled-components, and vanilla CSS. We standardized on Tailwind because it produces the most consistent results with AI generation and the fastest iteration speed for human engineers.
Why Tailwind works for MVPs:
- No naming decisions. Utility classes apply directly: no invented class names to chase down later, fewer hallucinated CSS selectors, less cleanup.
- Responsive by default. Every utility has responsive variants. A mobile-first layout is three extra characters, not a separate media query block.
- Design constraints built in. Tailwind’s spacing scale, color system, and typography presets prevent the “every component looks slightly different” problem that plagues MVPs built without a design system.
- No unused CSS in production. Tailwind’s purge step strips everything not referenced in the codebase. The CSS bundle for a typical MVP is under 15KB.
What we ruled out: shadcn/ui is excellent and we use its component patterns frequently, but Tailwind underneath is what makes it work. Material UI adds too much visual weight and bundle size for MVPs. Styled-components create runtime overhead and are harder to generate consistently across a project.
Backend: Node.js + Python for ML
We use both, depending on the project. The split is straightforward.
Node.js for the SaaS layer. Authentication, billing, CRUD APIs, real-time features with WebSockets: Node.js handles all of this with a smaller operational footprint than Python for web workloads. And because the frontend is already TypeScript, sharing types between frontend and backend eliminates an entire category of integration bugs. Most of the time, Next.js API routes are enough for the MVP: no separate backend service needed.
Python for ML workloads. When a product involves machine learning, data processing, or model inference, Python handles that layer. The ML ecosystem in Python is years ahead of everything else: model libraries, data processing pipelines, and inference servers all assume Python. We use FastAPI to expose ML endpoints as APIs, and deploy them on Modal for serverless GPU workloads or as containerized services for persistent inference.
FastAPI + Modal is our go-to for ML services. FastAPI gives us typed, documented API endpoints with minimal boilerplate. Modal gives us serverless GPU compute that scales to zero when idle: we are not paying for a GPU when nobody is running inference. For model training, batch processing, and any workload that needs a GPU for minutes or hours but not 24/7, Modal is dramatically cheaper than provisioning dedicated instances.
The split in practice: The SaaS layer runs on Node.js or Next.js API routes. The ML layer runs on Python via FastAPI, deployed on Modal. They communicate over HTTP. This keeps each service focused and independently scalable: the SaaS layer handles thousands of concurrent users while the ML layer handles compute-intensive tasks asynchronously.
What we do not use: Go (fast, but the ecosystem for AI and web SaaS is thin), Ruby on Rails (productive, but our toolchain is sharper in TypeScript/Python), and Elixir (excellent for real-time, but too niche for our pattern library to cover well).
Authentication: Auth.js or Clerk
Authentication is the single most underestimated piece of any SaaS MVP. We have seen founders lose weeks trying to hand-roll auth, and we have seen AI-generated auth code that ships with critical security gaps.
Our default is Auth.js (formerly NextAuth) for projects where we want full control over the auth flow and user data stays in our database. For projects that need to launch faster or require enterprise features like SSO and RBAC out of the box, we use Clerk.
Auth.js gives us email/password, OAuth providers (Google, GitHub, etc.), magic links, and session management with zero vendor lock-in. The user data stays in the project’s database. We have built the patterns enough times that the first-pass scaffold is production-quality, which is rare for auth code.
Clerk costs more per user but eliminates the need to build user management UI, email verification flows, organization management, and SSO configuration. For B2B SaaS products that will eventually need “invite your team” and role-based access, Clerk saves weeks of development.
What we avoid: Building auth from scratch. Every time. Even for “simple” projects. The gap between “login works” and “login is secure” is where security vulnerabilities live, and no MVP timeline has room for a custom auth implementation.
Payments: Stripe
There is no alternative worth considering for SaaS billing. Stripe handles subscription management, usage-based billing, invoicing, tax calculation, and payment method management across 135+ currencies.
We use Stripe Checkout for the initial payment flow, Stripe Billing for subscription management, and Stripe webhooks for keeping the application state in sync with payment state.
The integration patterns are well-defined enough that we ship reliable Stripe code on the first pass, which is rare for payment integrations. We have a set of tested webhook handlers that cover the common lifecycle events: subscription created, payment succeeded, payment failed, subscription canceled, invoice finalized. These get dropped into every project as a starting point.
What about Lemon Squeezy or Paddle? They handle tax compliance better out of the box (Stripe requires Stripe Tax as an add-on), and they act as merchant of record, which simplifies legal obligations. We use them when a client specifically requests it. But Stripe’s flexibility, documentation, and ecosystem make it our default.
Database: PostgreSQL (via Supabase or Neon)
PostgreSQL is the database. The only question is how we host it.
Supabase when the project benefits from its built-in features: real-time subscriptions, row-level security, edge functions, and a generous free tier. Supabase is essentially a Firebase alternative built on Postgres, and for MVPs that need real-time data (collaborative features, live dashboards, chat), it reduces the amount of custom infrastructure we need to build.
Neon when we want a serverless Postgres that scales to zero and branches like git. Neon’s branching feature is particularly useful during development: we can create a database branch for each feature, test against production-shaped data, and merge or discard. For projects deployed on Vercel, the integration is seamless.
What about MongoDB? We used it for years. The flexibility of schemaless documents is appealing early on, but it creates problems as the product grows. Missing fields, inconsistent data shapes, and the lack of relational joins lead to application-level workarounds that add complexity. Postgres with JSONB columns gives us the same document flexibility when we need it, plus real relations, transactions, and constraints when we need those.
Deployment: Vercel + Cloudflare + Railway/Fly.io
Vercel is the default deployment target for every Next.js project. Zero-config deploys from git, preview deployments for every pull request, edge functions for low-latency API routes, and automatic CDN distribution. A typical MVP deploys in under 60 seconds from git push.
Cloudflare handles DNS, CDN caching, DDoS protection, and Workers for edge compute when we need it. Every project runs behind Cloudflare regardless of where the application is hosted.
Railway and Fly.io for long-running backend services. Background job processors, WebSocket servers, persistent API services, and any workload that needs to stay alive between requests: these go on Railway or Fly.io. Railway is simpler to set up and works well for Python FastAPI services. Fly.io gives us more control over geographic distribution when latency matters.
Modal for ML and GPU workloads. Serverless GPU compute that scales to zero. Model inference, batch processing, and any workload that needs a GPU but not 24/7.
The pattern is always the same: Vercel for the Next.js frontend, Cloudflare for DNS and edge, Railway or Fly.io for long-running backend services, and Modal for ML compute. Each layer handles what it is best at.
Agent framework: Mastra + LiveKit
For products that include AI features (which is an increasing majority of what we build), Mastra is our agent framework and LiveKit handles real-time voice and video AI.
Mastra is the core of our AI layer. It is an open-source TypeScript agent framework that handles agent workflows, tool calling, memory management, RAG pipelines, and multi-step reasoning. It integrates with any LLM provider (OpenAI, Anthropic, Google, open-source models) and any vector store. We chose Mastra over LangChain because it is TypeScript-native: it matches our frontend stack, the agent abstractions are cleaner for production use, and our build loops are tighter when the agent code lives in the same type system as everything else.
Every AI-powered SaaS product we build runs on Mastra. Chatbots, document processing agents, customer support automation, internal knowledge bases, workflow agents that chain multiple tools: all of it goes through Mastra’s agent runtime. The framework handles the orchestration complexity so our engineers focus on the business logic and tool definitions.
LiveKit handles real-time communication when the AI agent needs to speak, listen, or process video. Voice AI agents, real-time transcription, and video analysis all run through LiveKit’s infrastructure. We deploy LiveKit agents as Python services that connect to the LiveKit server, process media streams, and return results in real-time.
The pattern: User interacts with the Next.js frontend. The frontend calls a Mastra agent (via API route or direct connection). The Mastra agent calls LLM providers, executes tools, manages conversation memory, and returns results. If the interaction involves voice or video, LiveKit handles the media layer.
What the full stack looks like
| Layer | Technology | Why |
|---|---|---|
| Frontend framework | Next.js + TypeScript | SSR, API routes, file routing, type safety |
| Styling | Tailwind CSS | Consistent AI output, fast iteration, small bundle |
| Authentication | Auth.js or Clerk | Secure defaults, no custom auth code |
| Payments | Stripe | Subscription billing, global payments, reliable webhooks |
| Database | PostgreSQL (Supabase or Neon) | Relational + JSONB flexibility, real-time, serverless |
| Frontend deployment | Vercel | Zero-config deploys, preview URLs, edge functions |
| DNS + edge | Cloudflare | CDN, DDoS protection, Workers |
| Long-running services | Railway / Fly.io | WebSockets, persistent APIs, always-on services |
| Background jobs | Inngest | Durable workflows, scheduled tasks, event-driven jobs |
| ML compute | Modal | Serverless GPU, scales to zero, FastAPI endpoints |
| Agent framework | Mastra | Agent workflows, tool calling, RAG, LLM-agnostic |
| Real-time AI | LiveKit | Voice agents, transcription, video processing |
| Backend (SaaS) | Node.js / Next.js API routes | Shared types with frontend, small footprint |
| Backend (ML) | Python + FastAPI | ML ecosystem, typed API endpoints, Modal deploy |
Every piece earns its place by making the next project faster. When a tool slows us down or introduces inconsistency, we replace it. This stack has been stable for over a year because nothing we have tried beats it for the specific constraint we optimize for: production-grade SaaS MVPs, delivered in days. (See the full stack reference for the complete list and the version we run today.)
Why this stack is built to move fast
Every piece of this stack exists because it makes the next project faster, cheaper, or more reliable. Here is why the choices compound.
Best platform for every workload. We run Vercel for Next.js, Railway and Fly.io for long-running services, Modal for GPU compute, and Inngest for background jobs. Four platforms sounds like complexity. In practice, it means each service runs on the infrastructure purpose-built for it: zero compromises on performance, zero wasted spend on idle compute. Inngest handles durable workflows, scheduled tasks, and event-driven jobs without us managing queue infrastructure.
Two languages, zero gaps. TypeScript covers the entire web layer: frontend, API routes, Mastra agents. Python covers the entire ML layer: model inference, data processing, FastAPI endpoints. Every SaaS + AI product needs both capabilities. Running them in clearly separated services means each codebase stays focused, and our drafts come back cleaner in both.
Supabase Postgres covers 95% of data needs. Relational queries, JSONB documents, real-time subscriptions, row-level security, full-text search: Supabase handles all of it without adding Redis, Elasticsearch, or a separate real-time service. For smaller projects, we have been exploring Convex, where its real-time sync and serverless functions collapse the entire backend into one layer. Most SaaS MVPs never outgrow Supabase. The ones that do tell us exactly where the bottleneck is, and we add a specialized tool for that specific need.
React Native for mobile, shared with the web. When a client needs mobile, React Native shares code with the Next.js frontend and produces apps that cover 90% of use cases. One team, one language, two platforms. Native Swift or Kotlin only when the product has a performance requirement that React Native cannot meet, which, for SaaS products, is rare.
What we are exploring next
We are not precious about this stack. The shiny object syndrome gets to us, and honestly, that is a feature. Staying on the bleeding edge is how we found Mastra before it was mainstream, how we adopted Modal before most teams knew it existed, and how we started using Inngest for background jobs instead of managing our own queue infrastructure.
Right now, we are building full-stack Cloudflare applications, running the entire stack on Cloudflare Workers, D1, R2, and Queues. The promise is a globally distributed application with sub-50ms latency everywhere, no cold starts, and a single vendor for compute, storage, and networking. We will keep everyone posted on how that plays out in production.
The stack evolves. The principle does not: pick the best tool for each job, standardize the patterns, and let the toolchain compress the predictable parts so the engineers who maintain this stack focus on the decisions that matter. (Our playbook is how those decisions ship.)