Client MVP suite: 5 full-stack products in 4 months
Five MVPs, four months, one engineer
I was the only engineer at the agency. Each project had real users from day one, a different domain, and a different set of requirements. There was no room for bespoke architecture decisions on every project. I needed a repeatable system that could be picked up fast, delivered clean, and maintained without me being permanently on call.
This is a breakdown of what I built, the patterns I relied on, and what shipping at that pace looks like.
The playbook
Before any client work started, I established a shared project template. Same folder structure, same tooling, same deployment pipeline. The only variable was the domain logic.
project/
├── apps/
│ ├── web/ ← SvelteKit or Next.js (per-project)
│ └── api/ ← AdonisJS REST API
├── packages/
│ ├── db/ ← Drizzle ORM schema + migrations
│ └── shared/ ← Zod schemas, shared types
└── docker-compose.yml
Having this ready meant I wasn't spending the first week on scaffolding. I was writing product code from day one.
The five projects
1. Multi-vendor marketplace: SvelteKit + AdonisJS
A local artisan goods marketplace where multiple vendors could list and manage products independently. The tricky parts:
- Each vendor had their own inventory, orders, and payout dashboard
- File uploads (product images) went direct-to-R2 via presigned PUTs. The API never touched the payload
- Local payment gateway (Midtrans) integration with webhook-based order confirmation
Vendor isolation was handled at the database level with a vendor_id foreign key on every resource. AdonisJS middleware enforced scope on every request. No vendor could ever touch another's data, not through bugs and not through URL manipulation.
Delivered in: 4.5 weeks
2. SaaS analytics dashboard: Next.js App Router
A B2B dashboard for a software client tracking engagement metrics across their product suite. Their backend already existed. I built only the frontend and auth layer.
The constraint was bundle size. Their users were often on slower connections in lower-bandwidth regions. I leaned hard into React Server Components to push as much rendering to the server as possible:
- Static summary cards → RSC, zero client JS
- Interactive charts (Recharts) → lazy-loaded client components, only hydrated on scroll-into-view
- Auth → JWT validated in middleware, no client-side session state
Final bundle: 68KB gzipped for the main page. No JS framework overhead on the critical path.
Delivered in: 3.5 weeks
3. Service booking platform: SvelteKit
A booking system for a services business: think appointment scheduling with staff availability, time slots, and confirmation emails.
The calendar was the hardest part. Naive implementations cause N+1 queries: for every rendered date cell, a query to check availability. I pre-computed the booked slot map server-side:
// One query, O(1) lookup in the template
const bookedSlots = await db
.select({ date: bookings.date, time: bookings.time })
.from(bookings)
.where(
and(
eq(bookings.staff_id, staffId),
between(bookings.date, rangeStart, rangeEnd),
),
);
const slotMap = new Map<string, Set<string>>();
for (const { date, time } of bookedSlots) {
if (!slotMap.has(date)) slotMap.set(date, new Set());
slotMap.get(date)!.add(time);
}One query per page load. The template checks slotMap.get(date)?.has(time). Constant time.
Delivered in: 5 weeks
4. Internal operations tool: Next.js + Drizzle
An internal CRUD tool for a logistics client to manage shipments, drivers, and delivery zones. Not glamorous, but it required a solid permissions system: three roles (admin, dispatcher, driver), each with different views and write access.
I enforced role-based access at the database query level rather than in the UI. Every query filters by the authenticated user's role and scope. Drivers see only their own assignments. Dispatchers see their zone. Admins see everything.
No custom auth library: just JWT claims, Drizzle's where clauses, and a single withUserScope(userId, role) query builder wrapper that every route used.
Delivered in: 3 weeks
5. Content publishing platform: SvelteKit + AdonisJS
A CMS-like tool for a media client to manage and publish articles, with a custom editor, image uploads, and a public-facing reader site all in one product.
The editor was the main challenge. Rich text editors are notoriously hard to SSR. I used Tiptap, wrapped in a <svelte:component> that only mounts client-side. The reader-facing article pages are fully SSR with zero client JS. Two different rendering contexts, one codebase.
Delivered in: 4 weeks
What made it work
Drizzle as the single source of truth. Schema defined once, types inferred everywhere. No drift between the database and the application layer. Running a migration was a one-liner, and the TypeScript compiler caught any shape mismatches immediately.
Presigned uploads, always. Every project that handled file uploads used direct-to-storage presigned PUTs. The API hands out a URL, the client uploads directly, the API confirms after. No proxied uploads, no memory pressure, no timeout issues on large files.
Zod at the boundary. Every API request was validated with a shared Zod schema before touching the database. Same schema used for form validation on the frontend. One definition, two uses, zero inconsistency.
Docker Compose for deployment. Every project shipped with a docker-compose.yml that the client's VPS could run with a single command. No deployment complexity, no CI/CD to set up for the client. Just SSH and docker compose up -d.
Results
| Project | Stack | Delivery |
|---|---|---|
| Marketplace | SvelteKit + Adonis | 4.5 weeks |
| SaaS Dashboard | Next.js App Router | 3.5 weeks |
| Booking System | SvelteKit | 5 weeks |
| Operations Tool | Next.js + Drizzle | 3 weeks |
| CMS + Reader | SvelteKit + Adonis | 4 weeks |
All five hit production inside a 4-month window. No post-launch critical bugs on any project. Every client had a working product, a Dockerised deployment, and enough documentation to hand off to their own team if needed.
Opinionated defaults beat flexibility when you're moving fast. The less I had to decide per project, the more energy I had for the parts that mattered.