Three iterations before the right structure
Karmpath is a content management and publishing platform — but it took three failed attempts before the product shape was clear enough to build confidently. The first two iterations tried to serve too many audiences. The third found the ThemeForest thesis: build for independent creators who need a professional platform without the platform lock-in.
That thesis determined every subsequent architectural decision. See My Development Philosophy for why shipping the wrong thing is still better than planning the right thing indefinitely.
Why a monorepo from the start
The target platform list for Karmpath included web, admin, and potential mobile surfaces — plus a component library that needed to be shared. Separate repos for each would have meant immediate divergence of shared types, auth utilities, and UI primitives. An Nx monorepo with explicit package boundaries prevents that from the first commit.
The structure: a shared @karmpath/auth package wrapping Better Auth sessions, a shared @karmpath/db package with Drizzle ORM schema and migration files, a shared @karmpath/ui component library, and the web and admin apps consuming them. A type error in the auth package shows up across all consumers immediately. A schema migration runs once and covers everything that touches the database.
Better Auth over rolling your own
Auth is the part of a product where the cost of a bug is highest and the differentiation is lowest. Better Auth handles sessions, refresh tokens, OAuth providers, and email verification with a typed API that integrates cleanly with Drizzle. The decision not to write a custom auth layer freed roughly three weeks that went into the RBAC implementation instead.
The 7×42 RBAC matrix — seven role levels, 42 permission dimensions — is Karmpath's main access control surface. That complexity is where the product-specific engineering lived. Better Auth handled the authentication boundary so I could focus on the authorisation logic that actually varies per tenant.
Neon Postgres for the database
Neon is serverless Postgres with branching — you can branch the database the same way you branch git. During Karmpath's development, every feature branch had its own database branch with isolated schema state. No shared staging database that different branches collide on. Drizzle's migration files applied to each branch independently.
The practical result: I could test a schema migration on the branch database, merge the branch, and apply the migration to production knowing it had already run once on an identical schema. That confidence is worth the marginal cost of Neon over a standard managed Postgres.
The RBAC and a11y investment
Two things in Karmpath took longer than expected and both paid forward. The 7×42 RBAC matrix required careful modelling — getting the permission dimensions right so new roles could be added without restructuring the permission table. The investment was a week. The result is a permission system that's additive, not a rewrite every time a new user type is introduced.
The a11y and SEO audit hitting 100 on both metrics came from treating accessibility as architecture rather than post-shipping polish. Semantic HTML, correct ARIA roles, structured data for SEO — these are cheaper to build in than to retrofit. A ThemeForest product that ranks well in search and is accessible to screen readers is a product that sells on merit, not just on screenshots.
What I'd do differently
Start the CI pipeline earlier. I enforced the zero-TypeScript-error rule from commit one, but the full test suite came later. Retrofitting tests onto working code is slower than writing them alongside. The Nx affected tooling makes per-package test runs fast — the value of that setup is highest when the test coverage is high, which means getting coverage up early matters.
See Lessons from Infrastructure for the infrastructure decisions that run parallel to the application architecture.