Client MVP Suite: 5 Full-Stack Products in 4 Months
Context
At the agency, I was the only engineer. Five clients, five MVPs, four months. 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 on-call permanently.
This is a breakdown of what I built, the patterns I relied on, and what shipping at that pace actually 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
The vendor isolation was handled at the database level using a vendor_id foreign key on every resource, with AdonisJS middleware enforcing scope on every request. No vendor could ever touch another's data — not through bugs, 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 was building the frontend and auth layer only.
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 required a solid permissions system — three roles (admin, dispatcher, driver) with different views and different write access.
I implemented role-based access at the database query level, not just the UI level. Every query was pre-filtered by the authenticated user's role and scope. The driver can only see their own assignments. The dispatcher can see their zone. Admin sees 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 lightweight 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, while 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.
The main takeaway: 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 actually mattered.