My micro-SaaS blueprint for 2026
A technical and pragmatic guide to the technology choices for building and deploying a micro-SaaS in 2026.
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:
- Unified full-stack: frontend and backend in a single project, with native shared typing between client and server for an optimized DX.
- Hybrid rendering: you decide page by page whether it should be rendered statically (great for landing pages or blog posts) or dynamically. Nuxt pre-renders static routes at build time, boosting performance and therefore SEO.
- Deploy anywhere: Nuxt is powered by Nitro (s'ouvre dans un nouvel onglet), which provides presets for Node, serverless environments (Lambda (s'ouvre dans un nouvel onglet), Vercel (s'ouvre dans un nouvel onglet), Netlify (s'ouvre dans un nouvel onglet), Cloudflare Workers (s'ouvre dans un nouvel onglet)), or even Docker (s'ouvre dans un nouvel onglet). This gives you tremendous deployment flexibility — and lets you pivot quickly if needed.
- Official modules: a rich collection of officially supported modules, including Nuxt Content (s'ouvre dans un nouvel onglet) which enables markdown-based editing for editorial content pages.
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.tsThe 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.
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.
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.
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:
- ESLint (s'ouvre dans un nouvel onglet) + Prettier (s'ouvre dans un nouvel onglet): automatic formatting
- simple-git-hooks (s'ouvre dans un nouvel onglet) + lint-staged (s'ouvre dans un nouvel onglet): formatting and linting before each commit
- commitlint (s'ouvre dans un nouvel onglet): enforces conventional commit rules
- semantic-release (s'ouvre dans un nouvel onglet): automated versioning and changelog generation
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:
- Lambda (Node 24, ARM64): to run the Nitro bundle, billed per millisecond with automatic scaling
- CloudFront (s'ouvre dans un nouvel onglet): CDN + on-the-fly URL rewriting (via CloudFront Functions)
- S3: for storing static assets and backups
- SQS: async job queue, with a DLQ for failures
- EventBridge (s'ouvre dans un nouvel onglet): scheduled crons for background tasks
Misc
A few additional tools round out the stack:
-
Dotenvx (s'ouvre dans un nouvel onglet) for secret management: environment files are encrypted per environment, making it safe to commit them to Git. At deploy time, they are synced to AWS Secrets Manager for runtime access by the Lambda function.
-
Klaro (s'ouvre dans un nouvel onglet) for consent and cookie management. Lightweight and configurable, it integrates in a few lines and blocks third-party scripts before consent is given.
-
Honeybadger (s'ouvre dans un nouvel onglet) for error tracking, on both client and server. I get notified the moment an unhandled exception occurs in production. Simple, effective, and without the outrageous pricing of alternatives.
-
ConfigCat (s'ouvre dans un nouvel onglet) for feature flags. I use it for progressive feature rollouts, overriding Stripe prices without a deployment, hot-swapping AI models, and managing beta testers. The OpenFeature (s'ouvre dans un nouvel onglet) integration lets me switch providers without touching the code.
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