create-skit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +36 -0
  2. package/bin/create-skit.mjs +1064 -0
  3. package/lib/module-application.mjs +281 -0
  4. package/lib/module-resolver.mjs +179 -0
  5. package/modules/README.md +22 -0
  6. package/modules/ai-dx/files/AGENTS.md +116 -0
  7. package/modules/ai-dx/files/ARCHITECTURE.md +103 -0
  8. package/modules/ai-dx/module.json +14 -0
  9. package/modules/ai-dx-claude/files/CLAUDE.md +8 -0
  10. package/modules/ai-dx-claude/module.json +13 -0
  11. package/modules/ai-dx-cursor/files/.cursor/rules/auth.mdc +53 -0
  12. package/modules/ai-dx-cursor/files/.cursor/rules/database.mdc +48 -0
  13. package/modules/ai-dx-cursor/files/.cursor/rules/env.mdc +43 -0
  14. package/modules/ai-dx-cursor/files/.cursor/rules/nextjs.mdc +58 -0
  15. package/modules/ai-dx-cursor/files/.cursor/rules/project.mdc +33 -0
  16. package/modules/ai-dx-cursor/files/.cursor/rules/testing.mdc +55 -0
  17. package/modules/ai-dx-cursor/module.json +18 -0
  18. package/modules/ai-dx-gemini/files/.gemini/GEMINI.md +5 -0
  19. package/modules/ai-dx-gemini/module.json +13 -0
  20. package/modules/auth-core/module.json +8 -0
  21. package/modules/auth-github/module.json +20 -0
  22. package/modules/billing-polar/module.json +20 -0
  23. package/modules/billing-stripe/module.json +23 -0
  24. package/modules/dashboard-shell/files/src/app/globals.css +756 -0
  25. package/modules/dashboard-shell/files/src/app/settings/page.tsx +67 -0
  26. package/modules/dashboard-shell/module.json +11 -0
  27. package/modules/db-pg/module.json +21 -0
  28. package/modules/db-postgresjs/module.json +21 -0
  29. package/modules/deploy-docker/files/.dockerignore +19 -0
  30. package/modules/deploy-docker/files/Dockerfile +25 -0
  31. package/modules/deploy-docker/module.json +11 -0
  32. package/modules/email-resend/module.json +21 -0
  33. package/modules/quality-baseline/module.json +8 -0
  34. package/modules/testing-baseline/module.json +8 -0
  35. package/package.json +40 -0
  36. package/presets/README.md +12 -0
  37. package/presets/blank.json +67 -0
  38. package/presets/dashboard.json +67 -0
  39. package/templates/base-web/.env.example +17 -0
  40. package/templates/base-web/.github/workflows/ci.yml +34 -0
  41. package/templates/base-web/.husky/pre-commit +3 -0
  42. package/templates/base-web/.husky/pre-push +3 -0
  43. package/templates/base-web/.prettierignore +3 -0
  44. package/templates/base-web/README.md +42 -0
  45. package/templates/base-web/drizzle.config.ts +16 -0
  46. package/templates/base-web/eslint.config.mjs +127 -0
  47. package/templates/base-web/manifest.json +5 -0
  48. package/templates/base-web/next-env.d.ts +4 -0
  49. package/templates/base-web/next.config.ts +5 -0
  50. package/templates/base-web/package.json +75 -0
  51. package/templates/base-web/playwright.config.ts +21 -0
  52. package/templates/base-web/prettier.config.mjs +9 -0
  53. package/templates/base-web/proxy.ts +23 -0
  54. package/templates/base-web/src/app/api/auth/[...all]/route.ts +5 -0
  55. package/templates/base-web/src/app/api/billing/checkout/route.ts +26 -0
  56. package/templates/base-web/src/app/api/billing/portal/route.ts +25 -0
  57. package/templates/base-web/src/app/api/email/test/route.ts +28 -0
  58. package/templates/base-web/src/app/api/webhooks/polar/route.ts +5 -0
  59. package/templates/base-web/src/app/api/webhooks/stripe/route.ts +5 -0
  60. package/templates/base-web/src/app/billing/page.tsx +55 -0
  61. package/templates/base-web/src/app/dashboard/page.tsx +15 -0
  62. package/templates/base-web/src/app/email/page.tsx +46 -0
  63. package/templates/base-web/src/app/error.tsx +27 -0
  64. package/templates/base-web/src/app/globals.css +534 -0
  65. package/templates/base-web/src/app/layout.tsx +19 -0
  66. package/templates/base-web/src/app/llms-full.txt/route.ts +158 -0
  67. package/templates/base-web/src/app/llms.txt/route.ts +59 -0
  68. package/templates/base-web/src/app/loading.tsx +24 -0
  69. package/templates/base-web/src/app/not-found.tsx +16 -0
  70. package/templates/base-web/src/app/page.tsx +5 -0
  71. package/templates/base-web/src/app/sign-in/page.tsx +14 -0
  72. package/templates/base-web/src/app/sign-up/page.tsx +14 -0
  73. package/templates/base-web/src/components/auth/email-auth-form.test.tsx +40 -0
  74. package/templates/base-web/src/components/auth/email-auth-form.tsx +128 -0
  75. package/templates/base-web/src/components/auth/sign-out-button.tsx +29 -0
  76. package/templates/base-web/src/db/index.ts +16 -0
  77. package/templates/base-web/src/db/schema/auth.ts +4 -0
  78. package/templates/base-web/src/db/schema/index.ts +2 -0
  79. package/templates/base-web/src/db/schema/projects.ts +17 -0
  80. package/templates/base-web/src/db/seeds/index.ts +32 -0
  81. package/templates/base-web/src/lib/auth-client.ts +5 -0
  82. package/templates/base-web/src/lib/auth-session.ts +21 -0
  83. package/templates/base-web/src/lib/auth.ts +23 -0
  84. package/templates/base-web/src/lib/billing/index.ts +37 -0
  85. package/templates/base-web/src/lib/billing/providers/polar.ts +80 -0
  86. package/templates/base-web/src/lib/billing/providers/stripe.ts +77 -0
  87. package/templates/base-web/src/lib/billing/types.ts +25 -0
  88. package/templates/base-web/src/lib/email/index.ts +19 -0
  89. package/templates/base-web/src/lib/email/templates.test.ts +12 -0
  90. package/templates/base-web/src/lib/email/templates.ts +40 -0
  91. package/templates/base-web/src/lib/env.ts +83 -0
  92. package/templates/base-web/tests/e2e/home.spec.ts +8 -0
  93. package/templates/base-web/tsconfig.json +34 -0
  94. package/templates/base-web/vitest.config.ts +19 -0
  95. package/templates/blank/.env.example +16 -0
  96. package/templates/blank/.github/workflows/ci.yml +34 -0
  97. package/templates/blank/.husky/pre-commit +3 -0
  98. package/templates/blank/.husky/pre-push +3 -0
  99. package/templates/blank/.prettierignore +3 -0
  100. package/templates/blank/drizzle.config.ts +16 -0
  101. package/templates/blank/eslint.config.mjs +127 -0
  102. package/templates/blank/next-env.d.ts +4 -0
  103. package/templates/blank/next.config.ts +5 -0
  104. package/templates/blank/package.json +75 -0
  105. package/templates/blank/playwright.config.ts +21 -0
  106. package/templates/blank/prettier.config.mjs +9 -0
  107. package/templates/blank/proxy.ts +28 -0
  108. package/templates/blank/src/app/api/auth/[...all]/route.ts +5 -0
  109. package/templates/blank/src/app/api/billing/checkout/route.ts +26 -0
  110. package/templates/blank/src/app/api/billing/portal/route.ts +25 -0
  111. package/templates/blank/src/app/api/email/test/route.ts +28 -0
  112. package/templates/blank/src/app/api/webhooks/polar/route.ts +5 -0
  113. package/templates/blank/src/app/api/webhooks/stripe/route.ts +5 -0
  114. package/templates/blank/src/app/billing/page.tsx +70 -0
  115. package/templates/blank/src/app/email/page.tsx +46 -0
  116. package/templates/blank/src/app/globals.css +394 -0
  117. package/templates/blank/src/app/layout.tsx +19 -0
  118. package/templates/blank/src/app/page.tsx +23 -0
  119. package/templates/blank/src/app/sign-in/page.tsx +18 -0
  120. package/templates/blank/src/app/sign-up/page.tsx +18 -0
  121. package/templates/blank/src/components/auth/email-auth-form.test.tsx +39 -0
  122. package/templates/blank/src/components/auth/email-auth-form.tsx +109 -0
  123. package/templates/blank/src/components/auth/sign-out-button.tsx +29 -0
  124. package/templates/blank/src/db/index.ts +16 -0
  125. package/templates/blank/src/db/schema/auth.ts +4 -0
  126. package/templates/blank/src/db/schema/index.ts +2 -0
  127. package/templates/blank/src/db/schema/projects.ts +17 -0
  128. package/templates/blank/src/db/seeds/index.ts +28 -0
  129. package/templates/blank/src/lib/auth-client.ts +5 -0
  130. package/templates/blank/src/lib/auth-session.ts +11 -0
  131. package/templates/blank/src/lib/auth.ts +23 -0
  132. package/templates/blank/src/lib/billing/index.ts +37 -0
  133. package/templates/blank/src/lib/billing/providers/polar.ts +80 -0
  134. package/templates/blank/src/lib/billing/providers/stripe.ts +77 -0
  135. package/templates/blank/src/lib/billing/types.ts +25 -0
  136. package/templates/blank/src/lib/email/index.ts +19 -0
  137. package/templates/blank/src/lib/email/templates.test.ts +15 -0
  138. package/templates/blank/src/lib/email/templates.ts +40 -0
  139. package/templates/blank/src/lib/env.ts +80 -0
  140. package/templates/blank/tsconfig.json +34 -0
  141. package/templates/blank/vitest.config.ts +19 -0
  142. package/templates/dashboard/.env.example +16 -0
  143. package/templates/dashboard/.github/workflows/ci.yml +34 -0
  144. package/templates/dashboard/.husky/pre-commit +3 -0
  145. package/templates/dashboard/.husky/pre-push +3 -0
  146. package/templates/dashboard/.prettierignore +3 -0
  147. package/templates/dashboard/drizzle.config.ts +16 -0
  148. package/templates/dashboard/eslint.config.mjs +127 -0
  149. package/templates/dashboard/next-env.d.ts +4 -0
  150. package/templates/dashboard/next.config.ts +5 -0
  151. package/templates/dashboard/package.json +75 -0
  152. package/templates/dashboard/playwright.config.ts +21 -0
  153. package/templates/dashboard/prettier.config.mjs +9 -0
  154. package/templates/dashboard/proxy.ts +36 -0
  155. package/templates/dashboard/src/app/api/auth/[...all]/route.ts +5 -0
  156. package/templates/dashboard/src/app/api/billing/checkout/route.ts +26 -0
  157. package/templates/dashboard/src/app/api/billing/portal/route.ts +25 -0
  158. package/templates/dashboard/src/app/api/email/test/route.ts +28 -0
  159. package/templates/dashboard/src/app/api/webhooks/polar/route.ts +5 -0
  160. package/templates/dashboard/src/app/api/webhooks/stripe/route.ts +5 -0
  161. package/templates/dashboard/src/app/billing/layout.tsx +22 -0
  162. package/templates/dashboard/src/app/billing/page.tsx +73 -0
  163. package/templates/dashboard/src/app/dashboard/layout.tsx +22 -0
  164. package/templates/dashboard/src/app/dashboard/page.tsx +104 -0
  165. package/templates/dashboard/src/app/email/layout.tsx +22 -0
  166. package/templates/dashboard/src/app/email/page.tsx +54 -0
  167. package/templates/dashboard/src/app/globals.css +1357 -0
  168. package/templates/dashboard/src/app/layout.tsx +25 -0
  169. package/templates/dashboard/src/app/page.tsx +154 -0
  170. package/templates/dashboard/src/app/settings/layout.tsx +22 -0
  171. package/templates/dashboard/src/app/settings/page.tsx +85 -0
  172. package/templates/dashboard/src/app/sign-in/page.tsx +47 -0
  173. package/templates/dashboard/src/app/sign-up/page.tsx +47 -0
  174. package/templates/dashboard/src/components/auth/email-auth-form.test.tsx +39 -0
  175. package/templates/dashboard/src/components/auth/email-auth-form.tsx +160 -0
  176. package/templates/dashboard/src/components/auth/sign-out-button.tsx +29 -0
  177. package/templates/dashboard/src/components/dashboard/shell.tsx +158 -0
  178. package/templates/dashboard/src/db/index.ts +16 -0
  179. package/templates/dashboard/src/db/schema/auth.ts +4 -0
  180. package/templates/dashboard/src/db/schema/index.ts +2 -0
  181. package/templates/dashboard/src/db/schema/projects.ts +17 -0
  182. package/templates/dashboard/src/db/seeds/index.ts +28 -0
  183. package/templates/dashboard/src/lib/auth-client.ts +5 -0
  184. package/templates/dashboard/src/lib/auth-session.ts +11 -0
  185. package/templates/dashboard/src/lib/auth.ts +41 -0
  186. package/templates/dashboard/src/lib/billing/index.ts +37 -0
  187. package/templates/dashboard/src/lib/billing/providers/polar.ts +80 -0
  188. package/templates/dashboard/src/lib/billing/providers/stripe.ts +77 -0
  189. package/templates/dashboard/src/lib/billing/types.ts +25 -0
  190. package/templates/dashboard/src/lib/email/index.ts +19 -0
  191. package/templates/dashboard/src/lib/email/templates.test.ts +15 -0
  192. package/templates/dashboard/src/lib/email/templates.ts +40 -0
  193. package/templates/dashboard/src/lib/env.ts +88 -0
  194. package/templates/dashboard/tsconfig.json +34 -0
  195. package/templates/dashboard/vitest.config.ts +19 -0
@@ -0,0 +1,59 @@
1
+ export function GET() {
2
+ const content = `# __PROJECT_NAME__
3
+
4
+ > Built with Skit — opinionated Next.js 16 SaaS starter.
5
+
6
+ ## Tech Stack
7
+
8
+ - Next.js 16 (App Router, React 19, TypeScript 5.9)
9
+ - Drizzle ORM + PostgreSQL
10
+ - Better Auth (email/password__AUTH_LLMS_EXTRA__)
11
+ - Tailwind CSS 4
12
+ - __BILLING_LLMS_LINE__
13
+ - __EMAIL_LLMS_LINE__
14
+ - Vitest + Playwright testing
15
+ - ESLint 9, Prettier
16
+
17
+ ## Structure
18
+
19
+ - src/app/ — Next.js pages (App Router)
20
+ - src/lib/ — shared utilities (auth, db, billing, email, env)
21
+ - src/db/ — Drizzle schema and seeds
22
+ - AGENTS.md — AI agent rules
23
+ - ARCHITECTURE.md — project architecture
24
+
25
+ ## Key Files
26
+
27
+ | File | Purpose |
28
+ |---|---|
29
+ | src/lib/auth.ts | Better Auth server config |
30
+ | src/lib/db.ts | Drizzle client |
31
+ | src/lib/billing.ts | Billing provider |
32
+ | src/lib/email.ts | Email client |
33
+ | src/lib/env.ts | Environment validation |
34
+ | src/middleware.ts | Route protection |
35
+ | drizzle.config.ts | Drizzle Kit config |
36
+
37
+ ## Commands
38
+
39
+ | Command | Description |
40
+ |---|---|
41
+ | pnpm dev | Start dev server |
42
+ | pnpm build | Production build |
43
+ | pnpm lint | Lint with ESLint |
44
+ | pnpm typecheck | TypeScript check |
45
+ | pnpm test | Unit tests (Vitest) |
46
+ | pnpm test:e2e | E2E tests (Playwright) |
47
+ | pnpm db:push | Push schema to database |
48
+ | pnpm db:seed | Seed demo data |
49
+ | pnpm db:studio | Open Drizzle Studio |
50
+
51
+ ## Full Documentation
52
+
53
+ See /llms-full.txt for the complete project context.
54
+ `;
55
+
56
+ return new Response(content, {
57
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
58
+ });
59
+ }
@@ -0,0 +1,24 @@
1
+ export default function Loading() {
2
+ return (
3
+ <main
4
+ style={{
5
+ display: "grid",
6
+ placeItems: "center",
7
+ minHeight: "100dvh",
8
+ background: "var(--bg, #09090b)"
9
+ }}
10
+ >
11
+ <div
12
+ style={{
13
+ width: 32,
14
+ height: 32,
15
+ border: "3px solid var(--border, rgba(255,255,255,0.06))",
16
+ borderTopColor: "var(--accent, #00d4aa)",
17
+ borderRadius: "50%",
18
+ animation: "spin 0.6s linear infinite"
19
+ }}
20
+ />
21
+ <style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
22
+ </main>
23
+ );
24
+ }
@@ -0,0 +1,16 @@
1
+ import Link from "next/link";
2
+
3
+ export default function NotFound() {
4
+ return (
5
+ <main className="shell">
6
+ <section className="hero">
7
+ <p className="eyebrow">404</p>
8
+ <h1>Page not found</h1>
9
+ <p className="lede">The page you are looking for does not exist or has been moved.</p>
10
+ <div className="hero-actions">
11
+ <Link href="/">Go home</Link>
12
+ </div>
13
+ </section>
14
+ </main>
15
+ );
16
+ }
@@ -0,0 +1,5 @@
1
+ import Link from "next/link";
2
+
3
+ export default function HomePage() {
4
+ return __HOME_PAGE_CONTENT__;
5
+ }
@@ -0,0 +1,14 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { EmailAuthForm } from "@/components/auth/email-auth-form";
4
+ import { getSession } from "@/lib/auth-session";
5
+
6
+ export default async function SignInPage() {
7
+ const session = await getSession();
8
+
9
+ if (session) {
10
+ redirect("/dashboard");
11
+ }
12
+
13
+ return <EmailAuthForm mode="sign-in" />;
14
+ }
@@ -0,0 +1,14 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { EmailAuthForm } from "@/components/auth/email-auth-form";
4
+ import { getSession } from "@/lib/auth-session";
5
+
6
+ export default async function SignUpPage() {
7
+ const session = await getSession();
8
+
9
+ if (session) {
10
+ redirect("/dashboard");
11
+ }
12
+
13
+ return <EmailAuthForm mode="sign-up" />;
14
+ }
@@ -0,0 +1,40 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { EmailAuthForm } from "@/components/auth/email-auth-form";
5
+
6
+ import type { ReactNode } from "react";
7
+
8
+ vi.mock("next/link", () => ({
9
+ default: ({ children, href }: { children: ReactNode; href: string }) => (
10
+ <a href={href}>{children}</a>
11
+ )
12
+ }));
13
+
14
+ vi.mock("next/navigation", () => ({
15
+ useRouter: () => ({
16
+ push: vi.fn(),
17
+ refresh: vi.fn()
18
+ })
19
+ }));
20
+
21
+ vi.mock("@/lib/auth-client", () => ({
22
+ authClient: {
23
+ signIn: {
24
+ email: vi.fn()
25
+ },
26
+ signUp: {
27
+ email: vi.fn()
28
+ }
29
+ }
30
+ }));
31
+
32
+ describe("EmailAuthForm", () => {
33
+ it("renders preset-specific auth form copy", () => {
34
+ render(<EmailAuthForm mode="__AUTH_FORM_TEST_MODE__" />);
35
+
36
+ expect(screen.getByRole("heading", { name: /__AUTH_FORM_TEST_HEADING__/i })).toBeTruthy();
37
+ expect(screen.getByLabelText(/__AUTH_FORM_TEST_LABEL__/i)).toBeTruthy();
38
+ __AUTH_SOCIAL_TEST_ASSERTION__
39
+ });
40
+ });
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useRouter } from "next/navigation";
5
+ import { useState } from "react";
6
+
7
+ import { authClient } from "@/lib/auth-client";
8
+
9
+ type EmailAuthFormProps = {
10
+ mode: "sign-in" | "sign-up";
11
+ };
12
+
13
+ export function EmailAuthForm({ mode }: EmailAuthFormProps) {
14
+ const isSignUp = mode === "sign-up";
15
+ const router = useRouter();
16
+ const [error, setError] = useState<string | null>(null);
17
+ const [isPending, setIsPending] = useState(false);
18
+ const [showPassword, setShowPassword] = useState(false);
19
+ __AUTH_SOCIAL_BUTTON_HANDLER__
20
+
21
+ async function handleSubmit(formData: FormData) {
22
+ const email = String(formData.get("email") ?? "");
23
+ const password = String(formData.get("password") ?? "");
24
+ const name = String(formData.get("name") ?? "");
25
+
26
+ setIsPending(true);
27
+ setError(null);
28
+
29
+ try {
30
+ const result = isSignUp
31
+ ? await authClient.signUp.email({ email, password, name })
32
+ : await authClient.signIn.email({ email, password });
33
+
34
+ if (result.error) {
35
+ setError(result.error.message ?? "Authentication failed.");
36
+ return;
37
+ }
38
+
39
+ router.push("/dashboard");
40
+ router.refresh();
41
+ } catch {
42
+ setError("Authentication failed.");
43
+ } finally {
44
+ setIsPending(false);
45
+ }
46
+ }
47
+
48
+ return (
49
+ <div className="auth-container">
50
+ <div className="auth-card">
51
+ <div className="auth-brand">__PROJECT_NAME__</div>
52
+
53
+ <div className="auth-header">
54
+ <h1>{isSignUp ? "__AUTH_FORM_SIGN_UP_HEADING__" : "__AUTH_FORM_SIGN_IN_HEADING__"}</h1>
55
+ <p>{isSignUp ? "__AUTH_FORM_SIGN_UP_LEDE__" : "__AUTH_FORM_SIGN_IN_LEDE__"}</p>
56
+ </div>
57
+ __AUTH_SOCIAL_BUTTON__
58
+
59
+ <div className="auth-tabs">
60
+ <Link href="/sign-in" className={`auth-tab ${!isSignUp ? "active" : ""}`}>
61
+ Sign in
62
+ </Link>
63
+ <Link href="/sign-up" className={`auth-tab ${isSignUp ? "active" : ""}`}>
64
+ Sign up
65
+ </Link>
66
+ </div>
67
+
68
+ <form
69
+ action={(formData) => { void handleSubmit(formData); }}
70
+ className="auth-form"
71
+ >
72
+ {isSignUp ? (
73
+ <label className="auth-field">
74
+ <span className="auth-label">NAME</span>
75
+ <div className="auth-input-wrap">
76
+ <svg className="auth-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="10" cy="7" r="3.5" /><path d="M3.5 17.5c0-3.5 2.9-6 6.5-6s6.5 2.5 6.5 6" /></svg>
77
+ <input name="name" type="text" placeholder="Ada Lovelace" required />
78
+ </div>
79
+ </label>
80
+ ) : null}
81
+
82
+ <label className="auth-field">
83
+ <span className="auth-label">EMAIL</span>
84
+ <div className="auth-input-wrap">
85
+ <svg className="auth-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="2" y="4" width="16" height="12" rx="2" /><path d="M2 6l8 5 8-5" /></svg>
86
+ <input name="email" type="email" placeholder="you@example.com" required />
87
+ </div>
88
+ </label>
89
+
90
+ <label className="auth-field">
91
+ <span className="auth-label">PASSWORD</span>
92
+ <div className="auth-input-wrap">
93
+ <svg className="auth-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="4" y="9" width="12" height="8" rx="2" /><path d="M7 9V6a3 3 0 016 0v3" /></svg>
94
+ <input
95
+ name="password"
96
+ type={showPassword ? "text" : "password"}
97
+ placeholder="At least 8 characters"
98
+ minLength={8}
99
+ required
100
+ />
101
+ <button
102
+ type="button"
103
+ className="auth-toggle-pw"
104
+ onClick={() => setShowPassword(!showPassword)}
105
+ tabIndex={-1}
106
+ >
107
+ {showPassword ? (
108
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 10s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z" /><circle cx="10" cy="10" r="2.5" /></svg>
109
+ ) : (
110
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 10s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z" /><circle cx="10" cy="10" r="2.5" /><line x1="3" y1="17" x2="17" y2="3" /></svg>
111
+ )}
112
+ </button>
113
+ </div>
114
+ </label>
115
+
116
+ {error ? <p className="form-error">{error}</p> : null}
117
+
118
+ <button type="submit" className="auth-submit" disabled={isPending}>
119
+ {isPending ? "Working..." : isSignUp ? "Create account" : "Sign in"}{" "}
120
+ {!isPending && <span className="auth-submit-arrow">→</span>}
121
+ </button>
122
+ </form>
123
+
124
+ <p className="auth-footer">secured by better-auth</p>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { useState } from "react";
5
+
6
+ import { authClient } from "@/lib/auth-client";
7
+
8
+ export function SignOutButton() {
9
+ const router = useRouter();
10
+ const [isPending, setIsPending] = useState(false);
11
+
12
+ async function handleSignOut() {
13
+ setIsPending(true);
14
+
15
+ try {
16
+ await authClient.signOut();
17
+ router.push("/sign-in");
18
+ router.refresh();
19
+ } finally {
20
+ setIsPending(false);
21
+ }
22
+ }
23
+
24
+ return (
25
+ <button type="button" className="__SIGN_OUT_BUTTON_CLASS__" onClick={() => void handleSignOut()}>
26
+ {isPending ? "Signing out..." : "Sign out"}
27
+ </button>
28
+ );
29
+ }
@@ -0,0 +1,16 @@
1
+ import "server-only";
2
+
3
+ import { drizzle } from "__DRIZZLE_DRIVER_IMPORT__";
4
+ __DATABASE_CLIENT_IMPORT__
5
+
6
+ import { env } from "@/lib/env";
7
+
8
+ __DATABASE_GLOBAL_BLOCK__
9
+
10
+ const client = __DATABASE_CLIENT_EXPRESSION__;
11
+
12
+ if (env.NODE_ENV !== "production") {
13
+ __DATABASE_DEV_CACHE__
14
+ }
15
+
16
+ export const db = __DATABASE_DRIZZLE_EXPRESSION__;
@@ -0,0 +1,4 @@
1
+ // This file is intentionally minimal.
2
+ // Run `pnpm auth:generate` to replace it with Better Auth's generated Drizzle schema.
3
+
4
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from "./auth";
2
+ export * from "./projects";
@@ -0,0 +1,17 @@
1
+ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+
3
+ export const projects = pgTable("projects", {
4
+ id: text("id").primaryKey(),
5
+ name: text("name").notNull(),
6
+ slug: text("slug").notNull().unique(),
7
+ createdAt: timestamp("created_at", {
8
+ withTimezone: true
9
+ })
10
+ .defaultNow()
11
+ .notNull(),
12
+ updatedAt: timestamp("updated_at", {
13
+ withTimezone: true
14
+ })
15
+ .defaultNow()
16
+ .notNull()
17
+ });
@@ -0,0 +1,32 @@
1
+ import { db } from "@/db";
2
+ import { projects } from "@/db/schema";
3
+
4
+ const shouldSeedDemoData = ["yes"].includes("__SEED_DEMO_DATA__");
5
+
6
+ async function main() {
7
+ if (!shouldSeedDemoData) {
8
+ console.log("Demo seed data disabled.");
9
+ return;
10
+ }
11
+
12
+ await db
13
+ .insert(projects)
14
+ .values({
15
+ id: "project-demo",
16
+ name: "Demo Project",
17
+ slug: "demo-project"
18
+ })
19
+ .onConflictDoNothing();
20
+
21
+ console.log("Demo application data seed complete.");
22
+ console.log("Note: auth users are not created by db:seed. Sign up through Better Auth flows.");
23
+ }
24
+
25
+ main().catch((error) => {
26
+ console.error("Database seed failed.");
27
+ console.error(
28
+ "Make sure PostgreSQL is running, DATABASE_URL is set, and schema migrations have been applied first."
29
+ );
30
+ console.error(error);
31
+ process.exit(1);
32
+ });
@@ -0,0 +1,5 @@
1
+ "use client";
2
+
3
+ import { createAuthClient } from "better-auth/react";
4
+
5
+ export const authClient = createAuthClient();
@@ -0,0 +1,21 @@
1
+ import "server-only";
2
+
3
+ import { headers } from "next/headers";
4
+
5
+ import { auth } from "./auth";
6
+ import { env } from "./env";
7
+
8
+ export async function getSession() {
9
+ try {
10
+ return await auth.api.getSession({
11
+ headers: await headers()
12
+ });
13
+ } catch (error) {
14
+ if (env.NODE_ENV !== "production") {
15
+ console.warn("Better Auth session lookup failed. Falling back to anonymous mode.", error);
16
+ return null;
17
+ }
18
+
19
+ throw error;
20
+ }
21
+ }
@@ -0,0 +1,23 @@
1
+ import "server-only";
2
+
3
+ import { betterAuth } from "better-auth";
4
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
5
+
6
+ import { db } from "../db";
7
+ import { env } from "./env";
8
+ import * as schema from "../db/schema";
9
+
10
+ export const auth = betterAuth({
11
+ appName: "__PROJECT_NAME__",
12
+ baseURL: env.BETTER_AUTH_URL,
13
+ secret: env.BETTER_AUTH_SECRET,
14
+ trustedOrigins: [env.NEXT_PUBLIC_APP_URL],
15
+ database: drizzleAdapter(db, {
16
+ provider: "pg",
17
+ schema
18
+ }),
19
+ emailAndPassword: {
20
+ enabled: true,
21
+ autoSignIn: true
22
+ }__AUTH_SOCIAL_PROVIDERS_BLOCK__
23
+ });
@@ -0,0 +1,37 @@
1
+ import "server-only";
2
+
3
+ import { env } from "@/lib/env";
4
+
5
+ __BILLING_PROVIDER_IMPORTS__
6
+
7
+ import type { BillingProvider, BillingProviderName } from "./types";
8
+
9
+ export function getBillingProviderName(input?: string): BillingProviderName {
10
+ if (input === "stripe" || input === "polar") {
11
+ return input;
12
+ }
13
+
14
+ if (env.BILLING_PROVIDER === "stripe" || env.BILLING_PROVIDER === "polar") {
15
+ return env.BILLING_PROVIDER;
16
+ }
17
+
18
+ if (env.BILLING_PROVIDER === "both") {
19
+ return "stripe";
20
+ }
21
+
22
+ throw new Error("Billing is disabled for this project.");
23
+ }
24
+
25
+ export function getBillingProvider(input?: string): BillingProvider {
26
+ const provider = getBillingProviderName(input);
27
+ const billingProviderFactories: Partial<Record<BillingProviderName, () => BillingProvider>> = {
28
+ __BILLING_PROVIDER_FACTORIES__
29
+ };
30
+ const factory = billingProviderFactories[provider];
31
+
32
+ if (!factory) {
33
+ throw new Error(`Billing provider is not installed: ${provider}.`);
34
+ }
35
+
36
+ return factory();
37
+ }
@@ -0,0 +1,80 @@
1
+ import "server-only";
2
+
3
+ import { env } from "@/lib/env";
4
+
5
+ import type {
6
+ BillingProvider,
7
+ CheckoutSessionInput,
8
+ PortalSessionInput,
9
+ WebhookInput
10
+ } from "../types";
11
+
12
+ function getPolarApiBaseUrl() {
13
+ return env.POLAR_SERVER === "production"
14
+ ? "https://api.polar.sh/v1"
15
+ : "https://sandbox-api.polar.sh/v1";
16
+ }
17
+
18
+ async function polarFetch<T>(pathname: string, body: Record<string, unknown>) {
19
+ if (!env.POLAR_ACCESS_TOKEN) {
20
+ throw new Error("POLAR_ACCESS_TOKEN is required for Polar billing.");
21
+ }
22
+
23
+ const response = await fetch(`${getPolarApiBaseUrl()}${pathname}`, {
24
+ method: "POST",
25
+ headers: {
26
+ Authorization: `Bearer ${env.POLAR_ACCESS_TOKEN}`,
27
+ "Content-Type": "application/json"
28
+ },
29
+ body: JSON.stringify(body)
30
+ });
31
+
32
+ if (!response.ok) {
33
+ const message = await response.text();
34
+ throw new Error(`Polar request failed: ${message}`);
35
+ }
36
+
37
+ return (await response.json()) as T;
38
+ }
39
+
40
+ export class PolarBillingProvider implements BillingProvider {
41
+ async createCheckoutSession(input: CheckoutSessionInput) {
42
+ if (!env.POLAR_PRODUCT_ID) {
43
+ throw new Error("POLAR_PRODUCT_ID is required for Polar checkout.");
44
+ }
45
+
46
+ const checkout = await polarFetch<{ url?: string }>("/checkouts", {
47
+ products: [env.POLAR_PRODUCT_ID],
48
+ external_customer_id: input.userId,
49
+ customer_email: input.customerEmail,
50
+ success_url: input.successUrl,
51
+ metadata: {
52
+ userId: input.userId
53
+ }
54
+ });
55
+
56
+ if (!checkout.url) {
57
+ throw new Error("Polar did not return a checkout URL.");
58
+ }
59
+
60
+ return { url: checkout.url };
61
+ }
62
+
63
+ async createCustomerPortalSession(input: PortalSessionInput) {
64
+ const session = await polarFetch<{ customer_portal_url?: string }>("/customer-sessions/", {
65
+ external_customer_id: input.userId
66
+ });
67
+
68
+ if (!session.customer_portal_url) {
69
+ throw new Error("Polar did not return a customer portal URL.");
70
+ }
71
+
72
+ return { url: session.customer_portal_url };
73
+ }
74
+
75
+ async handleWebhook(_input: WebhookInput) {
76
+ throw new Error(
77
+ "Polar webhook verification is intentionally left for follow-up implementation after confirming your webhook signing strategy."
78
+ );
79
+ }
80
+ }
@@ -0,0 +1,77 @@
1
+ import "server-only";
2
+
3
+ import Stripe from "stripe";
4
+
5
+ import { env } from "@/lib/env";
6
+
7
+ import type {
8
+ BillingProvider,
9
+ CheckoutSessionInput,
10
+ PortalSessionInput,
11
+ WebhookInput
12
+ } from "../types";
13
+
14
+ let stripeClient: Stripe | null = null;
15
+
16
+ function getStripe() {
17
+ if (!env.STRIPE_SECRET_KEY) {
18
+ throw new Error("STRIPE_SECRET_KEY is required for Stripe billing.");
19
+ }
20
+
21
+ stripeClient ??= new Stripe(env.STRIPE_SECRET_KEY);
22
+ return stripeClient;
23
+ }
24
+
25
+ export class StripeBillingProvider implements BillingProvider {
26
+ async createCheckoutSession(input: CheckoutSessionInput) {
27
+ if (!env.STRIPE_PRICE_ID) {
28
+ throw new Error("STRIPE_PRICE_ID is required for Stripe checkout.");
29
+ }
30
+
31
+ const stripe = getStripe();
32
+ const session = await stripe.checkout.sessions.create({
33
+ mode: "subscription",
34
+ line_items: [
35
+ {
36
+ price: env.STRIPE_PRICE_ID,
37
+ quantity: 1
38
+ }
39
+ ],
40
+ customer_email: input.customerEmail,
41
+ success_url: input.successUrl,
42
+ cancel_url: input.cancelUrl,
43
+ allow_promotion_codes: true,
44
+ metadata: {
45
+ userId: input.userId
46
+ }
47
+ });
48
+
49
+ if (!session.url) {
50
+ throw new Error("Stripe did not return a checkout URL.");
51
+ }
52
+
53
+ return { url: session.url };
54
+ }
55
+
56
+ async createCustomerPortalSession(
57
+ _input: PortalSessionInput
58
+ ): Promise<{ url: string }> {
59
+ throw new Error(
60
+ "Stripe portal requires a persisted Stripe customer ID. Add customer mapping before enabling portal access."
61
+ );
62
+ }
63
+
64
+ async handleWebhook(input: WebhookInput) {
65
+ if (!env.STRIPE_WEBHOOK_SECRET) {
66
+ throw new Error("STRIPE_WEBHOOK_SECRET is required for Stripe webhooks.");
67
+ }
68
+
69
+ const signature = input.headers.get("stripe-signature");
70
+ if (!signature) {
71
+ throw new Error("Missing Stripe signature header.");
72
+ }
73
+
74
+ const stripe = getStripe();
75
+ stripe.webhooks.constructEvent(input.rawBody, signature, env.STRIPE_WEBHOOK_SECRET);
76
+ }
77
+ }