My Monorepo Setup
I maintain a monorepo for all my personal projects. It contains this portfolio site, Cursor Wrapped, Secret Santa, Spoticover, Date Calculator, and a handful of other apps. They share infrastructure code, TypeScript configs, ESLint rules, Vite configs, and backend utilities. Deploying a new project takes about 15 lines of code.
This post is about why I chose this structure, the tooling decisions behind it, and some opinions about the modern frontend ecosystem that informed those choices.
The Anti-Magic Philosophy
I don't like magic in my frameworks. I don't like things happening that I didn't explicitly ask for. I don't like vendor lock-in. I don't like pricing models designed to extract maximum value from developers who don't know better.
This philosophy guides most of my tooling choices.
Why Not Vercel
Vercel is the most money-hungry solution I've ever encountered in web development. The pricing is designed to catch you out. The framework (Next.js) is designed to create load. The Link component prefetches aggressively. React Server Components are, in my opinion, fundamentally unsafe for most use cases. And once you're in, you're locked in.
I don't believe in SSR for most applications. Most clients can handle a moderate SPA. If you need server-rendered content, build BFF (Backend for Frontend) endpoints. If you need SEO, use SSG with hydration. The complexity of RSC isn't worth it for 90% of projects.
The lack of configurability is the final nail. When something goes wrong with Vercel magic, you're stuck. You can't debug it. You can't work around it. You file a support ticket and hope.
Why AWS (And Not Cloudflare)
I'm an AWS fanboy. I'll admit it. I couple to it hard.
Cloudflare has some nice things. Workers are genuinely good. The pricing model is more honest than Vercel. The CDN and WAF are solid. But I keep coming back to AWS for a few reasons.
First, AWS Control Tower and AWS Organizations. I can spin up isolated accounts for different projects, manage billing centrally, and apply security policies across everything. I'm constantly developing ideas for commercialisation, and knowing exactly which project is costing what is invaluable. Not to mention, often my smaller projects are far from secure.
Second, the "use what you need" model. I'm not locked into a plan tier. I use AWS S3, CloudFront, Route53, ACM, Lambda, DynamoDB, RDS, API Gateway, SQS, SNS, etc.. I pay for what I use. If I need something new, I add it. If I stop using something, I stop paying for it.
Third, Infrastructure as Code is nicer. Pulumi with AWS feels more complete than Pulumi with Cloudflare. The resource coverage is better. The documentation is better. The patterns are more established.
The Stack
Vike for SSG
Vike is my preferred framework for static sites (like this portfolio). It's a regular open source framework with no vendor lock-in and no magic out of the box.
If you want magic, you configure it. You add a plugin that does it. But nothing happens that you didn't ask for.
I can run Vike anywhere. I can even do SSR with Vike on any infrastructure I choose. It's the control that I like. Compare that to Next.js, where you're basically choosing to deploy to Vercel or fight an uphill battle.
React + Vite for SPAs
For single-page applications, it's React + Vite. Fast dev server, fast builds, good plugin ecosystem, no magic. The Vite config is explicit. The build output is predictable.
shadcn for Components
I used to build everything with custom components. Every button, every input, every modal. Hand-crafted with care.
Then I realised something: users don't care if your site looks like every other tech startup, as long as it solves their problems.
shadcn/ui changed my velocity. It's not a component library in the traditional sense. It's a collection of components you copy into your project. You own them. You can modify them. There's no version to upgrade, no breaking changes to worry about.
The components are built on Radix primitives, which means accessibility is handled. The styling is Tailwind, which I was already using. It just fits.
The Monorepo Structure
The repo has two main directories:
common-packages/
Shared code that any project can use:
@rebnz/infrastructure- Pulumi components for deploying SPAs (S3 + CloudFront + SSL + cache invalidation)@rebnz/backend-utils- DynamoDB repository implementations, error mapping for Neverthrow, common validation utilities, base classes for Controllers, Services, and Repositories, common middlewares, etc@rebnz/typescript-config- Base TypeScript configs for different project types@rebnz/eslint-config- Shared linting rules@rebnz/vite-config- Shared Vite configurations
projects/
Individual projects, each with their own apps, services, packages, and infrastructure:
projects/
commerical-project-example/
apps/
frontend/
marketing-homepage/
services/
backend/
infrastructure/
platform/
marketing/
packages/
core-types/
reb-quickdeploy/
apps/
santa/
cursor-wrapped/
services/
santa-api/
infrastructure/
The 15-Line Deployment
Here's what it looks like to deploy a new basic SPA:
import { deployAmplifySpa, createServiceBucket } from "@rebnz/infrastructure";
const bucket = createServiceBucket({ namespace: "my-app" });
deployAmplifySpa({
buildDirectory: "../apps/my-app/dist",
namespace: "my-app",
serviceBucketId: bucket.id,
appName: "my-app",
domain: "my-app.reb.nz",
awsProfile: "default",
version: commitHash,
});That's it. The shared infrastructure package handles:
- Creating the Amplify project
- Attaching domains
- Deploying the assets via S3
All the complexity is in the shared package, tested and refined over multiple projects. New projects get all of that for free.
The same is true for more complex projects where configuration will someday be king. The shared infrastructure package provides utilities for deploying directly via S3 and Cloudfront. It takes care of:
- Creating an S3 bucket with proper security settings
- Setting up CloudFront with Origin Access Control
- Requesting and validating an SSL certificate
- Configuring cache behaviors for different asset types
- Invalidating the cache on every deployment
When you need configuration beyond what you expect from a utility, simply eject and create your own resources with the same Pulumi name.
The Trade-offs
It's not all upside. Monorepos have costs.
IDE performance can suffer with large repos. I haven't hit this yet, but I'm aware it's coming.
Build times can grow. Turborepo helps here. I only rebuild what changed.
Cognitive overhead is real. New contributors need to understand the structure before they can be productive.
Dependency management gets tricky. A dependency update in a shared package affects every project that uses it.
For my use case (one developer, multiple small projects, shared infrastructure patterns) the trade-offs are worth it. For a team of 50 working on a single product, a polyrepo might make more sense.
Conclusion
The monorepo isn't about following a trend. It's about reducing the friction of shipping. When I have an idea for a new project, I don't want to spend a day setting up infrastructure. I want to write code and deploy it.
The anti-magic philosophy means I understand what's happening. When something breaks, I can fix it. When I need something different, I can build it. I'm not waiting for a vendor to add a feature or fix a bug.
It's more work upfront. But the compound interest of shared infrastructure pays off every time I ship something new.