Engineering

The Stack We Recommend to Most Clients and Why

We've shipped enough products to have strong opinions about technology choices. Here's the stack we reach for on most projects, what we considered and rejected, and the reasoning behind every decision.

Lanos Technologies10 min read

When people ask us what stack we use, they usually want a list. Next.js, Postgres, Vercel. Done.

But a list without reasoning is useless. Anybody can list technologies. The value is in understanding why you'd pick one thing over another, and more importantly, when you'd pick something different. So here's not just what we use, but the thinking behind each choice. Including the things we've tried and stopped using.

TypeScript everywhere

We write TypeScript on the frontend and the backend. Full stop. This is the single decision that has the biggest impact on long-term code quality and team velocity across the projects we build.

The argument for TypeScript over plain JavaScript is straightforward. When you're building a product for someone else, the codebase needs to be understandable by engineers who weren't involved in the original build. Type annotations serve as documentation that stays in sync with the code. A function signature that says createUser(email: string, role: "admin" | "member"): Promise<User> tells the next engineer exactly what this function expects and what it returns. No guessing. No reading through the implementation to figure out the shape of the data.

We've inherited JavaScript codebases where understanding a single API endpoint required reading four files because there was no way to know what shape the data took at each step. TypeScript eliminates that entire class of confusion.

Is TypeScript perfect? No. The compiler can be slow on large projects. Some library type definitions are incomplete. And there's a learning curve for engineers coming from vanilla JavaScript. But the tradeoff is overwhelmingly worth it for any project that will be maintained for more than six months.

Frontend: Next.js with the App Router

We used to build with plain React and a separate backend. We stopped doing that about two years ago.

The problem with a standalone React SPA is that you end up rebuilding things that Next.js gives you for free. Routing. Server-side rendering. API routes. Image optimization. Metadata management. Code splitting. Each of these is a solved problem in Next.js that would cost you days or weeks to implement well in a vanilla React setup.

The App Router (introduced in Next.js 13 and stabilized through 14 and 15) is what made us go all-in. Server Components let us fetch data on the server and send rendered HTML to the client, which means faster initial page loads and less client-side JavaScript. For products that care about SEO or have content-heavy pages, this is a massive win.

We use React Server Components as the default. Client Components only when we need interactivity: forms, modals, dropdowns, anything that responds to user input. This keeps the client-side bundle small and the application fast.

For styling, we use CSS Modules or Vanilla CSS depending on the project's complexity. We've used Tailwind on a few projects and it works well for prototyping, but we've found that on longer engagements the utility-first approach can make the markup harder to read and maintain, especially when a new engineer joins the project mid-stream.

Backend: same Next.js, or separate Node services

For most products, Next.js API routes handle the backend. This keeps the project in a single codebase, a single deployment, and a single mental model. For an early-stage SaaS with 5 to 15 API endpoints, there's no good reason to maintain a separate backend service. This is the same philosophy behind our recommendation to start with a monolith instead of microservices.

When does a separate backend make sense? When the product has background processing needs that don't fit the serverless model. Long-running jobs, WebSocket connections, scheduled tasks, or processing that takes more than 30 seconds. Serverless functions have timeout limits and they're not designed for persistent connections.

In those cases, we'll add a Node.js service running on Railway or a small VPS. Still TypeScript. Same data models. But running as a long-lived process instead of a request-response function.

We've considered Go for performance-critical services and we like the language. But for the products we typically build (B2B SaaS, marketplaces, internal tools), the performance difference between Node.js and Go doesn't justify the added complexity of maintaining two languages in the same project. Node.js is fast enough. The bottleneck is almost always the database, not the application server.

Database: PostgreSQL, always

This is our most opinionated choice and the one we're most confident about.

We use PostgreSQL for everything. Relational data, JSON documents, full-text search, geospatial queries, time-series data. Postgres handles all of these through built-in features and extensions. The ecosystem around PostgreSQL is so capable that for most products, you don't need a second database.

We've used MongoDB on projects and we've inherited MongoDB-based applications. In every case, the data was relational. Users have teams. Teams have projects. Projects have tasks. This is relational data, and modeling it in a document database means either duplicating data across documents (and dealing with consistency problems) or constantly doing application-level joins (and losing the performance benefits of a relational database).

MongoDB is a good database. It's excellent for truly document-oriented data: logs, event streams, content management where the schema varies per document. But most SaaS products are not document-oriented. They're relational. And PostgreSQL is the best relational database available, with the added benefit of handling the document-oriented parts through its native JSONB support.

For SaaS products that serve multiple customers from one platform, PostgreSQL also has excellent tools for multi-tenant data isolation, including row-level security policies that enforce tenant boundaries at the database level.

For the ORM layer, we use Prisma on most projects. The schema-as-code approach works well for teams, the migration system is reliable, and the generated client provides excellent TypeScript types. We've been watching Drizzle closely and have used it on a couple of projects where the Prisma client was too heavy. Drizzle is lighter, faster, and closer to raw SQL, which some engineers prefer.

We run PostgreSQL on managed services. Supabase if the project benefits from their real-time features and auth. Neon if we want serverless Postgres that scales to zero during development. AWS RDS if the client has existing AWS infrastructure. We almost never manage database servers ourselves. Managed Postgres has gotten good enough that there's no reason to.

Infrastructure and deployment

For most projects, the production setup looks like this:

The application runs on Vercel. The database runs on Supabase or Neon. File uploads go to Cloudflare R2 or AWS S3. Transactional emails go through Resend. Error monitoring goes through Sentry.

This stack costs between $20 and $200 per month at startup scale and handles tens of thousands of users without any infrastructure work. No Docker configuration. No server management. No Kubernetes. The CI/CD pipeline is "push to main and it deploys."

Is this the right setup for a company doing 10 million requests per day? No. That company needs dedicated infrastructure, probably on AWS or GCP, with load balancers and auto-scaling and monitoring. But that company also has a dedicated infrastructure team. The companies we work with in the early stages don't have that team and shouldn't need one.

We've set up AWS infrastructure for clients who need it. VPCs, ECS clusters, RDS instances, CloudFront distributions. The AWS ecosystem is incredibly capable but it's operationally heavy. A simple Next.js application on AWS can easily involve 8 to 12 different services. On Vercel, it involves one.

We use Railway when a project needs a long-running backend process alongside the Vercel frontend. Railway sits in a nice middle ground between "managed platform" and "raw cloud infrastructure." You get containers, environment variables, private networking, and scaling without writing CloudFormation templates.

CI/CD, testing, and monitoring

Every project gets a GitHub Actions workflow that runs TypeScript compilation checks and linting on every pull request. This catches obvious errors before code review.

For testing, we're honest about what we prioritize. We write integration tests for critical business logic: payment flows, authentication, data calculations that can't be wrong. We write end-to-end tests for the core user journey. We don't aim for 100% test coverage because the effort required to get from 80% to 100% is disproportionate to the value it provides for early-stage products.

For monitoring in production, the setup is Sentry for error tracking, Vercel Analytics or PostHog for usage data, and Uptime Robot for availability alerts. This takes about 30 minutes to set up and covers 90% of what an early-stage product needs. We'll add Datadog or Grafana for more complex projects, but most products don't need that until they have a dedicated engineering team.

What we've tried and stopped using

Create React App. Officially deprecated at this point, but we stopped using it long before that. The build times were slow, the configuration was inflexible, and every project ended up ejecting to customize the webpack config.

Firebase. We've used it for a few projects that needed real-time data and rapid prototyping. It's fast to start with but limiting at scale. Custom queries are painful, data modeling follows Google's design rather than your product's needs, and migrating off Firebase later is genuinely difficult. We recommend Supabase instead for projects that want that "backend as a service" experience with the flexibility of a real database underneath.

Heroku. We used Heroku for years. The developer experience was great when it worked. But the pricing changed, the free tier disappeared in late 2022, and performance relative to competitors declined. Railway does everything Heroku did, for less money, with a better interface.

Styled Components. We used CSS-in-JS libraries for a while. The developer experience was good. The runtime performance was not. Server-side rendering with styled-components added complexity, and the trend in the React ecosystem has moved toward zero-runtime CSS solutions. We followed that trend and haven't looked back.

When we deviate from this stack

We're opinionated, not dogmatic. Here's when we reach for something different.

If a client has an existing product in Python or Ruby and wants to add features, we work in their stack. We're not going to rewrite a working Rails application in Next.js just because we prefer it.

If a product needs real-time collaboration (like a collaborative document editor), we'll use WebSocket infrastructure that doesn't fit neatly into the serverless model. That usually means a Node.js WebSocket server on Railway alongside the Next.js frontend.

If the product is a mobile app, not a web app, we use React Native. Different ecosystem, different considerations, different post.

And if a client comes to us with a strong preference for a specific technology, we'll have a conversation about why. Sometimes their reasoning is good and we adjust. Sometimes we explain our concerns and they adjust. The goal is always the right stack for the product, not a religious adherence to our defaults.

The meta-point

A technology stack is not a product. Users don't care whether you built their app with Next.js or Nuxt, Postgres or MySQL, Vercel or AWS. They care whether the product works, loads quickly, and does what they need it to do.

We have strong stack opinions because they make us more efficient, and efficiency translates to faster delivery and lower cost for clients. But the stack is always in service of the product. Never the other way around.


Want to know what the right technical choices are for your specific product? Walk us through the idea and we'll tell you what we'd build it with and why.


TopicsTech StackNext.jsPostgreSQLTypeScriptInfrastructureArchitecture

Explore More

More engineering insights, delivered.

Practical thinking on architecture, infrastructure, and shipping software that lasts.

← Browse all insights