Public
demo
CommitsREADME.mdAdded+94−0
content-collections.tsAdded+111−0
Initial commit
Minertti · Oct 23, 2026 · 732e949d109786055170bf44cf4dc21e79613a54
2 files changed
+205−0| @@ -0,0 +1,94 @@ | ||
| 1 | https://www.achromatic.dev/docs/starter-kits/pro-nextjs-drizzle | |
| 2 | npm install | |
| 3 | npm run db:migrate | |
| 4 | npm run dev | |
| 5 | ||
| 6 | # SaaS Template | |
| 7 | ||
| 8 | A production-ready SaaS boilerplate with authentication, billing, organizations and more. | |
| 9 | ||
| 10 | ## Tech Stack | |
| 11 | ||
| 12 | | Category | Technologies | | |
| 13 | | ----------------- | ------------------------------------------ | | |
| 14 | | **Framework** | Next.js 16.3, React 19, TypeScript 7 | | |
| 15 | | **Styling** | Tailwind CSS 4, Base UI, Lucide Icons | | |
| 16 | | **Data** | tRPC, React Query, Drizzle ORM, PostgreSQL | | |
| 17 | | **Auth** | Better Auth (email, Google OAuth, 2FA) | | |
| 18 | | **Billing** | Stripe (subscriptions, per-seat, one-time) | | |
| 19 | | **Email** | Resend, React Email | | |
| 20 | | **Storage** | S3-compatible (AWS S3 / Cloudflare R2) | | |
| 21 | | **Observability** | Sentry, Google Analytics 4 | | |
| 22 | ||
| 23 | ## Quick Start | |
| 24 | ||
| 25 | ```bash | |
| 26 | npm install | |
| 27 | cp .env.example .env | |
| 28 | # Edit .env: set DATABASE_URL and BETTER_AUTH_SECRET | |
| 29 | npm run db:migrate | |
| 30 | npm run dev | |
| 31 | ``` | |
| 32 | ||
| 33 | Open [http://localhost:3000](http://localhost:3000) | |
| 34 | ||
| 35 | **Full setup guide:** [README_QUICKSTART.md](./README_QUICKSTART.md) | |
| 36 | ||
| 37 | ## Documentation | |
| 38 | ||
| 39 | | Document | Description | | |
| 40 | | ---------------------------------------------------- | -------------------------------------------------------------- | | |
| 41 | | [README_QUICKSTART.md](./README_QUICKSTART.md) | Step-by-step setup guide | | |
| 42 | | [README_DATABASE.md](./README_DATABASE.md) | Drizzle ORM, schema, migrations | | |
| 43 | | [README_AUTH.md](./README_AUTH.md) | Authentication, roles, sessions, 2FA | | |
| 44 | | [README_TRPC.md](./README_TRPC.md) | tRPC API layer and procedures | | |
| 45 | | [README_BILLING.md](./README_BILLING.md) | Stripe billing, subscriptions, webhooks | | |
| 46 | | [README_STRIPE.md](./README_STRIPE.md) | Stripe local testing, production webhooks, and required events | | |
| 47 | | [README_EMAIL.md](./README_EMAIL.md) | Resend email system and templates | | |
| 48 | | [README_MCP.md](./README_MCP.md) | Local read-only MCP server | | |
| 49 | | [README_STORAGE.md](./README_STORAGE.md) | S3-compatible file storage | | |
| 50 | | [README_CONTENT.md](./README_CONTENT.md) | Blog, docs and marketing pages | | |
| 51 | | [README_AI.md](./README_AI.md) | AI chatbot system | | |
| 52 | | [README_OBSERVABILITY.md](./README_OBSERVABILITY.md) | Sentry, logging, analytics | | |
| 53 | | [README_DEPLOYMENT.md](./README_DEPLOYMENT.md) | Vercel deployment guide | | |
| 54 | | [AGENTS.md](./AGENTS.md) | AI agent guidelines (Cursor, Claude, Copilot) | | |
| 55 | | [.env.example](./.env.example) | All environment variables | | |
| 56 | | [LICENSE](./LICENSE) | Achromatic License | | |
| 57 | ||
| 58 | ## Commands | |
| 59 | ||
| 60 | | Command | Description | | |
| 61 | | ----------------------- | ------------------------------- | | |
| 62 | | `npm run dev` | Start development server | | |
| 63 | | `npm run build` | Build for production | | |
| 64 | | `npm run db:migrate` | Run database migrations | | |
| 65 | | `npm run db:studio` | Open Drizzle Studio (DB GUI) | | |
| 66 | | `npm run docker:up` | Start PostgreSQL | | |
| 67 | | `npm run stripe:listen` | Forward Stripe webhooks locally | | |
| 68 | | `npm run mcp:build` | Compile the local MCP server | | |
| 69 | | `npm run mcp:start` | Start the MCP server over stdio | | |
| 70 | | `npm run deps:check` | Check for dependency updates | | |
| 71 | | `npm run deps:update` | Update package.json versions | | |
| 72 | ||
| 73 | ## Configuration | |
| 74 | ||
| 75 | ### AI Credits | |
| 76 | ||
| 77 | Stripe products and prices are generated from the app's billing configuration. For Stripe keys, local webhook testing, and production setup, follow [README_STRIPE.md](./README_STRIPE.md). | |
| 78 | ||
| 79 | ## Project Structure | |
| 80 | ||
| 81 | ``` | |
| 82 | app/ | |
| 83 | ├── (marketing)/ # Public pages (landing, blog, docs) | |
| 84 | ├── (saas)/ # Protected app (dashboard, settings) | |
| 85 | └── api/ # API routes (auth, webhooks, tRPC) | |
| 86 | components/ # React components | |
| 87 | config/ # App, auth, billing configuration | |
| 88 | lib/ # Core libraries (auth, billing, db, email) | |
| 89 | trpc/ # tRPC API routers | |
| 90 | ``` | |
| 91 | ||
| 92 | ## License | |
| 93 | ||
| 94 | [Achromatic License](./LICENSE) | |
| @@ -0,0 +1,111 @@ | ||
| 1 | import { defineCollection, defineConfig } from "@content-collections/core"; | |
| 2 | import { compileMDX } from "@content-collections/mdx"; | |
| 3 | import { | |
| 4 | createDocSchema, | |
| 5 | createMetaSchema, | |
| 6 | transformMDX, | |
| 7 | } from "@fumadocs/content-collections/configuration"; | |
| 8 | import rehypeShiki from "@shikijs/rehype"; | |
| 9 | import { remarkImage } from "fumadocs-core/mdx-plugins"; | |
| 10 | import { z } from "zod/v4"; | |
| 11 | ||
| 12 | function sanitizePath(path: string) { | |
| 13 | return path | |
| 14 | .replace(/(\.[a-zA-Z-]{2,5})$/, "") | |
| 15 | .replace(/^\//, "") | |
| 16 | .replace(/\/$/, "") | |
| 17 | .replace(/index$/, ""); | |
| 18 | } | |
| 19 | ||
| 20 | // Blog posts collection | |
| 21 | const posts = defineCollection({ | |
| 22 | name: "posts", | |
| 23 | directory: "content/posts", | |
| 24 | include: "**/*.{mdx,md}", | |
| 25 | schema: z.object({ | |
| 26 | title: z.string(), | |
| 27 | date: z.string(), | |
| 28 | image: z.string().optional(), | |
| 29 | authorName: z.string(), | |
| 30 | authorImage: z.string().optional(), | |
| 31 | authorLink: z.string().optional(), | |
| 32 | excerpt: z.string().optional(), | |
| 33 | tags: z.array(z.string()), | |
| 34 | published: z.boolean(), | |
| 35 | content: z.string(), | |
| 36 | }), | |
| 37 | transform: async (document, context) => { | |
| 38 | const body = await compileMDX(context, document, { | |
| 39 | rehypePlugins: [ | |
| 40 | [ | |
| 41 | rehypeShiki, | |
| 42 | { | |
| 43 | theme: "nord", | |
| 44 | }, | |
| 45 | ], | |
| 46 | ], | |
| 47 | }); | |
| 48 | ||
| 49 | return { | |
| 50 | ...document, | |
| 51 | body, | |
| 52 | rawBody: document.content, | |
| 53 | path: sanitizePath(document._meta.path), | |
| 54 | }; | |
| 55 | }, | |
| 56 | }); | |
| 57 | ||
| 58 | // Legal pages collection (privacy, terms, etc.) | |
| 59 | const legalPages = defineCollection({ | |
| 60 | name: "legalPages", | |
| 61 | directory: "content/legal", | |
| 62 | include: "**/*.{mdx,md}", | |
| 63 | schema: z.object({ | |
| 64 | title: z.string(), | |
| 65 | content: z.string(), | |
| 66 | }), | |
| 67 | transform: async (document, context) => { | |
| 68 | const body = await compileMDX(context, document); | |
| 69 | ||
| 70 | return { | |
| 71 | ...document, | |
| 72 | body, | |
| 73 | path: sanitizePath(document._meta.path), | |
| 74 | }; | |
| 75 | }, | |
| 76 | }); | |
| 77 | ||
| 78 | // Documentation collection (using fumadocs) | |
| 79 | const docs = defineCollection({ | |
| 80 | name: "docs", | |
| 81 | directory: "content/docs", | |
| 82 | include: "**/*.mdx", | |
| 83 | schema: z.object({ | |
| 84 | ...createDocSchema(z), | |
| 85 | content: z.string(), | |
| 86 | }), | |
| 87 | transform: async (document, context) => | |
| 88 | transformMDX(document, context, { | |
| 89 | remarkPlugins: [ | |
| 90 | [ | |
| 91 | remarkImage, | |
| 92 | { | |
| 93 | publicDir: "public", | |
| 94 | }, | |
| 95 | ], | |
| 96 | ], | |
| 97 | }), | |
| 98 | }); | |
| 99 | ||
| 100 | // Documentation metadata collection | |
| 101 | const docsMeta = defineCollection({ | |
| 102 | name: "docsMeta", | |
| 103 | directory: "content/docs", | |
| 104 | include: "**/meta*.json", | |
| 105 | parser: "json", | |
| 106 | schema: z.object(createMetaSchema(z)), | |
| 107 | }); | |
| 108 | ||
| 109 | export default defineConfig({ | |
| 110 | content: [posts, legalPages, docs, docsMeta], | |
| 111 | }); | |