production-saas-starter: a B2B SaaS skeleton in Go and Next.js with tenancy built in
Production-ready B2B SaaS Starter Kit (Go + Next.js). Modular Monolith. Hexagonal Arch.
At a glance
- What is it?
- The production-saas-starter is a MIT-licensed boilerplate for business software that pairs a Go 1.27 API on Gin with a Next.js 16 front end, and it ships the parts founders usually rewrite badly: magic link authentication, organisation-scoped access backed by PostgreSQL row-level security, three explicit roles and optional Polar billing. It suits a team starting a multi-tenant product, and it is a starting point rather than a framework.
- Who is it for?
- The production-saas-starter is worth forking if you are building business software for organisations, because the expensive parts of that shape are already written, with a magic link sign-in, a three role model, organisation scoping enforced in the API and in the database, and generated queries on both sides, so you are not the first person to get the tenant isolation subtly wrong.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the starter is, and the scope it deliberately keeps
The project describes itself as a lean B2B SaaS boilerplate for founders, built with Next.js 16 and Go 1.27, and the shape of the offer is stated in one sentence: start with organization accounts, team management, and optional billing, then add your product.
That last clause is the important one, because a starter kit is usually sold on the breadth of what it includes, and this one lists what is coming soon instead. Audit logs, a customer-facing webhooks interface and built-in charts and usage tracking are named as ideas for optional extensions, outside the minimal core.
The repository metadata calls the architecture a modular monolith with hexagonal architecture, and the documentation points at a separate architecture document for the code structure, a setup document, a development document, an upgrade guide for existing installations, a verification document recording completed checks and provider limitations, and a document on AI development covering repository-local coding skills, agent roles and optional Better Auth documentation tools.
The stack is pinned to specific versions, which is worth noting for a boilerplate. The front end is Next.js 16 at version 16.3.6 with the App Router and API routes, React 19 at 19.3.0, TypeScript 5.9.3, Tailwind CSS 3.4.19, shadcn/ui with Radix UI, and TanStack Query at 5.103.2. The back end is Go 1.27.1 with Gin, PostgreSQL, and SQLC as a type-safe SQL compiler with no ORM.
Authentication is Better Auth, self-hosted, with magic links and organisation membership, and billing is Polar.sh, optional. The deployment story is Docker and Docker Compose with Caddy as the entry point, which is a sensible default for a project that wants one command to run and a reverse proxy that handles certificates.
The licence is MIT, the primary language is TypeScript, the default branch is main, and the last push to the repository was on 2026-10-15. The tree has a go-b2b-starter/ directory for the Go service, a next_b2b_starter/ directory for the front end, a scripts/ directory, a tests/ directory and a docs/ directory.
The tenancy model, which is the part that is actually expensive
The feature list names multi-tenancy as organisation-scoped access enforced by the Go API and PostgreSQL, roles and permissions as three explicit roles, Member, Manager and Admin, checked against current membership, and team management covering invites, role changes and settings updates.
Read those three together and the design becomes clear. The access model is not a flag on a user row. It is a membership, and the role is checked against the current membership rather than against a cached copy of it, and the same rule is expressed twice, once in the API and once in the database.
The database side is PostgreSQL row-level security, and the backend stack lists organization authorization as live Better Auth membership checks plus PostgreSQL row-level security. That pairing is the part that distinguishes this from most starters. A check in the API alone protects the API, and an API that a future engineer forgets to extend is a data leak. A row-level security policy in the database protects the data whatever the application does, because the database refuses the query.
Doing both means the application is written as though it can trust the tenant, which is what keeps the query code readable, and the database is written as though the application might be wrong, which is what keeps the data safe. The cost is that the two have to be kept consistent, and the documentation's reference to provider limitations recorded in the verification document suggests the author has been explicit about where the model stops.
The three roles are also explicit rather than configurable, and that is a sensible default for a boilerplate. Member, Manager and Admin cover most business products, and a role model you can extend is a role model that has to be designed, whereas a role model you have to rename is not.
The local setup, and the Mailpit trick that makes magic links testable
The getting started section is built around one command.
Docker and Docker Compose are the prerequisites, and the documentation is explicit that Docker is all you need for the default setup on Linux, macOS or WSL. For local development outside containers the requirements are Go 1.27.1, Node.js 24 LTS and pnpm 10.32.1.
From the cloned repository, one command creates a private .env file and starts the application:
./setup.shAfter that there are three steps. Visit http://localhost:3000. Sign in by creating your workspace and opening the magic link in the local email inbox at http://localhost:8025, and configure real SMTP for deployment using the setup document. And enable billing when needed, by configuring Polar and setting BILLING_ENABLED=true, since billing is disabled by default.
The local inbox is the interesting part, because a magic link sign-in is otherwise awkward to develop against. Rather than requiring a real SMTP server, the stack runs Mailpit, and the environment file configures it:
COMPOSE_PROFILES=local
MAILPIT_PORT=8025
SMTP_HOST=mailpit
SMTP_PORT=1025
SMTP_SECURE=false
EMAIL_FROM=Starter <[email protected]>So the default profile is local, the mail catcher is a service on the compose network, and the from address is a deliberately non-routable local domain. A developer clicks through the real sign-in flow with a real message, and nobody has to configure an SMTP provider to see their first magic link.
The environment file is also where the security decisions are visible. The secrets are separated, with a POSTGRES_PASSWORD for the superuser, an APP_DATABASE_PASSWORD for the application role and an AUTH_DATABASE_PASSWORD for the authentication role, alongside BETTER_AUTH_SECRET and AUTH_INTERNAL_SECRET for the authentication service. The comment at the top says that ./setup.sh fills blank secrets locally and preserves existing values, so the script is a local convenience that does not overwrite a configured environment.
The database roles, and the migration step that is not optional
The compose file shows the part of the setup that a lot of starters get wrong, which is the database role separation.
Three services carry it. The postgres service runs PostgreSQL 18.6 on Alpine, pinned by image digest rather than by tag, with a health check that uses pg_isready against the configured user and database, an interval of five seconds, a three second timeout and ten retries. The database-init service runs a script that creates the roles, and it needs the application and authentication passwords, which is why those are separate variables in the environment file rather than one shared password. The backend-migrate service then runs the Go binary with the migrate command and waits for the role creation to have completed successfully before it starts.
The depends_on conditions are the interesting part. The migration service depends on database-init with the condition service_completed_successfully, and database-init depends on postgres with the condition service_healthy. So the ordering is: the database is up and accepting connections, then the roles exist, then the migrations run. That ordering is not incidental, because a migration that creates tables owned by a role that does not exist yet fails in a way that is confusing, and a migration that runs against a database that is not ready fails in a way that is worse.
The volumes section has a comment about PostgreSQL 18 and above keeping PGDATA in a major-version subdirectory, and it says to keep the volume name so the upstream entrypoint detects and rejects old data. That is a small piece of operational knowledge about a specific PostgreSQL release, and it is the kind of comment that saves somebody an afternoon when a database refuses to start after an upgrade.
The two database roles are also the mechanism behind the row-level security story. A role that can only see its own organisation's rows is implemented with a session variable set by the API on connection, and the migration and the API have to agree on the name of that variable, which is why the roles are created by a script that lives in the repository rather than by a line in the setup instructions.
The type safety claim, and what it does not cover
The type safety feature is stated as generated SQLC database queries in Go and TypeScript in the frontend, and the backend stack describes SQLC as a type-safe SQL compiler for Go with no ORM.
That is a strong choice, and it is worth being precise about what it does and does not buy. SQLC reads the SQL you wrote and generates Go functions with typed parameters and typed rows, so a query that returns a column you renamed stops compiling rather than failing at runtime. On the front end the generated types mean the shape of a row is known when the fetch is written.
What it does not buy is any protection against the query being wrong. A query that joins the wrong table, or filters on a column that is not tenant scoped, generates happily and types perfectly. The generated types protect the boundary between the query and the code that consumes it, and the tenant isolation is protected by the row-level security policies rather than by the type system. Those are two different guarantees, and the project lists them as two different features, which is the right way to present them.
The no ORM decision also has a second-order effect. Without an ORM, there is no lazy loading and no identity map, so the shape of a request is visible in the code rather than implied by a relationship definition, and the cost is that you write the queries and you own the migrations. For a B2B product with a stable schema that is a good trade, and for a product whose schema will change weekly it is a cost.
The coming-soon list makes a related point about scope. Audit logging, a webhooks interface and analytics are all features that are easy to add badly and expensive to retrofit, and the project is honest that they are ideas rather than inclusions. A team that needs audit logs on day one will have to build them, and the record of activities is exactly the kind of thing a multi-tenant product needs before it has paying customers rather than after.
Editorial conclusion
The production-saas-starter is worth forking if you are building business software for organisations, because the expensive parts of that shape are already written, with a magic link sign-in, a three role model, organisation scoping enforced in the API and in the database, and generated queries on both sides, so you are not the first person to get the tenant isolation subtly wrong. It is the wrong tool if you are building a consumer product with no organisations, and it is the wrong tool if you would rather own the whole stack. Before you rely on it, check three things: that the tenancy model fits, because the access rules are built around organisation membership, live Better Auth membership checks and PostgreSQL row-level security, and a product whose central entity is a user rather than a team will inherit a structure it does not need; that the SQL layer is generated rather than hand-written, because the project uses SQLC as a type-safe SQL compiler with no ORM, so changing a query means regenerating and the database is still PostgreSQL with its own migrations; and that the local setup is honest about what it does, since the one-line script starts a Mailpit instance so the magic link arrives at a local inbox on port 8025, and real deployments need SMTP configured separately, which is exactly the step a first run tends to forget.
Frequently asked questions
How do I get the production-saas-starter running locally?
Clone the repository and run ./setup.sh, which creates a private .env file with filled secrets and starts the application. Docker and Docker Compose are all you need on Linux, macOS or WSL, while working outside containers needs Go 1.27.1, Node.js 24 LTS and pnpm 10.32.1. Then visit http://localhost:3000 and sign in with the magic link from the local Mailpit inbox at http://localhost:8025.
How does the starter enforce multi-tenant access?
Organisation-scoped access is enforced by the Go API and by PostgreSQL row-level security, with authorization based on live Better Auth membership checks rather than a cached role. There are three explicit roles, Member, Manager and Admin, checked against the current membership, and the database uses separate roles for migrations, the application and authentication, created by a script that the migration service waits for.
Is billing included in the starter, and how do I turn it on?
Billing is optional and disabled by default, so the core sign-in and team management do not require it. To enable it you configure Polar, set BILLING_ENABLED=true, and the starter provides checkout, a customer portal and the current subscription status, with the environment file carrying the Polar environment, access token and product id.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/moasq-production-saas-starter)Was this useful?