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,25 @@
1
+ export type BillingProviderName = "stripe" | "polar";
2
+
3
+ export type CheckoutSessionInput = {
4
+ customerEmail: string;
5
+ successUrl: string;
6
+ cancelUrl: string;
7
+ userId: string;
8
+ };
9
+
10
+ export type PortalSessionInput = {
11
+ customerEmail: string;
12
+ returnUrl: string;
13
+ userId: string;
14
+ };
15
+
16
+ export type WebhookInput = {
17
+ headers: Headers;
18
+ rawBody: string;
19
+ };
20
+
21
+ export interface BillingProvider {
22
+ createCheckoutSession(input: CheckoutSessionInput): Promise<{ url: string }>;
23
+ createCustomerPortalSession(input: PortalSessionInput): Promise<{ url: string }>;
24
+ handleWebhook(input: WebhookInput): Promise<void>;
25
+ }
@@ -0,0 +1,19 @@
1
+ import "server-only";
2
+
3
+ __EMAIL_PROVIDER_IMPORTS__
4
+
5
+ __EMAIL_ENV_IMPORT__
6
+
7
+ import type { EmailTemplateResult } from "./templates";
8
+
9
+ __EMAIL_PROVIDER_SETUP__
10
+
11
+ __EMAIL_FROM_ADDRESS_HELPER__
12
+
13
+ export async function sendEmail(__EMAIL_SEND_SIGNATURE__: {
14
+ to: string | string[];
15
+ template: EmailTemplateResult;
16
+ }) {
17
+ void __EMAIL_SEND_SIGNATURE__;
18
+ __EMAIL_SEND_IMPLEMENTATION__
19
+ }
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { __EMAIL_TEMPLATE_TEST_IMPORT__ } from "@/lib/email/templates";
4
+
5
+ describe("__EMAIL_TEMPLATE_TEST_SUITE__", () => {
6
+ it("__EMAIL_TEMPLATE_TEST_CASE__", () => {
7
+ const template = __EMAIL_TEMPLATE_TEST_FACTORY__;
8
+
9
+ expect(template.subject).toContain("__EMAIL_TEMPLATE_EXPECT_SUBJECT__");
10
+ expect(template.text).toContain("__EMAIL_TEMPLATE_EXPECT_TEXT__");
11
+ });
12
+ });
@@ -0,0 +1,40 @@
1
+ export type EmailTemplateResult = {
2
+ subject: string;
3
+ text: string;
4
+ html: string;
5
+ };
6
+
7
+ export function createWelcomeEmailTemplate(input: {
8
+ appName: string;
9
+ recipientName?: string | null;
10
+ }): EmailTemplateResult {
11
+ const greeting = input.recipientName ? `Hi ${input.recipientName},` : "Hi there,";
12
+
13
+ return {
14
+ subject: `Welcome to ${input.appName}`,
15
+ text: `${greeting}\n\nYour starter app is ready. Auth, billing, and the core project structure are already in place.\n\nNext step: sign in and continue building.\n`,
16
+ html: `<p>${greeting}</p><p>Your starter app is ready. Auth, billing, and the core project structure are already in place.</p><p>Next step: sign in and continue building.</p>`
17
+ };
18
+ }
19
+
20
+ export function createPasswordResetPlaceholderTemplate(input: {
21
+ appName: string;
22
+ resetUrl: string;
23
+ }): EmailTemplateResult {
24
+ return {
25
+ subject: `Reset your ${input.appName} password`,
26
+ text: `Use this link to reset your password: ${input.resetUrl}`,
27
+ html: `<p>Use this link to reset your password:</p><p><a href="${input.resetUrl}">${input.resetUrl}</a></p>`
28
+ };
29
+ }
30
+
31
+ export function createBillingUpdatePlaceholderTemplate(input: {
32
+ appName: string;
33
+ provider: string;
34
+ }): EmailTemplateResult {
35
+ return {
36
+ subject: `${input.appName} billing update`,
37
+ text: `A billing event was received from ${input.provider}. Replace this placeholder with your real billing notification workflow.`,
38
+ html: `<p>A billing event was received from <strong>${input.provider}</strong>.</p><p>Replace this placeholder with your real billing notification workflow.</p>`
39
+ };
40
+ }
@@ -0,0 +1,83 @@
1
+ import "server-only";
2
+
3
+ import { z } from "zod";
4
+
5
+ const envSchema = z.object({
6
+ NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
7
+ NEXT_PUBLIC_APP_URL: z.string().url(),
8
+ BETTER_AUTH_URL: z.string().url(),
9
+ DATABASE_URL: z.string().min(1),
10
+ BETTER_AUTH_SECRET: z.string().min(1),
11
+ __AUTH_PROVIDER_ENV_SCHEMA__
12
+ BILLING_PROVIDER: z.enum(["stripe", "polar", "both", "none"]).default("stripe"),
13
+ EMAIL_PROVIDER: z.enum(["resend", "none"]).default("resend"),
14
+ RESEND_API_KEY: z.string().min(1).optional(),
15
+ EMAIL_FROM: z.string().min(1).optional(),
16
+ STRIPE_PRICE_ID: z.string().min(1).optional(),
17
+ STRIPE_SECRET_KEY: z.string().min(1).optional(),
18
+ STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(),
19
+ POLAR_ACCESS_TOKEN: z.string().min(1).optional(),
20
+ POLAR_ORGANIZATION_ID: z.string().min(1).optional(),
21
+ POLAR_PRODUCT_ID: z.string().min(1).optional(),
22
+ POLAR_SERVER: z.enum(["production", "sandbox"]).default("sandbox"),
23
+ POLAR_WEBHOOK_SECRET: z.string().min(1).optional()
24
+ });
25
+
26
+ type Env = z.infer<typeof envSchema>;
27
+
28
+ let cachedEnv: Env | null = null;
29
+ const isBuildRuntime =
30
+ process.env.npm_lifecycle_event === "build" ||
31
+ process.env.NEXT_PHASE === "phase-production-build";
32
+ const isProductionRuntime = process.env.NODE_ENV === "production" && !isBuildRuntime;
33
+
34
+ function readRequiredValue(name: string, fallback: string) {
35
+ const value = process.env[name];
36
+
37
+ if (value) {
38
+ return value;
39
+ }
40
+
41
+ if (!isProductionRuntime) {
42
+ return fallback;
43
+ }
44
+
45
+ return value;
46
+ }
47
+
48
+ function parseEnv(): Env {
49
+ return envSchema.parse({
50
+ NODE_ENV: process.env.NODE_ENV,
51
+ NEXT_PUBLIC_APP_URL: readRequiredValue("NEXT_PUBLIC_APP_URL", "http://localhost:3000"),
52
+ BETTER_AUTH_URL: readRequiredValue("BETTER_AUTH_URL", "http://localhost:3000"),
53
+ DATABASE_URL: readRequiredValue(
54
+ "DATABASE_URL",
55
+ "postgresql://postgres:postgres@localhost:5432/skit"
56
+ ),
57
+ BETTER_AUTH_SECRET: readRequiredValue("BETTER_AUTH_SECRET", "build-placeholder-secret"),
58
+ __AUTH_PROVIDER_ENV_VALUES__
59
+ BILLING_PROVIDER: process.env.BILLING_PROVIDER,
60
+ EMAIL_PROVIDER: process.env.EMAIL_PROVIDER,
61
+ RESEND_API_KEY: process.env.RESEND_API_KEY,
62
+ EMAIL_FROM: process.env.EMAIL_FROM,
63
+ STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID,
64
+ STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
65
+ STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
66
+ POLAR_ACCESS_TOKEN: process.env.POLAR_ACCESS_TOKEN,
67
+ POLAR_ORGANIZATION_ID: process.env.POLAR_ORGANIZATION_ID,
68
+ POLAR_PRODUCT_ID: process.env.POLAR_PRODUCT_ID,
69
+ POLAR_SERVER: process.env.POLAR_SERVER,
70
+ POLAR_WEBHOOK_SECRET: process.env.POLAR_WEBHOOK_SECRET
71
+ });
72
+ }
73
+
74
+ export function getEnv(): Env {
75
+ cachedEnv ??= parseEnv();
76
+ return cachedEnv;
77
+ }
78
+
79
+ export const env = new Proxy({} as Env, {
80
+ get(_target, property) {
81
+ return getEnv()[property as keyof Env];
82
+ }
83
+ });
@@ -0,0 +1,8 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test("__HOME_E2E_TEST_NAME__", async ({ page }) => {
4
+ await page.goto("/");
5
+
6
+ await expect(page.getByRole("heading", { name: /__HOME_E2E_HEADING__/i })).toBeVisible();
7
+ await expect(page.getByRole("link", { name: /__HOME_E2E_LINK__/i })).toBeVisible();
8
+ });
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "es2022"],
5
+ "allowJs": false,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "exactOptionalPropertyTypes": true,
10
+ "noEmit": true,
11
+ "esModuleInterop": true,
12
+ "module": "esnext",
13
+ "moduleResolution": "bundler",
14
+ "baseUrl": ".",
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "jsx": "react-jsx",
18
+ "incremental": true,
19
+ "verbatimModuleSyntax": true,
20
+ "moduleDetection": "force",
21
+ "plugins": [{ "name": "next" }],
22
+ "paths": {
23
+ "@/*": ["./src/*"]
24
+ }
25
+ },
26
+ "include": [
27
+ "next-env.d.ts",
28
+ "**/*.ts",
29
+ "**/*.tsx",
30
+ ".next/types/**/*.ts",
31
+ ".next/dev/types/**/*.ts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { defineConfig } from "vitest/config";
5
+
6
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ export default defineConfig({
9
+ resolve: {
10
+ alias: {
11
+ "@": path.resolve(dirname, "./src")
12
+ }
13
+ },
14
+ test: {
15
+ environment: "jsdom",
16
+ globals: true,
17
+ include: ["src/**/*.test.{ts,tsx}"]
18
+ }
19
+ });
@@ -0,0 +1,16 @@
1
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
2
+ BETTER_AUTH_URL=http://localhost:3000
3
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/__PROJECT_NAME__
4
+ BETTER_AUTH_SECRET=replace-me
5
+ RESEND_API_KEY=re_xxx
6
+ EMAIL_FROM=Panda Starter <onboarding@example.com>
7
+ STRIPE_SECRET_KEY=sk_test_xxx
8
+ STRIPE_PRICE_ID=price_xxx
9
+ STRIPE_WEBHOOK_SECRET=whsec_xxx
10
+ POLAR_ACCESS_TOKEN=polar_pat_xxx
11
+ POLAR_ORGANIZATION_ID=org_xxx
12
+ POLAR_PRODUCT_ID=prod_xxx
13
+ POLAR_SERVER=sandbox
14
+ POLAR_WEBHOOK_SECRET=polar_whsec_xxx
15
+ BILLING_PROVIDER=__BILLING_PROVIDER__
16
+ EMAIL_PROVIDER=__EMAIL_PROVIDER__
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+
8
+ jobs:
9
+ quality:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v4
14
+
15
+ - name: Setup Node
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: 22
19
+ cache: __CI_CACHE__
20
+
21
+ - name: Install
22
+ run: __CI_INSTALL_COMMAND__
23
+
24
+ - name: Lint
25
+ run: __CI_RUN_LINT__
26
+
27
+ - name: Typecheck
28
+ run: __CI_RUN_TYPECHECK__
29
+
30
+ - name: Unit tests
31
+ run: __CI_RUN_TEST__
32
+
33
+ - name: Build
34
+ run: __CI_RUN_BUILD__
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env sh
2
+
3
+ npm run lint-staged && npm run typecheck
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env sh
2
+
3
+ npm run test
@@ -0,0 +1,3 @@
1
+ .next
2
+ coverage
3
+ node_modules
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ if (!process.env.DATABASE_URL) {
4
+ throw new Error("DATABASE_URL is required to use Drizzle.");
5
+ }
6
+
7
+ export default defineConfig({
8
+ schema: "./src/db/schema/index.ts",
9
+ out: "./src/db/migrations",
10
+ dialect: "postgresql",
11
+ dbCredentials: {
12
+ url: process.env.DATABASE_URL
13
+ },
14
+ verbose: true,
15
+ strict: true
16
+ });
@@ -0,0 +1,127 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { defineConfig, globalIgnores } from "eslint/config";
5
+ import prettier from "eslint-config-prettier/flat";
6
+ import nextTs from "eslint-config-next/typescript";
7
+ import nextVitals from "eslint-config-next/core-web-vitals";
8
+ import importPlugin from "eslint-plugin-import";
9
+ import unusedImports from "eslint-plugin-unused-imports";
10
+ import globals from "globals";
11
+
12
+ const SOURCE_FILES = ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"];
13
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ export default defineConfig([
16
+ ...nextVitals,
17
+ ...nextTs,
18
+ {
19
+ files: SOURCE_FILES,
20
+ languageOptions: {
21
+ parserOptions: {
22
+ projectService: true,
23
+ tsconfigRootDir: dirname
24
+ },
25
+ globals: {
26
+ ...globals.browser,
27
+ ...globals.nodeBuiltin
28
+ }
29
+ },
30
+ plugins: {
31
+ import: importPlugin,
32
+ "unused-imports": unusedImports
33
+ },
34
+ settings: {
35
+ "import/resolver": {
36
+ typescript: true
37
+ }
38
+ },
39
+ rules: {
40
+ "@typescript-eslint/consistent-type-imports": [
41
+ "error",
42
+ {
43
+ prefer: "type-imports",
44
+ fixStyle: "inline-type-imports"
45
+ }
46
+ ],
47
+ "@typescript-eslint/no-floating-promises": "error",
48
+ "@typescript-eslint/no-misused-promises": [
49
+ "error",
50
+ {
51
+ checksVoidReturn: {
52
+ attributes: false
53
+ }
54
+ }
55
+ ],
56
+ "@typescript-eslint/no-unused-vars": "off",
57
+ "import/first": "error",
58
+ "import/newline-after-import": "error",
59
+ "import/no-duplicates": "error",
60
+ "import/order": [
61
+ "error",
62
+ {
63
+ groups: [
64
+ "builtin",
65
+ "external",
66
+ "internal",
67
+ ["parent", "sibling", "index"],
68
+ "object",
69
+ "type"
70
+ ],
71
+ alphabetize: {
72
+ order: "asc",
73
+ caseInsensitive: true
74
+ },
75
+ "newlines-between": "always",
76
+ pathGroups: [
77
+ {
78
+ pattern: "@/**",
79
+ group: "internal",
80
+ position: "before"
81
+ }
82
+ ],
83
+ pathGroupsExcludedImportTypes: ["builtin"]
84
+ }
85
+ ],
86
+ "unused-imports/no-unused-imports": "error",
87
+ "unused-imports/no-unused-vars": [
88
+ "warn",
89
+ {
90
+ vars: "all",
91
+ varsIgnorePattern: "^_",
92
+ args: "after-used",
93
+ argsIgnorePattern: "^_"
94
+ }
95
+ ]
96
+ }
97
+ },
98
+ {
99
+ files: SOURCE_FILES,
100
+ ignores: ["drizzle.config.ts", "src/lib/env.ts"],
101
+ rules: {
102
+ "no-restricted-properties": [
103
+ "error",
104
+ {
105
+ object: "process",
106
+ property: "env",
107
+ message: "Use the validated env object from src/lib/env.ts."
108
+ }
109
+ ]
110
+ }
111
+ },
112
+ prettier,
113
+ globalIgnores([
114
+ ".next/**",
115
+ "out/**",
116
+ "build/**",
117
+ "coverage/**",
118
+ "drizzle.config.ts",
119
+ "eslint.config.mjs",
120
+ "next.config.ts",
121
+ "playwright.config.ts",
122
+ "prettier.config.mjs",
123
+ "proxy.ts",
124
+ "next-env.d.ts",
125
+ "vitest.config.ts"
126
+ ])
127
+ ]);
@@ -0,0 +1,4 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // This file is managed by Next.js.
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {};
4
+
5
+ export default nextConfig;
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "packageManager": "__PACKAGE_MANAGER__",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "next dev",
9
+ "build": "next build",
10
+ "start": "next start",
11
+ "lint": "eslint . --max-warnings=0",
12
+ "lint:fix": "eslint . --fix",
13
+ "typecheck": "tsc --noEmit",
14
+ "format": "prettier . --write",
15
+ "format:check": "prettier . --check",
16
+ "check": "__CHECK_COMMAND__",
17
+ "prepare": "husky",
18
+ "lint-staged": "lint-staged",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "test:e2e": "playwright test",
22
+ "test:e2e:headed": "playwright test --headed",
23
+ "db:generate": "drizzle-kit generate",
24
+ "db:migrate": "drizzle-kit migrate",
25
+ "db:push": "drizzle-kit push",
26
+ "db:studio": "drizzle-kit studio",
27
+ "db:seed": "tsx src/db/seeds/index.ts",
28
+ "auth:generate": "npx @better-auth/cli@latest generate --config src/lib/auth.ts --output src/db/schema/auth.ts",
29
+ "auth:secret": "npx @better-auth/cli@latest secret"
30
+ },
31
+ "dependencies": {
32
+ "better-auth": "1.3.8",
33
+ "drizzle-orm": "0.44.x",
34
+ "next": "16.2.x",
35
+ __DATABASE_DEPENDENCIES__
36
+ "react": "19.2.x",
37
+ "react-dom": "19.2.x",
38
+ __PROVIDER_PACKAGE_DEPENDENCIES__
39
+ "server-only": "^0.0.1",
40
+ "zod": "4.1.5"
41
+ },
42
+ "devDependencies": {
43
+ "@better-auth/cli": "1.3.8",
44
+ "@playwright/test": "1.55.0",
45
+ "@testing-library/react": "16.3.0",
46
+ "@types/node": "^24.0.0",
47
+ __DATABASE_DEV_DEPENDENCIES__
48
+ "@types/react": "^19.0.0",
49
+ "@types/react-dom": "^19.0.0",
50
+ "drizzle-kit": "0.31.x",
51
+ "eslint": "9.35.0",
52
+ "eslint-config-next": "16.2.x",
53
+ "eslint-config-prettier": "10.1.8",
54
+ "eslint-import-resolver-typescript": "^3.10.1",
55
+ "eslint-plugin-import": "2.32.0",
56
+ "eslint-plugin-unused-imports": "4.2.0",
57
+ "globals": "16.3.0",
58
+ "husky": "^9.1.7",
59
+ "jsdom": "26.1.0",
60
+ "lint-staged": "^16.1.6",
61
+ "playwright": "1.55.0",
62
+ "prettier": "3.6.2",
63
+ "tsx": "4.20.x",
64
+ "typescript": "5.9.x",
65
+ "vitest": "3.2.4"
66
+ },
67
+ "lint-staged": {
68
+ "*.{js,jsx,ts,tsx,mjs,cjs}": [
69
+ "eslint --fix"
70
+ ],
71
+ "*.{json,md,css,yml,yaml,html}": [
72
+ "prettier --write"
73
+ ]
74
+ }
75
+ }
@@ -0,0 +1,21 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+
3
+ export default defineConfig({
4
+ testDir: "./tests/e2e",
5
+ fullyParallel: true,
6
+ use: {
7
+ baseURL: "http://127.0.0.1:3000",
8
+ trace: "on-first-retry"
9
+ },
10
+ webServer: {
11
+ command: "npm run dev",
12
+ url: "http://127.0.0.1:3000",
13
+ reuseExistingServer: !process.env.CI
14
+ },
15
+ projects: [
16
+ {
17
+ name: "chromium",
18
+ use: { ...devices["Desktop Chrome"] }
19
+ }
20
+ ]
21
+ });
@@ -0,0 +1,9 @@
1
+ /** @type {import("prettier").Config} */
2
+ const config = {
3
+ semi: true,
4
+ singleQuote: false,
5
+ trailingComma: "none",
6
+ printWidth: 88
7
+ };
8
+
9
+ export default config;
@@ -0,0 +1,28 @@
1
+ import { NextResponse } from "next/server";
2
+ import type { NextRequest } from "next/server";
3
+
4
+ import { getSessionCookie } from "better-auth/cookies";
5
+
6
+ export function proxy(request: NextRequest) {
7
+ const sessionCookie = getSessionCookie(request);
8
+ const { pathname } = request.nextUrl;
9
+
10
+ if (
11
+ !sessionCookie &&
12
+ (pathname.startsWith("/dashboard") ||
13
+ pathname.startsWith("/billing") ||
14
+ pathname.startsWith("/email"))
15
+ ) {
16
+ return NextResponse.redirect(new URL("/sign-in", request.url));
17
+ }
18
+
19
+ if (sessionCookie && (pathname === "/sign-in" || pathname === "/sign-up")) {
20
+ return NextResponse.redirect(new URL("/dashboard", request.url));
21
+ }
22
+
23
+ return NextResponse.next();
24
+ }
25
+
26
+ export const config = {
27
+ matcher: ["/dashboard/:path*", "/billing/:path*", "/email/:path*", "/sign-in", "/sign-up"]
28
+ };
@@ -0,0 +1,5 @@
1
+ import { toNextJsHandler } from "better-auth/next-js";
2
+
3
+ import { auth } from "@/lib/auth";
4
+
5
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -0,0 +1,26 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { getSession } from "@/lib/auth-session";
4
+ import { getBillingProvider, getBillingProviderName } from "@/lib/billing";
5
+ import { env } from "@/lib/env";
6
+
7
+ export async function POST(request: Request) {
8
+ const session = await getSession();
9
+
10
+ if (!session) {
11
+ redirect("/sign-in");
12
+ }
13
+
14
+ const formData = await request.formData();
15
+ const selected = String(formData.get("provider") ?? "");
16
+ const providerName = getBillingProviderName(selected);
17
+ const provider = getBillingProvider(providerName);
18
+ const checkout = await provider.createCheckoutSession({
19
+ userId: session.user.id,
20
+ customerEmail: session.user.email,
21
+ successUrl: `${env.NEXT_PUBLIC_APP_URL}/billing?success=1&provider=${providerName}`,
22
+ cancelUrl: `${env.NEXT_PUBLIC_APP_URL}/billing?canceled=1&provider=${providerName}`
23
+ });
24
+
25
+ redirect(checkout.url);
26
+ }
@@ -0,0 +1,25 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { getSession } from "@/lib/auth-session";
4
+ import { getBillingProvider, getBillingProviderName } from "@/lib/billing";
5
+ import { env } from "@/lib/env";
6
+
7
+ export async function POST(request: Request) {
8
+ const session = await getSession();
9
+
10
+ if (!session) {
11
+ redirect("/sign-in");
12
+ }
13
+
14
+ const formData = await request.formData();
15
+ const selected = String(formData.get("provider") ?? "");
16
+ const providerName = getBillingProviderName(selected);
17
+ const provider = getBillingProvider(providerName);
18
+ const portal = await provider.createCustomerPortalSession({
19
+ userId: session.user.id,
20
+ customerEmail: session.user.email,
21
+ returnUrl: `${env.NEXT_PUBLIC_APP_URL}/billing?provider=${providerName}`
22
+ });
23
+
24
+ redirect(portal.url);
25
+ }