The free, open-source Next.js SaaS starter kit.
Auth, payments, admin and emails, wired up and production-ready. Ship your product this weekend.
Features · Tech stack · Quick start · Docs · Deploy · Structure · Roadmap
Most SaaS boilerplates either cost a few hundred dollars or ship as a barebones demo. OpenStarterKit is different:
- 💳 Payments included in the free tier: full Stripe Checkout, Customer Portal and webhooks, not paywalled behind a Pro plan.
- 🔓 It is plain Next.js: no compiler, no config DSL, no proprietary CLI. The App Router, Server Actions and Prisma behave exactly as their own docs say, with plain PostgreSQL underneath: host it anywhere, own your data.
- 🧩 A complete SaaS, not a starter demo: auth with two-factor and session management, billing, an admin panel with real MRR metrics, transactional emails and a polished landing page.
- 📖 MIT licensed: use it for anything, commercial included. No license keys, no unlock fees.
- 🌍 Ships in more than one language: every user-facing string lives in a message file, with routing, fallback and a language switch already wired. No translation service and no account required, and it is in the free kit instead of a paid add-on.
- 🕹️ A publishable demo, built in: one env var turns a deployment into a safe public demo of your product, with one-click explore accounts and writes disabled. Normally that is something you build yourself.
| Feature | What you get | |
|---|---|---|
| 🔐 | Authentication | Better Auth: Google + GitHub OAuth, magic link, email + password with reset, account linking, self-hosted two-factor (TOTP) with backup codes, and active session management |
| 💳 | Payments | Stripe Checkout, Customer Portal, signature-verified webhooks, subscriptions + one-time payments, free trials, promotion codes, optional Stripe Tax, PDF invoices, multiple tiers, usage-based example |
| 🛠️ | Admin panel | User management, search + pagination, live MRR metrics, promote users to admin |
| 📊 | User dashboard | Plan status, billing history, profile & settings |
| 📁 | Projects CRUD | A ready example resource with ownership checks to build on |
| 📧 | Transactional email | Resend-powered welcome, subscription, magic link, password reset and email-change confirmation messages |
| 🎨 | Design system | Custom Tailwind v4 UI (Button, Card, Badge, Input, Table), brand-neutral by default with one-file or env rebranding |
| 🌗 | Dark mode | System-aware theme with no flash of unstyled content |
| 🧱 | Landing page | Hero, Features, Pricing and FAQ sections ready to edit |
| 🕹️ | Demo mode | Ship a public demo of your product: DEMO_MODE="true" gives one-click explore accounts, disabled writes and checkout, and a seeded dataset you can reset |
| ✍️ | Blog & content | File-based MDX blog with categories and an RSS feed: write a Markdown file, commit, publish |
| 🔎 | SEO | Sitemap, robots, dynamic Open Graph images and Article JSON-LD out of the box |
| 🌍 | i18n | next-intl with a prefix only for non-default locales, per-key fallback to English, hreflang, and bilingual Markdown docs with a staleness check |
| 📨 | Waitlist & contact | Double opt-in newsletter waitlist (admin export, Resend sync) and a spam-protected contact form |
| 🤖 | AI-ready | Ships agent instructions for Claude Code, Cursor and Copilot (AGENTS.md) so your assistant is productive on day one |
| ✅ | CI + security | GitHub Actions pipeline (lint, tests, build), security headers, per-endpoint rate limiting (in-memory by default, two optional env vars move it to a shared store), an axe-core accessibility audit against WCAG 2.1 AA across the app, Dependabot with grouped updates |
- Framework: Next.js 16 with the App Router and Turbopack, on React 19 with Server Components rendering on the server by default. No compiler layer and no config DSL on top, so routing and Server Actions behave exactly as their own docs say.
- Language: TypeScript in strict mode, across the app, the tests and the environment schema.
- Styling: Tailwind CSS v4 with dark mode wired through CSS variables, so changing the theme is one file instead of a sweep through components.
- UI: Radix UI primitives under a design system written for this kit (Button, Card, Badge, Input, Table and the rest). Radix handles the accessible behavior, you own the look, and both live in your codebase with no component library to upgrade around.
- Icons: Lucide, an open source set shipped as React components, so an icon is something you import and not an asset you manage.
- Notifications: Sonner for toasts, wired to every success and failure the interface reports back, Server Actions included.
- Auth: Better Auth with Google and GitHub OAuth, magic link, email and password, and two-factor authentication you host yourself. Sessions live in your own database, so there is no auth vendor and no per-user pricing as you grow.
- Database: Prisma 7 on PostgreSQL. Plain Postgres underneath keeps your data portable, and Neon has a free managed tier if you would rather not run one yourself.
- Payments: Stripe with Checkout, the Customer Portal and signature-verified webhooks, in the free kit instead of behind a Pro plan.
- Emails: Resend for welcome, subscription, magic link and password reset messages, with the templates in your repository rather than in a dashboard you do not own.
- Internationalization: next-intl with plain JSON message files, plus routing, fallback and a language switch already wired.
- Validation: Zod on forms, Server Actions and the environment schema, with the same definitions shared by client and server.
- Content: the blog and docs are Markdown files with frontmatter, rendered through MDX with GitHub Flavored Markdown, so publishing a post means adding a file to the repository.
- Analytics: Vercel Analytics, mounted in the root layout and active on Vercel deployments. Delete the component if you would rather collect nothing.
- Tests: Vitest on the pure logic, the health endpoint and the Stripe webhook, and Playwright end to end on signup and checkout.
- CI: GitHub Actions running lint, tests, build and
npm auditon every push, so a broken or vulnerable commit fails before it reaches you.
/ → Landing page (Hero, Features, Pricing, FAQ)
/pricing → Dedicated pricing page
/login → Sign in (OAuth, magic link, email + password)
/signup → Create an account
/forgot-password → Password reset request (+ /reset-password)
/2fa → Second factor, when the account has one
/dashboard → User overview + plan status
/dashboard/billing → Subscription management + invoice history
/dashboard/settings → Profile, sign-in methods, password, two-factor & active sessions
/admin → Admin panel (ADMIN role required)
📚 Prefer step-by-step guides? The full documentation lives in docs/ and is rendered at openstarterkit.dev/docs: getting started, configuration, authentication, deployment.
You need: Node 24 or newer, git, and a PostgreSQL database (Neon has a free tier).
git clone https://github.com/openstarterkit/nextjs-saas-starter-kit.git
cd nextjs-saas-starter-kit
npm installnpm run devOpen http://localhost:3000. There is no database yet, so the page lists the steps left and marks the one you are on. It appears in development only, until the database is ready.
Create a PostgreSQL database and copy the connection string it gives you. On Neon that is the pooled one, and it ends with ?sslmode=require: change that to ?sslmode=verify-full, the stricter mode, or Node prints an SSL warning at every start.
cp .env.example .env.local # Command Prompt on Windows: copy .env.example .env.localTwo values are enough for the first run:
DATABASE_URL="postgresql://..." # the connection string from your database
AUTH_SECRET="..." # any random value; generate one with the command belownode -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"Paste what it prints between the quotes of AUTH_SECRET.
Leave DIRECT_URL as it is: the kit does not read it, and it ships in the example only because some hosts set it for you.
Everything else (sign-in providers, Stripe, email) can wait: each feature turns on when its variables are set, and stays quietly off when they are not. Every variable is listed with its comment in .env.example and explained in Configuration.
npx prisma migrate deploy # applies the committed migrations
npx prisma db seed # six example plans, one inactive (edit prisma/seed.ts later)Both read .env.local, the same file the app reads.
Restart npm run dev and open the site again. Click Sign in: in development the login page has a Dev Login (Admin) button, which needs no OAuth app and lands you in the dashboard as an admin. 🎉
Next: sign-in providers, Stripe and email each have their setup in Configuration, and the same five steps with more detail are in Getting started.
Your brand lives in two files. Swap them and the whole app follows:
src/config/site.ts # name, tagline, description, contact email, links
src/components/logo.tsx # logo mark + wordmark
Then rewrite the landing copy and the /privacy + /terms placeholders, and it's your product. Details in Make it yours.
Click the Deploy with Vercel button at the top, or:
npm i -g vercel
vercelVercel's free Hobby plan is for non-commercial use: if you sell with the app, budget for Vercel Pro or deploy on any Node.js host (Deployment).
Set all environment variables in the Vercel dashboard. For Stripe webhooks in production, create a new endpoint in the Stripe dashboard pointing to https://yourdomain.com/api/webhooks/stripe. Builds do not run migrations: apply them to the production database yourself, with npm run check:deploy before and after (see Deployment).
After signing in for the first time, run:
npx prisma studioFind your user in the User table and set role to ADMIN. You'll then see the Admin Panel shortcut in your dashboard.
The kit ships brand-neutral: a placeholder name and a black + grayscale theme, a blank canvas you make yours. Everything (metadata, navbar, footer, emails, legal pages, Open Graph images) reads from a single source, so you can rebrand two ways:
- From config:
src/config/site.ts(name, tagline, description, links),src/components/logo.tsx(the logo mark), and the color tokens insrc/app/globals.css. - From env: set the
NEXT_PUBLIC_BRAND_*variables (name, tagline, logo accent, colors) and the app rebrands with no code changes. The gradient, glow and Open Graph images follow your accent automatically. See Configuration.
Change those, replace the landing copy, and the kit is fully yours. The only intentional exception is the optional "Built with" badge, which credits the kit itself. The code is MIT: use it for anything, no strings attached.
Want a public demo of your product (like the kit's own demo) without collecting anyone's personal data? Deploy a second instance of this repo against an isolated database and set:
DEMO_MODE="true" # on the demo deployment
NEXT_PUBLIC_DEMO_URL="" # on the marketing deployment → points "Sign in"/"Demo" at the demoWith DEMO_MODE on, the OAuth buttons are disabled and the login page offers one-click shared accounts instead: Explore as User and Explore as Admin (so visitors can see the admin panel too). Fill the demo database with believable fake users, subscriptions and projects:
npm run db:seed:demo # ⚠️ wipes users/projects and recreates fixtures; re-run to resetNo real emails, no real payments (use Stripe test keys), nothing to GDPR-worry about.
src/
├── app/
│ ├── (public)/ # Landing pages (no auth required)
│ ├── (auth)/ # Login, signup, magic link, password reset & second factor
│ ├── (dashboard)/ # Protected user area
│ ├── (admin)/ # Admin panel (ADMIN role)
│ └── api/ # API routes (auth, checkout, billing, webhooks)
├── components/
│ ├── ui/ # Design system (Button, Card, Badge, Input, Table)
│ ├── landing/ # Landing page sections
│ ├── billing/ # Stripe billing components
│ ├── dashboard/ # Dashboard-specific components
│ ├── settings/ # Two-factor card, active sessions, sign-in methods
│ └── admin/ # Admin components
├── lib/
│ ├── prisma.ts # Prisma client (Prisma 7 + adapter)
│ ├── stripe.ts # Stripe client (lazy proxy)
│ ├── email.ts # Resend email helpers
│ └── utils.ts # cn() utility
├── app/actions/ # Server actions (profile, admin)
└── auth.ts # Better Auth configuration
prisma/
├── schema.prisma # User, Account, Session, TwoFactor, Plan, Subscription
└── seed.ts # Seeds example plans (replace with your own)
npm test # run once
npm run test:watch # re-run on change
npm run test:coverage # with a coverage reportVitest, on the pure logic in src/lib: the rate limiter and its shared store, the password policy, the backup code alphabet, the account linking rule that decides when a provider may attach itself, the billing rules behind free trials, discounts and the Stripe Tax check, the session rules behind role changes and revocation, the Markdown parsers behind the blog, docs and changelog, the environment schema, the builders behind structured data and page metadata, and the boundary every page reads the session through. Plus the two routes worth guarding: the health endpoint, and the Stripe webhook, tested with real signatures generated by Stripe's own SDK, so a forged or replayed request is proven never to reach the database writes behind it.
npx playwright install chromium # once, about 115 MB
npm run test:e2ePlaywright on the flows that cost something when they break:
- Signing up and signing in: account created, session issued, dashboard reached, duplicate email refused.
- Starting a checkout: billing page reachable, anonymous requests bounced, handoff to Stripe returns a real Stripe URL.
- The whole two-factor chain: enable it, sign out, sign back in with a code, then with a backup code. The test computes the TOTP itself, so it runs with nobody in front of it.
- Accessibility: axe-core against WCAG 2.1 AA on the public pages, an error state and the signed-in pages, each measured at its full height so contrast below the first screen is judged too, failing on serious and critical findings.
Deliberately not part of npm test or CI: these need a database and a browser binary, and a clone should not have to install either to run the unit suite. Chromium only, because three engines mean three downloads and three times the flakes.
Nothing here sends email or takes a payment. The test server runs with the Resend key blanked, so registering signs the user straight in instead of mailing a magic link, and the checkout test stops at the handoff rather than driving Stripe's hosted page. Accounts created by the run are deleted afterwards.
Coverage of src/lib, excluding the thin wrappers around Prisma, Stripe and Resend:
| Lines | 79% |
| Statements | 77% |
| Functions | 82% |
| Branches | 71% |
Run npm run test:coverage to check those numbers yourself: they are printed by the command, not published to a badge service.
Why no coverage badge? Because it would travel with the kit and cost you something. A badge points at one specific repository, so every clone would show our coverage as if it were yours. Publishing it needs a third-party account, a token in your CI, and an uploader that runs where your secrets live. None of that belongs in a starter you are about to make your own: the command above reports the real figure for your code, and nothing leaves your machine.
Want one anyway? Perfectly reasonable, and common on open source projects. Point Codecov, Coveralls or whichever tool you prefer at your repository, then add its upload step to .github/workflows/ci.yml after npm test. Both services are free for public repositories. The workflow is yours to edit, and leaving it out of the kit is what makes adding it a decision rather than a leftover.
Need coverage enforced rather than reported? Thresholds that fail a build, history over time, and a report someone can hand to a compliance review are an organisation's problem, not a solo maker's. That is the kind of requirement the paid Pro tier is aimed at. Everything above stays in the free kit.
What is fully covered: the changelog parser, the environment schema, the password policy, the auth boundary, and the builders behind structured data and page metadata. What is not: the functions that read the database or the filesystem (getEntitlement, getAllPosts). Those need integration tests against a real client, and a unit test that mocks Prisma or Stripe only proves the mock works. Where a mocked test still earns its place is the decision taken around a call rather than the call itself: recordUsage is covered for the guards that drop an event in a keyless install and for the payload it builds, not for what Stripe does with it.
Your environment is validated at boot (src/instrumentation.ts): a configuration that is half done, such as a Stripe key with no webhook secret, stops the server with an explanatory message instead of failing at the first payment.
npm run smoke -- https://your-app.com
npm run smoke -- https://your-app.com --expect-version 2.3.6Checks a running deployment from the outside: /api/health (including whether the database has every migration the build ships), home, blog index, feed, a real post, the SEO surfaces, and two routing guards (an unknown path is a 404, /dashboard redirects to sign in). With --expect-version it also compares the version the site reports with the one you are releasing, which is how you catch a deploy that succeeded while still serving the previous build.
Every request is a GET, so it is safe to run against production. The flows that write something (contact form, waitlist) send real email and stay on the manual checklist. Demo deployments are detected from /api/health and checked the other way around: the smoke verifies they are not indexable.
Dependabot is configured in .github/dependabot.yml and runs weekly, on Monday.
Patch and minor bumps are grouped by area (Next, React, Prisma, UI, everything else) so they arrive as a handful of pull requests instead of a dozen. Major bumps come one at a time, because those are the ones worth reading. Majors of next, react and react-dom are excluded on purpose: those are release work, planned on the roadmap, not maintenance to merge on a Monday morning.
GitHub Actions versions are checked monthly.
CI runs lint, unit tests, the build and npm audit --omit=dev --audit-level=high on every push and pull request to main. The audit step is a gate, not a report: a high advisory in a runtime dependency stops the pipeline.
Why Prisma 7? New WASM engine requires a driver adapter; we use @prisma/adapter-pg. See src/lib/prisma.ts.
Why Stripe lazy proxy? new Stripe("") throws at module load time. The proxy defers instantiation to first request. See src/lib/stripe.ts.
Why Better Auth? Auth.js is now part of Better Auth, and its own README points new projects at it. Sessions are rows in your database, so revoking one is deleting it, and the plugin system covers what a starter needs without a vendor.
Why Tailwind v4? Native CSS variables, no config file needed, @custom-variant for dark mode.
OpenStarterKit ships with a small "Built with OpenStarterKit" badge in the app footer. It's on by default: it costs you nothing and helps other makers find the kit.
Want to remove it? You're completely free to. Just set:
NEXT_PUBLIC_REMOVE_BRANDING="true"No license to buy, no fee, no unlock: the badge simply disappears.
If the kit saved you a weekend, the nicest way to say thanks is a coffee. Totally optional: ☕ buy us a coffee.
OpenStarterKit ships continuously and stays free: pull main for updates. A paid Pro tier (teams & scale) is coming later.
- 📍 ROADMAP.md: what's next
- 📝 CHANGELOG.md: what shipped
Every release is tagged, so Watch → Custom → Releases on the repository tells you when one lands without subscribing to every commit and issue.
From v1.6 every user facing string lives in a message file, not inside a component, with one exception on purpose: the placeholder privacy, terms and cookies pages, which you replace with text of your own before launch and which should not be assembled from a file that quietly falls back to another language. Everything else means filling one JSON file instead of hunting through a hundred components, and needing two languages at once means the routing, the fallback and the language switch are already there rather than a refactor you pay for later.
Built on next-intl, with no translation service and no account required: the files are plain JSON in the standard layout, so running them through Crowdin or anything else is your call. Your Markdown docs translate too, one file at a time: see docs/i18n.md.
On openstarterkit.dev the language switch is real: every page of the documentation is there in Italian, and each translation records a checksum of the English file it was written from, so npm run check:translations says which ones went stale instead of leaving you to compare by hand, in your repository exactly as in ours. The blog stays English on purpose. Anything without a translation, page or message key, falls back to English with a line saying so rather than a gap, which is the behaviour you inherit and can see working on us instead of reading a description of it.
Found a vulnerability in the kit? Please don't open a public issue: report it privately through our security policy.
Once this is your app, write your own policy: add a SECURITY.md to your repository, or one in a .github repository if you keep several projects under an organisation.
On dependencies, the kit is kept clear of every advisory that has a fix, and Dependabot opens the updates weekly. What can still show up in npm audit are advisories inside Next.js's own dependency tree: those clear when Next ships a release, not when you run npm audit fix, and the "fix" npm suggests for them is a downgrade to an ancient major that you do not want. Run npm audit and read what it points at before assuming your clone is exposed.
The demo blog posts, the docs and the placeholder marketing copy that ship with this kit were produced with the help of AI tools. They are placeholders: replace them with your own before you launch.
The blog covers in content/blog/covers/ are hand written SVG. They carry two color placeholders that the kit fills in with your brand, so you can keep the artwork and change the palette without redrawing anything. See docs/blog.md for how they work, and for one thing worth knowing before you swap them for generated images.
The logo mark is a lucide icon (ISC licence) rendered as code next to your product name, so there is no image file to swap. Your app ships the Hexagon as a placeholder: change it in src/components/logo.tsx, and change src/app/icon.tsx in the same pass, because the favicon is generated as the miniature of that same mark.
MIT, free for personal and commercial use.
Built with ❤️ by OpenStarterKit
If this saved you time, a ⭐ on the repo means a lot.

