Skip to content

Writing

Why I Built My Own Standard, and Why You Should Build Yours

After rebuilding auth, roles, audit logs and CI for every new project, I put those decisions into one repository. What is in standard, how it keeps itself honest, and how to build your own.

Why I Built My Own Standard, and Why You Should Build Yours, by Maulana Sodiqin

This year I built two client platforms on almost the same stack: Hono and oRPC on the server, Drizzle and Postgres, better-auth, a React SPA on TanStack Router, and a queue for background jobs. Both times, the first stretch of work had nothing to do with the client's problem. It was sign-in, password reset, roles, an audit log, file uploads, mail in development, health checks, CI, and deciding again how errors should travel from the database to the UI.

After the second time I stopped copying folders between repositories and started standard: one repository that holds those decisions, already wired together, so the next project starts from the part that is actually new. It went public on 9 September and has had close to 200 commits since.

What is in it

The stack is the one I keep reaching for:

  • moon and pnpm workspaces for the monorepo
  • Hono and oRPC for the API, with Effect for the business logic
  • Drizzle and Postgres, better-auth with one role per user
  • RabbitMQ and Redis for background work, nodemailer with mailpit in development, S3-compatible storage for files
  • React 19 with TanStack Router, Query, Form and Store, Vite, Tailwind 4 and shadcn/ui
  • Biome, Vitest and Playwright

None of those choices are unusual. What took the time is everything a real product needs that a starter template usually leaves out:

  • Accounts that behave like production accounts. Email confirmation, password reset, invitations, deactivation that signs a user out everywhere without deleting them, a list of sessions an admin can revoke, and two-factor authentication with an authenticator app, ten backup codes and an admin reset.
  • Roles and permissions shared between the API and the web app, so a button is hidden by the same rule the endpoint enforces.
  • An activity log that records who changed what, including which fields.
  • Health endpoints that mean something. /healthz only says the process is up, /ready checks every dependency and returns 503 when one is down, and /metrics serves Prometheus data behind a token.
  • One version number. The root package.json version is served by both the API and the web app, and every change bumps it, so /health always tells you which build you are looking at and whether the two sides are out of step.
  • An example module, notes with attachments, that touches every layer. A new module is built by following it.

The decisions are written down

The code is maybe half of it. The other half is documentation that says how the code is supposed to grow:

  • AGENTS.md lists the rules a contributor works under and which file to read before which kind of change.
  • A single TypeScript ruleset covers component signatures, arrow functions only, no plain strings, files under 200 lines, ts-pattern for branching, ts-belt for data, and where Effect is allowed.
  • docs/adding-a-module.md lists every place a new module, endpoint or permission has to be registered, including the ones you would not find by reading the folder tree.
  • docs/operations/ has the deployment guide, backups and restores, alerting, logging, retention and runbooks.

Writing this down mattered more than I expected, because a lot of my code is now written with coding agents. An agent follows the conventions it can read. When the rules live in my head, every session starts by rediscovering them. When they live in AGENTS.md, the agent and a new teammate get the same instructions, and the output looks like the rest of the codebase.

Rules the build enforces

Documentation alone drifts. The rules that matter most are checked by tools:

  • The API is organised by module, each with domain, application, infrastructure and presentation layers. moon run api:arch fails the build when a layer imports upward or one module reaches into another without going through its public entry point.
  • Packages carry a tag (infra, identity or app), and moon refuses to build the project graph if a dependency points the wrong way.
  • trunk is protected for everyone, admins included. Every change goes through a pull request, three checks must pass (check, test and build; API and web end-to-end tests; a Drizzle schema drift check), and merges are squash-only so history stays linear.
  • Releases are automatic. Every merge carries a version bump, the workflow tags the commit once its checks are green, and the release notes come from the pull request's changelog section.

There is also a compliance matrix in docs/kpi/ that compares the repository against an external engineering standards rubric: 37 standards from the rubric plus 16 the boilerplate adds on its own, each ranked P0 to P3 with its current status. It is the most useful file when someone asks whether the project is ready for production, because the answer is a table instead of an opinion.

It has to stay neutral

standard is meant to be reused, so AGENTS.md bans any client, company or vendor name anywhere in it, including commit messages and pull request titles. Release notes are generated from pull request bodies, so a name that slips into one keeps showing up on the release page. Domain examples are deliberately generic, which is why the sample module is notes and not anything from a real project.

Why you should build your own

You could start from someone else's boilerplate, and for a one-off project that is the right call. But a boilerplate you did not build is a set of decisions you have not made. The first time something breaks, you are debugging another person's opinions.

Building your own gives you a few things a template cannot:

  • You know why every piece is there, so you know which pieces you can remove.
  • Your conventions stop being habits and become documents, which is what lets teammates and agents follow them.
  • Each project improves the next one. A fix made in a project built on it can go back into standard, and every later project starts with it.

It also has a cost. A standard is a product you maintain. Dependencies move, Effect 4 is still a release candidate, and the documentation has to change with the code or it starts to lie.

If you want to try it, I would not start from an empty folder. Wait until you have built two or three projects on the same stack, then extract what they had in common from the most recent one. Start with what you rebuilt every time, which for me was auth, roles, the activity log and the CI pipeline. Write the conventions down as soon as you notice yourself explaining them twice. Then use it for the next project and fix whatever gets in your way.

The repository is public at github.com/maulanasdqn/standard. Feel free to read it, fork it, or take the parts that fit how you work. It will be more useful to you as a reference for your own standard than as a template to copy as is.