See all articles
SaaS Architecture Nuxt Blueprint Cloud

My micro-SaaS blueprint for 2026

A technical and pragmatic guide to the technology choices for building and deploying a micro-SaaS in 2026.

Stéphane Monfort
11 min read
Share

Launching a micro-SaaS has never been more accessible than in 2026. Today, you can start from scratch in the morning and have a product billing customers by evening. The challenge is no longer whether you can do it, but where to begin so you can build on solid foundations: framework, database, authentication, payments, hosting, AI…

In the age of vibe coding, I’m convinced that software architecture fundamentals are more relevant than ever. A good structure has become a key factor in ensuring a product’s longevity and scalability. A well-thought-out, coherent design is what allows agents to produce controlled and lasting results.

This article presents my blueprint for building a micro-SaaS: my preferred stack, why I made these choices, and where I sometimes changed my mind. This isn’t gospel — it’s a highly subjective point of view, above all a battle-tested perspective from running a micro-SaaS in production.

Framework: Nuxt

As the backbone of the project, I quickly settled on Nuxt 4 (s'ouvre dans un nouvel onglet), the Vue (s'ouvre dans un nouvel onglet) meta-framework. I had already used it on past projects with great results, so the choice came naturally. Here’s what I value about it:

Why not Next.js (s'ouvre dans un nouvel onglet)? Great question! Next.js is excellent and has captured a staggering market share, but its relative lock-in with Vercel feels problematic to me. As soon as you want to leave their platform, everything can get more complex. With Nuxt and its Nitro engine, I maintain stronger control and a greater sense of independence. Vue’s simplicity is also a strong draw for me. I should admit that AI agents have a better knowledge of Next/React codebases than Nuxt/Vue — but nothing deal-breaking.

Architecture: modular monolith

The temptation to go with microservices is real, even if the hype has faded somewhat over the past few years. For an early-stage micro-SaaS, it’s almost always a mistake: distributed complexity (networking, consistency, observability) piles on top of business complexity, and you quickly spend more time wiring services together than shipping essential features.

I opted instead for a modular monolith: a single codebase in a monorepo, a single deployment, but a strict internal organization that keeps the door open to extracting services as dedicated projects if the need ever becomes clear.

In practice, each business domain in the Nuxt backend is a self-contained module, for example:

server/modules/
├── auth/
├── profile/
├── application/
├── subscription/
├── mailing/
└── shared/

Each module follows a hexagonal architecture (ports & adapters). I drew freely from this repository (s'ouvre dans un nouvel onglet), which remains a go-to reference I strongly recommend reading. A module is structured as follows:

server/modules/profile/
├── module.ts # Factory
├── core/
│ ├── entities/ # Business types and interfaces
│ │ └── user-profile.entity.ts
│ ├── ports/ # Interface contracts
│ │ ├── profile-repository.port.ts
│ │ └── avatar-storage.port.ts
│ └── usecases/ # Commands, queries, listeners
│ ├── update-profile.command.ts
│ ├── upload-avatar.command.ts
│ └── get-profile.query.ts
└── adapters/
├── primary/
│ └── rest/ # HTTP entry points (API routes)
│ └── profile.adapter.ts
└── secondary/ # Concrete implementations
├── drizzle-profile-repository.adapter.ts
└── s3-avatar-storage.adapter.ts

The core principle of hexagonal architecture is simple but profound: the core knows nothing about infrastructure. Entities, ports, and use cases never import an adapter, a framework, or an external library. It’s the adapters’ job to bridge the gap with the outside world.

  • Primary adapters (in adapters/primary/) are the entry points that drive the application: REST controllers, SQS (s'ouvre dans un nouvel onglet) listeners, CLI commands… They receive external stimuli, translate them into use case calls, and format the response. If you change your transport protocol (REST → GraphQL → WebSocket, for example), only this directory changes — the core stays untouched. In our case, Nuxt API routes import a primary REST adapter from a module.
  • Secondary adapters (in adapters/secondary/) are the output points driven by the application: Drizzle (s'ouvre dans un nouvel onglet) repositories for database access, Stripe (s'ouvre dans un nouvel onglet) clients, S3-compatible storage (S3 (s'ouvre dans un nouvel onglet)), email sending… They implement the ports defined in the core and are injected in the module’s factory. If you switch from Stripe to PayPal or from Drizzle to Prisma (s'ouvre dans un nouvel onglet), only this directory changes. The business core, once again, remains untouched.

Ports describe the contract expected by the domain as a simple interface.

server/modules/profile/core/ports/profile-repository.port.ts
export interface ProfileRepository {
findByUserId(userId: string): Promise<UserProfile | null>;
save(profile: UserProfile): Promise<void>;
}

The domain is therefore extremely easy to test, and adapters are swappable without touching any business logic.

Use cases are classes that implement a business use case and orchestrate the various ports. Concretely, a use case implements a run() method returning a Result<Ok, Err> via ts-results-es (s'ouvre dans un nouvel onglet). This explicit typing forces the caller to handle errors (something traditional exceptions don’t enforce), reinforcing the overall robustness of the application. Rust developers, among others, will know exactly what I mean.

server/modules/profile/core/usecases/update-profile.command.ts
export class UpdateProfile extends BaseUseCase<UpdateProfileInput, void, ProfileNotFound> {
readonly name = 'UpdateProfile';
constructor(
private readonly repository: ProfileRepository,
private readonly storage: AvatarStorage,
protected readonly eventBus: EventBus
) {
super();
}
protected async run(input: UpdateProfileInput): Promise<Result<void, ProfileNotFound>> {
const profile = await this.repository.findByUserId(input.userId);
if (!profile) return Err(new ProfileNotFound());
const updated = { ...profile, ...input.data };
await this.repository.save(updated);
await this.eventBus.emit({ type: 'PROFILE_UPDATED', payload: { userId: input.userId } });
return Ok.EMPTY;
}
}

Finally, modules can communicate with each other via an event bus, enabling better decoupling.

This architecture has an undeniable upfront cost: more files, more boilerplate, more layers. However, that pain has become marginal now that code-generation agents produce these structures almost instantly. In my view, this investment pays off very quickly and greatly improves the application’s maintainability.

Database: PostgreSQL + Drizzle

No surprises here: I chose PostgreSQL (s'ouvre dans un nouvel onglet) as the primary database. It’s the standard for any web application nowadays, with multiple deployment options: Supabase (s'ouvre dans un nouvel onglet), Neon (s'ouvre dans un nouvel onglet), or RDS (s'ouvre dans un nouvel onglet) if you’re already an AWS (s'ouvre dans un nouvel onglet) customer.

For the ORM, I chose Drizzle (s'ouvre dans un nouvel onglet) over Prisma:

  • Lighter: Drizzle is a thin layer on top of the SQL driver. No client generator, no Prisma binary to maintain, no heavy abstraction layer.
  • Type-safe: types are inferred directly from the schema. Drizzle generates exact SQL types, not approximations.
  • Direct SQL writing: when a query gets complex (CTEs, window functions), you write raw SQL. Drizzle doesn’t hide the SQL from you — it strongly types it.

Migrations are managed with Drizzle Kit: you modify your schema, run drizzle-kit generate, and get clean SQL that you can review manually before applying.

Authentication: Better Auth

For authentication, I went with Better Auth (s'ouvre dans un nouvel onglet). I’ve tested many different authentication libraries in TypeScript contexts over the years, with mixed results, and Better Auth feels like the most solid solution available today.

  • Framework-agnostic: works with any framework via its HTTP adapter (h3 for Nitro, but also Express (s'ouvre dans un nouvel onglet), Hono (s'ouvre dans un nouvel onglet), etc.)
  • Database in your schema: users and sessions live in your own database, not with a third party. You stay in control.
  • Rich ecosystem: for simplicity, I only use social logins (Google + LinkedIn), which frees the application from managing and securing user passwords. The Better Auth configuration is straightforward and well-documented.

Payments: Stripe

Stripe remains the simplest choice for payments. The Checkout (s'ouvre dans un nouvel onglet) (subscription creation) + Customer Portal (customer self-management) combo covers 90% of needs without writing any payment UI.

Stripe webhooks fit naturally into the modular architecture described above.

UI & Design: shadcn/vue + Tailwind v4

I build the interface with shadcn-vue (s'ouvre dans un nouvel onglet) (the shadcn/ui (s'ouvre dans un nouvel onglet) port for Vue) and Tailwind (s'ouvre dans un nouvel onglet) v4.

What I like about shadcn:

  • Copy, don’t import: components live in your code, not in a library. You own them, modify them, customize them.
  • Headless with reka-ui (s'ouvre dans un nouvel onglet): accessibility is handled by the primitives, the style is yours.
  • Consistent design system: colors are CSS variables, not hardcoded values. Change the primary palette, the entire theme follows.

AI: Vercel AI SDK

AI has become a standard ingredient in micro-SaaS products. I use it through the Vercel AI SDK (s'ouvre dans un nouvel onglet), which provides a clean abstraction layer over providers. I’m also exploring equivalent alternatives in parallel — the hexagonal architecture lets me swap implementations painlessly.

A few principles I apply:

  • Specialized agents, not a generalist: each AI operation has its own prompt, its own output schema, its own preferred model.
  • Versioned prompts: stored in server/assets/prompts/. Like code, they go through Git, code reviews, and automated deployments.
  • Guardrails: token limit per call, cost limit per user per month, fallback model (if Claude is down, we fall back to GPT).

Async: Event Bus + Job Queue

A micro-SaaS sometimes handles long-running operations: AI generation, document exports, third-party API sync… Running these synchronously inside an HTTP request risks timeouts and a poor user experience.

My solution: a business event bus feeding an async job queue. For long-running tasks that can’t fit in a synchronous HTTP call, I use an SQS queue that triggers a Lambda function to execute the job asynchronously. A custom-built Nitro plugin routes messages from an SQS queue to the right use case within the Nitro server, running alongside Nuxt’s native HTTP routes.

Emails: Resend

Email is a cross-cutting concern in any micro-SaaS: welcome on signup, profile update confirmation, notifications, follow-ups, invoices… I use Resend (s'ouvre dans un nouvel onglet) for all transactional and marketing emails.

Why Resend over AWS SES? SES is powerful but complex to set up (verified domains, DKIM, SPF, sandbox restrictions at the start). Resend integrates in 5 minutes: an API key, a template, and you’re done. It also offers its own broadcast service for newsletters and an SDK for fine-grained management of contacts and customer segments for targeted campaigns.

I use the vuemail (s'ouvre dans un nouvel onglet) library (the React Email (s'ouvre dans un nouvel onglet) equivalent for Vue) to compose templates the same way I build the rest of the site.

Testing & Quality

On a solo or small-team project, the temptation to skip tests is real. I don’t take that shortcut — tests are now an indispensable safety net for a good agentic workflow.

My testing stack is fairly standard: vitest (s'ouvre dans un nouvel onglet) for unit-testing use cases, playwright (s'ouvre dans un nouvel onglet) for end-to-end tests on critical user flows.

On top of that, I noticed several times that the architecture guidelines explicitly stated in the CLAUDE.md file were not always fully respected. I therefore added architecture tests with ts-arch (s'ouvre dans un nouvel onglet), which reinforce the agentic workflow with a deterministic check that architecture rules are being followed (module dependency boundaries, for example). Since then, my coding agent consistently produces code that matches my guidelines — correcting itself when it strays.

server/modules/__tests__/architecture.test.ts
describe('Hexagonal layers', () => {
test('core files must not depend on adapters', async () => {
const rule = filesOfProject(TSCONFIG)
.inFolder('core')
.shouldNot()
.dependOnFiles()
.inFolder('adapters');
const violations = await rule.check();
expect(violations, fmt(violations as FileDep[])).toHaveLength(0);
});
});

I round out the workflow with a few standard tools:

Hosting: AWS

For convenience, I started with Vercel as my hosting solution. Vercel natively recognizes a Nuxt project on a GitHub repository and deploys it in one click — hard to beat. But with strong AWS expertise, I found myself struggling to accept some of Vercel’s plan limitations.

I quickly moved to a self-managed deployment on AWS orchestrated by CDK (s'ouvre dans un nouvel onglet), with a fairly simple architecture:

Misc

A few additional tools round out the stack:

Conclusion

This blueprint is not absolute truth — it’s a highly subjective view shaped by my personal journey. I fully understand it won’t suit everyone, and that’s perfectly fine!

TL;DR: the stack checklist

  • Framework: Nuxt 4 (full-stack modular monolith, Nitro server)
  • Architecture: modular, ports & adapters, Result type, event bus
  • DB: PostgreSQL + Drizzle ORM
  • Auth: Better Auth (OAuth social)
  • Payments: Stripe (Checkout + Portal, feature flags)
  • UI: shadcn/vue + Tailwind v4
  • AI: Vercel AI SDK (specialized agents, versioned prompts)
  • Async: Event bus + job queue (SQS in production)
  • Tests: Vitest + Playwright + tsarch
  • Hosting: AWS CDK, Lambda, CloudFront, SQS

Comments