← Back to Blog

The Stack We Use for Every SaaS MVP (and Why)

Our opinionated SaaS MVP stack: Next.js, Convex, TypeScript, Tailwind, Mastra, Python, Vercel. Why we chose each piece, what we ruled out, what we'd revisit.

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:

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

LayerTechnologyWhy
Frontend frameworkNext.js + TypeScriptSSR, API routes, file routing, type safety
StylingTailwind CSSConsistent AI output, fast iteration, small bundle
AuthenticationAuth.js or ClerkSecure defaults, no custom auth code
PaymentsStripeSubscription billing, global payments, reliable webhooks
DatabasePostgreSQL (Supabase or Neon)Relational + JSONB flexibility, real-time, serverless
Frontend deploymentVercelZero-config deploys, preview URLs, edge functions
DNS + edgeCloudflareCDN, DDoS protection, Workers
Long-running servicesRailway / Fly.ioWebSockets, persistent APIs, always-on services
Background jobsInngestDurable workflows, scheduled tasks, event-driven jobs
ML computeModalServerless GPU, scales to zero, FastAPI endpoints
Agent frameworkMastraAgent workflows, tool calling, RAG, LLM-agnostic
Real-time AILiveKitVoice agents, transcription, video processing
Backend (SaaS)Node.js / Next.js API routesShared types with frontend, small footprint
Backend (ML)Python + FastAPIML 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.)

// frequently asked

Common questions

What tech stack does Asyncdot use for SaaS MVPs?
Asyncdot builds SaaS MVPs on Next.js with TypeScript, Tailwind CSS for styling, PostgreSQL (via Supabase or Neon) for the database, Stripe for payments, Auth.js or Clerk for authentication, Vercel for frontend deployment, Railway or Fly.io for long-running services, and Modal for ML compute. AI features use Mastra as the agent framework and LiveKit for real-time voice and video.
Why use Next.js instead of React with a separate backend?
Next.js combines server-side rendering, API routes, and file-based routing in one framework. For SaaS MVPs, this eliminates the need to deploy and maintain a separate backend service for basic CRUD operations. The API routes handle authentication callbacks, webhooks, and data mutations. When a product outgrows API routes, splitting into a dedicated backend is straightforward, but most MVPs never need to.
Is TypeScript necessary for an MVP?
Yes. TypeScript adds roughly 10% more keystrokes but prevents entire categories of runtime bugs. More importantly for AI-native development, drafts come back cleaner when the toolchain has type context to work against. The types serve as documentation, catch integration errors at build time, and make the codebase safer to modify as the product evolves.
Why PostgreSQL instead of MongoDB for SaaS products?
PostgreSQL provides relational data modeling, transactions, constraints, and JSONB columns for flexible document storage when needed. MongoDB's schemaless approach is appealing early on but creates problems as the product grows: missing fields, inconsistent data shapes, and the lack of relational joins lead to application-level workarounds. Postgres with JSONB gives you document flexibility plus relational integrity.
What AI frameworks does Asyncdot use?
Asyncdot uses Mastra as its core agent framework, handling agent workflows, tool calling, RAG pipelines, memory management, and multi-step reasoning. For real-time voice and video AI, we use LiveKit. ML workloads run on Python with FastAPI, deployed on Modal for serverless GPU compute. The AI layer runs as separate services from the main Next.js application, communicating over HTTP. This separation keeps each service focused and independently scalable.