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.
- package/README.md +36 -0
- package/bin/create-skit.mjs +1064 -0
- package/lib/module-application.mjs +281 -0
- package/lib/module-resolver.mjs +179 -0
- package/modules/README.md +22 -0
- package/modules/ai-dx/files/AGENTS.md +116 -0
- package/modules/ai-dx/files/ARCHITECTURE.md +103 -0
- package/modules/ai-dx/module.json +14 -0
- package/modules/ai-dx-claude/files/CLAUDE.md +8 -0
- package/modules/ai-dx-claude/module.json +13 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/auth.mdc +53 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/database.mdc +48 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/env.mdc +43 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/nextjs.mdc +58 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/project.mdc +33 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/testing.mdc +55 -0
- package/modules/ai-dx-cursor/module.json +18 -0
- package/modules/ai-dx-gemini/files/.gemini/GEMINI.md +5 -0
- package/modules/ai-dx-gemini/module.json +13 -0
- package/modules/auth-core/module.json +8 -0
- package/modules/auth-github/module.json +20 -0
- package/modules/billing-polar/module.json +20 -0
- package/modules/billing-stripe/module.json +23 -0
- package/modules/dashboard-shell/files/src/app/globals.css +756 -0
- package/modules/dashboard-shell/files/src/app/settings/page.tsx +67 -0
- package/modules/dashboard-shell/module.json +11 -0
- package/modules/db-pg/module.json +21 -0
- package/modules/db-postgresjs/module.json +21 -0
- package/modules/deploy-docker/files/.dockerignore +19 -0
- package/modules/deploy-docker/files/Dockerfile +25 -0
- package/modules/deploy-docker/module.json +11 -0
- package/modules/email-resend/module.json +21 -0
- package/modules/quality-baseline/module.json +8 -0
- package/modules/testing-baseline/module.json +8 -0
- package/package.json +40 -0
- package/presets/README.md +12 -0
- package/presets/blank.json +67 -0
- package/presets/dashboard.json +67 -0
- package/templates/base-web/.env.example +17 -0
- package/templates/base-web/.github/workflows/ci.yml +34 -0
- package/templates/base-web/.husky/pre-commit +3 -0
- package/templates/base-web/.husky/pre-push +3 -0
- package/templates/base-web/.prettierignore +3 -0
- package/templates/base-web/README.md +42 -0
- package/templates/base-web/drizzle.config.ts +16 -0
- package/templates/base-web/eslint.config.mjs +127 -0
- package/templates/base-web/manifest.json +5 -0
- package/templates/base-web/next-env.d.ts +4 -0
- package/templates/base-web/next.config.ts +5 -0
- package/templates/base-web/package.json +75 -0
- package/templates/base-web/playwright.config.ts +21 -0
- package/templates/base-web/prettier.config.mjs +9 -0
- package/templates/base-web/proxy.ts +23 -0
- package/templates/base-web/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/base-web/src/app/api/billing/checkout/route.ts +26 -0
- package/templates/base-web/src/app/api/billing/portal/route.ts +25 -0
- package/templates/base-web/src/app/api/email/test/route.ts +28 -0
- package/templates/base-web/src/app/api/webhooks/polar/route.ts +5 -0
- package/templates/base-web/src/app/api/webhooks/stripe/route.ts +5 -0
- package/templates/base-web/src/app/billing/page.tsx +55 -0
- package/templates/base-web/src/app/dashboard/page.tsx +15 -0
- package/templates/base-web/src/app/email/page.tsx +46 -0
- package/templates/base-web/src/app/error.tsx +27 -0
- package/templates/base-web/src/app/globals.css +534 -0
- package/templates/base-web/src/app/layout.tsx +19 -0
- package/templates/base-web/src/app/llms-full.txt/route.ts +158 -0
- package/templates/base-web/src/app/llms.txt/route.ts +59 -0
- package/templates/base-web/src/app/loading.tsx +24 -0
- package/templates/base-web/src/app/not-found.tsx +16 -0
- package/templates/base-web/src/app/page.tsx +5 -0
- package/templates/base-web/src/app/sign-in/page.tsx +14 -0
- package/templates/base-web/src/app/sign-up/page.tsx +14 -0
- package/templates/base-web/src/components/auth/email-auth-form.test.tsx +40 -0
- package/templates/base-web/src/components/auth/email-auth-form.tsx +128 -0
- package/templates/base-web/src/components/auth/sign-out-button.tsx +29 -0
- package/templates/base-web/src/db/index.ts +16 -0
- package/templates/base-web/src/db/schema/auth.ts +4 -0
- package/templates/base-web/src/db/schema/index.ts +2 -0
- package/templates/base-web/src/db/schema/projects.ts +17 -0
- package/templates/base-web/src/db/seeds/index.ts +32 -0
- package/templates/base-web/src/lib/auth-client.ts +5 -0
- package/templates/base-web/src/lib/auth-session.ts +21 -0
- package/templates/base-web/src/lib/auth.ts +23 -0
- package/templates/base-web/src/lib/billing/index.ts +37 -0
- package/templates/base-web/src/lib/billing/providers/polar.ts +80 -0
- package/templates/base-web/src/lib/billing/providers/stripe.ts +77 -0
- package/templates/base-web/src/lib/billing/types.ts +25 -0
- package/templates/base-web/src/lib/email/index.ts +19 -0
- package/templates/base-web/src/lib/email/templates.test.ts +12 -0
- package/templates/base-web/src/lib/email/templates.ts +40 -0
- package/templates/base-web/src/lib/env.ts +83 -0
- package/templates/base-web/tests/e2e/home.spec.ts +8 -0
- package/templates/base-web/tsconfig.json +34 -0
- package/templates/base-web/vitest.config.ts +19 -0
- package/templates/blank/.env.example +16 -0
- package/templates/blank/.github/workflows/ci.yml +34 -0
- package/templates/blank/.husky/pre-commit +3 -0
- package/templates/blank/.husky/pre-push +3 -0
- package/templates/blank/.prettierignore +3 -0
- package/templates/blank/drizzle.config.ts +16 -0
- package/templates/blank/eslint.config.mjs +127 -0
- package/templates/blank/next-env.d.ts +4 -0
- package/templates/blank/next.config.ts +5 -0
- package/templates/blank/package.json +75 -0
- package/templates/blank/playwright.config.ts +21 -0
- package/templates/blank/prettier.config.mjs +9 -0
- package/templates/blank/proxy.ts +28 -0
- package/templates/blank/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/blank/src/app/api/billing/checkout/route.ts +26 -0
- package/templates/blank/src/app/api/billing/portal/route.ts +25 -0
- package/templates/blank/src/app/api/email/test/route.ts +28 -0
- package/templates/blank/src/app/api/webhooks/polar/route.ts +5 -0
- package/templates/blank/src/app/api/webhooks/stripe/route.ts +5 -0
- package/templates/blank/src/app/billing/page.tsx +70 -0
- package/templates/blank/src/app/email/page.tsx +46 -0
- package/templates/blank/src/app/globals.css +394 -0
- package/templates/blank/src/app/layout.tsx +19 -0
- package/templates/blank/src/app/page.tsx +23 -0
- package/templates/blank/src/app/sign-in/page.tsx +18 -0
- package/templates/blank/src/app/sign-up/page.tsx +18 -0
- package/templates/blank/src/components/auth/email-auth-form.test.tsx +39 -0
- package/templates/blank/src/components/auth/email-auth-form.tsx +109 -0
- package/templates/blank/src/components/auth/sign-out-button.tsx +29 -0
- package/templates/blank/src/db/index.ts +16 -0
- package/templates/blank/src/db/schema/auth.ts +4 -0
- package/templates/blank/src/db/schema/index.ts +2 -0
- package/templates/blank/src/db/schema/projects.ts +17 -0
- package/templates/blank/src/db/seeds/index.ts +28 -0
- package/templates/blank/src/lib/auth-client.ts +5 -0
- package/templates/blank/src/lib/auth-session.ts +11 -0
- package/templates/blank/src/lib/auth.ts +23 -0
- package/templates/blank/src/lib/billing/index.ts +37 -0
- package/templates/blank/src/lib/billing/providers/polar.ts +80 -0
- package/templates/blank/src/lib/billing/providers/stripe.ts +77 -0
- package/templates/blank/src/lib/billing/types.ts +25 -0
- package/templates/blank/src/lib/email/index.ts +19 -0
- package/templates/blank/src/lib/email/templates.test.ts +15 -0
- package/templates/blank/src/lib/email/templates.ts +40 -0
- package/templates/blank/src/lib/env.ts +80 -0
- package/templates/blank/tsconfig.json +34 -0
- package/templates/blank/vitest.config.ts +19 -0
- package/templates/dashboard/.env.example +16 -0
- package/templates/dashboard/.github/workflows/ci.yml +34 -0
- package/templates/dashboard/.husky/pre-commit +3 -0
- package/templates/dashboard/.husky/pre-push +3 -0
- package/templates/dashboard/.prettierignore +3 -0
- package/templates/dashboard/drizzle.config.ts +16 -0
- package/templates/dashboard/eslint.config.mjs +127 -0
- package/templates/dashboard/next-env.d.ts +4 -0
- package/templates/dashboard/next.config.ts +5 -0
- package/templates/dashboard/package.json +75 -0
- package/templates/dashboard/playwright.config.ts +21 -0
- package/templates/dashboard/prettier.config.mjs +9 -0
- package/templates/dashboard/proxy.ts +36 -0
- package/templates/dashboard/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/dashboard/src/app/api/billing/checkout/route.ts +26 -0
- package/templates/dashboard/src/app/api/billing/portal/route.ts +25 -0
- package/templates/dashboard/src/app/api/email/test/route.ts +28 -0
- package/templates/dashboard/src/app/api/webhooks/polar/route.ts +5 -0
- package/templates/dashboard/src/app/api/webhooks/stripe/route.ts +5 -0
- package/templates/dashboard/src/app/billing/layout.tsx +22 -0
- package/templates/dashboard/src/app/billing/page.tsx +73 -0
- package/templates/dashboard/src/app/dashboard/layout.tsx +22 -0
- package/templates/dashboard/src/app/dashboard/page.tsx +104 -0
- package/templates/dashboard/src/app/email/layout.tsx +22 -0
- package/templates/dashboard/src/app/email/page.tsx +54 -0
- package/templates/dashboard/src/app/globals.css +1357 -0
- package/templates/dashboard/src/app/layout.tsx +25 -0
- package/templates/dashboard/src/app/page.tsx +154 -0
- package/templates/dashboard/src/app/settings/layout.tsx +22 -0
- package/templates/dashboard/src/app/settings/page.tsx +85 -0
- package/templates/dashboard/src/app/sign-in/page.tsx +47 -0
- package/templates/dashboard/src/app/sign-up/page.tsx +47 -0
- package/templates/dashboard/src/components/auth/email-auth-form.test.tsx +39 -0
- package/templates/dashboard/src/components/auth/email-auth-form.tsx +160 -0
- package/templates/dashboard/src/components/auth/sign-out-button.tsx +29 -0
- package/templates/dashboard/src/components/dashboard/shell.tsx +158 -0
- package/templates/dashboard/src/db/index.ts +16 -0
- package/templates/dashboard/src/db/schema/auth.ts +4 -0
- package/templates/dashboard/src/db/schema/index.ts +2 -0
- package/templates/dashboard/src/db/schema/projects.ts +17 -0
- package/templates/dashboard/src/db/seeds/index.ts +28 -0
- package/templates/dashboard/src/lib/auth-client.ts +5 -0
- package/templates/dashboard/src/lib/auth-session.ts +11 -0
- package/templates/dashboard/src/lib/auth.ts +41 -0
- package/templates/dashboard/src/lib/billing/index.ts +37 -0
- package/templates/dashboard/src/lib/billing/providers/polar.ts +80 -0
- package/templates/dashboard/src/lib/billing/providers/stripe.ts +77 -0
- package/templates/dashboard/src/lib/billing/types.ts +25 -0
- package/templates/dashboard/src/lib/email/index.ts +19 -0
- package/templates/dashboard/src/lib/email/templates.test.ts +15 -0
- package/templates/dashboard/src/lib/email/templates.ts +40 -0
- package/templates/dashboard/src/lib/env.ts +88 -0
- package/templates/dashboard/tsconfig.json +34 -0
- package/templates/dashboard/vitest.config.ts +19 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
async function readJson(filePath) {
|
|
5
|
+
const content = await readFile(filePath, "utf8");
|
|
6
|
+
return JSON.parse(content);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function loadModule(repoRoot, moduleName) {
|
|
10
|
+
const modulePath = path.join(repoRoot, "modules", moduleName, "module.json");
|
|
11
|
+
return readJson(modulePath);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getBillingProviderFromDefinitions(moduleDefinitions) {
|
|
15
|
+
const providers = new Set();
|
|
16
|
+
|
|
17
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
18
|
+
for (const provider of moduleDefinition.template?.billingProviders ?? []) {
|
|
19
|
+
providers.add(provider);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (providers.has("stripe") && providers.has("polar")) {
|
|
24
|
+
return "both";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (providers.has("stripe")) {
|
|
28
|
+
return "stripe";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (providers.has("polar")) {
|
|
32
|
+
return "polar";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return "none";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getEmailProviderFromDefinitions(moduleDefinitions) {
|
|
39
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
40
|
+
if (moduleDefinition.template?.emailProvider) {
|
|
41
|
+
return moduleDefinition.template.emailProvider;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return "none";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getPackageDependenciesFromDefinitions(moduleDefinitions) {
|
|
49
|
+
const dependencyLines = [];
|
|
50
|
+
|
|
51
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
52
|
+
dependencyLines.push(...(moduleDefinition.template?.packageDependencies ?? []));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return dependencyLines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getBillingImportsFromDefinitions(moduleDefinitions) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
|
|
61
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
62
|
+
lines.push(...(moduleDefinition.template?.billing?.imports ?? []));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getBillingFactoriesFromDefinitions(moduleDefinitions) {
|
|
69
|
+
const lines = [];
|
|
70
|
+
|
|
71
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
72
|
+
lines.push(...(moduleDefinition.template?.billing?.factories ?? []));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getStripeWebhookRouteBody(moduleDefinitions) {
|
|
79
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
80
|
+
if (moduleDefinition.template?.billing?.stripeWebhookRouteBody) {
|
|
81
|
+
return moduleDefinition.template.billing.stripeWebhookRouteBody;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return " return new Response(\"Stripe billing is not enabled for this project.\", { status: 404 });";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getPolarWebhookRouteBody(moduleDefinitions) {
|
|
89
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
90
|
+
if (moduleDefinition.template?.billing?.polarWebhookRouteBody) {
|
|
91
|
+
return moduleDefinition.template.billing.polarWebhookRouteBody;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return " return new Response(\"Polar billing is not enabled for this project.\", { status: 404 });";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getStripeWebhookRouteImport(moduleDefinitions) {
|
|
99
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
100
|
+
if (moduleDefinition.template?.billing?.stripeWebhookRouteBody) {
|
|
101
|
+
return 'import { getBillingProvider } from "@/lib/billing";';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getStripeWebhookRouteSignature(moduleDefinitions) {
|
|
109
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
110
|
+
if (moduleDefinition.template?.billing?.stripeWebhookRouteBody) {
|
|
111
|
+
return "request: Request";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return "_request: Request";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getPolarWebhookRouteImport(moduleDefinitions) {
|
|
119
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
120
|
+
if (moduleDefinition.template?.billing?.polarWebhookRouteBody) {
|
|
121
|
+
return 'import { getBillingProvider } from "@/lib/billing";';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getPolarWebhookRouteSignature(moduleDefinitions) {
|
|
129
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
130
|
+
if (moduleDefinition.template?.billing?.polarWebhookRouteBody) {
|
|
131
|
+
return "request: Request";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return "_request: Request";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getEmailImportsFromDefinitions(moduleDefinitions) {
|
|
139
|
+
const lines = [];
|
|
140
|
+
|
|
141
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
142
|
+
lines.push(...(moduleDefinition.template?.email?.imports ?? []));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getEmailEnvImport(moduleDefinitions) {
|
|
149
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
150
|
+
if (moduleDefinition.template?.email?.setup) {
|
|
151
|
+
return 'import { env } from "@/lib/env";';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getEmailSetupFromDefinitions(moduleDefinitions) {
|
|
159
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
160
|
+
if (moduleDefinition.template?.email?.setup) {
|
|
161
|
+
return moduleDefinition.template.email.setup;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getEmailFromAddressHelper(moduleDefinitions) {
|
|
169
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
170
|
+
if (moduleDefinition.template?.email?.setup) {
|
|
171
|
+
return `function getFromAddress() {
|
|
172
|
+
if (!env.EMAIL_FROM) {
|
|
173
|
+
throw new Error("EMAIL_FROM is required for email sending.");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return env.EMAIL_FROM;
|
|
177
|
+
}`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getEmailSendImplementation(moduleDefinitions) {
|
|
185
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
186
|
+
if (moduleDefinition.template?.email?.sendImplementation) {
|
|
187
|
+
return moduleDefinition.template.email.sendImplementation;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return ' return { skipped: true as const, provider: "none" as const };';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getEmailSendSignature(moduleDefinitions) {
|
|
195
|
+
for (const moduleDefinition of moduleDefinitions) {
|
|
196
|
+
if (moduleDefinition.template?.email?.sendImplementation) {
|
|
197
|
+
return "input";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return "_input";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function buildModuleTokenReplacements(repoRoot, scaffoldPlan) {
|
|
205
|
+
const replacements = {};
|
|
206
|
+
const moduleDefinitions = [];
|
|
207
|
+
|
|
208
|
+
for (const moduleName of scaffoldPlan.modules) {
|
|
209
|
+
const moduleDefinition = await loadModule(repoRoot, moduleName);
|
|
210
|
+
moduleDefinitions.push(moduleDefinition);
|
|
211
|
+
|
|
212
|
+
for (const [token, value] of Object.entries(
|
|
213
|
+
moduleDefinition.template?.tokenReplacements ?? {}
|
|
214
|
+
)) {
|
|
215
|
+
replacements[token] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
replacements.__BILLING_PROVIDER__ = getBillingProviderFromDefinitions(moduleDefinitions);
|
|
220
|
+
replacements.__EMAIL_PROVIDER__ = getEmailProviderFromDefinitions(moduleDefinitions);
|
|
221
|
+
replacements.__PROVIDER_PACKAGE_DEPENDENCIES__ =
|
|
222
|
+
getPackageDependenciesFromDefinitions(moduleDefinitions);
|
|
223
|
+
replacements.__BILLING_PROVIDER_IMPORTS__ = getBillingImportsFromDefinitions(moduleDefinitions);
|
|
224
|
+
replacements.__BILLING_PROVIDER_FACTORIES__ =
|
|
225
|
+
getBillingFactoriesFromDefinitions(moduleDefinitions);
|
|
226
|
+
replacements.__STRIPE_WEBHOOK_ROUTE_IMPORT__ =
|
|
227
|
+
getStripeWebhookRouteImport(moduleDefinitions);
|
|
228
|
+
replacements.__STRIPE_WEBHOOK_ROUTE_SIGNATURE__ =
|
|
229
|
+
getStripeWebhookRouteSignature(moduleDefinitions);
|
|
230
|
+
replacements.__STRIPE_WEBHOOK_ROUTE_BODY__ =
|
|
231
|
+
getStripeWebhookRouteBody(moduleDefinitions);
|
|
232
|
+
replacements.__POLAR_WEBHOOK_ROUTE_IMPORT__ =
|
|
233
|
+
getPolarWebhookRouteImport(moduleDefinitions);
|
|
234
|
+
replacements.__POLAR_WEBHOOK_ROUTE_SIGNATURE__ =
|
|
235
|
+
getPolarWebhookRouteSignature(moduleDefinitions);
|
|
236
|
+
replacements.__POLAR_WEBHOOK_ROUTE_BODY__ = getPolarWebhookRouteBody(moduleDefinitions);
|
|
237
|
+
replacements.__EMAIL_PROVIDER_IMPORTS__ = getEmailImportsFromDefinitions(moduleDefinitions);
|
|
238
|
+
replacements.__EMAIL_ENV_IMPORT__ = getEmailEnvImport(moduleDefinitions);
|
|
239
|
+
replacements.__EMAIL_PROVIDER_SETUP__ = getEmailSetupFromDefinitions(moduleDefinitions);
|
|
240
|
+
replacements.__EMAIL_FROM_ADDRESS_HELPER__ =
|
|
241
|
+
getEmailFromAddressHelper(moduleDefinitions);
|
|
242
|
+
replacements.__EMAIL_SEND_SIGNATURE__ = getEmailSendSignature(moduleDefinitions);
|
|
243
|
+
replacements.__EMAIL_SEND_IMPLEMENTATION__ =
|
|
244
|
+
getEmailSendImplementation(moduleDefinitions);
|
|
245
|
+
|
|
246
|
+
return replacements;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function buildModuleFileOverlays(repoRoot, scaffoldPlan) {
|
|
250
|
+
const overlays = [];
|
|
251
|
+
|
|
252
|
+
for (const moduleName of scaffoldPlan.modules) {
|
|
253
|
+
const moduleDefinition = await loadModule(repoRoot, moduleName);
|
|
254
|
+
|
|
255
|
+
for (const relativeFilePath of moduleDefinition.template?.copyFiles ?? []) {
|
|
256
|
+
overlays.push({
|
|
257
|
+
source: path.join(repoRoot, "modules", moduleName, "files", relativeFilePath),
|
|
258
|
+
destination: relativeFilePath
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return overlays;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function buildModulePruneList(scaffoldPlan) {
|
|
267
|
+
const filesToRemove = [];
|
|
268
|
+
const moduleSet = new Set(scaffoldPlan.modules);
|
|
269
|
+
|
|
270
|
+
if (!moduleSet.has("billing-stripe")) {
|
|
271
|
+
filesToRemove.push("src/lib/billing/providers/stripe.ts");
|
|
272
|
+
filesToRemove.push("src/app/api/webhooks/stripe/route.ts");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!moduleSet.has("billing-polar")) {
|
|
276
|
+
filesToRemove.push("src/lib/billing/providers/polar.ts");
|
|
277
|
+
filesToRemove.push("src/app/api/webhooks/polar/route.ts");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return filesToRemove;
|
|
281
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
async function readJson(filePath) {
|
|
5
|
+
const content = await readFile(filePath, "utf8");
|
|
6
|
+
return JSON.parse(content);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function loadPreset(repoRoot, presetName) {
|
|
10
|
+
const presetPath = path.join(repoRoot, "presets", `${presetName}.json`);
|
|
11
|
+
return readJson(presetPath);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function loadModule(repoRoot, moduleName) {
|
|
15
|
+
const modulePath = path.join(repoRoot, "modules", moduleName, "module.json");
|
|
16
|
+
return readJson(modulePath);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mapDatabaseModule(databaseDriver) {
|
|
20
|
+
return databaseDriver === "postgres.js" ? "db-postgresjs" : "db-pg";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mapBillingModules(billingProvider) {
|
|
24
|
+
if (billingProvider === "stripe") {
|
|
25
|
+
return ["billing-stripe"];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (billingProvider === "polar") {
|
|
29
|
+
return ["billing-polar"];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (billingProvider === "both") {
|
|
33
|
+
return ["billing-stripe", "billing-polar"];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mapEmailModules(emailProvider) {
|
|
40
|
+
if (emailProvider === "resend") {
|
|
41
|
+
return ["email-resend"];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mapAuthModules(authMode) {
|
|
48
|
+
if (authMode === "email-password+github") {
|
|
49
|
+
return ["auth-github"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (authMode === "email-password+google") {
|
|
53
|
+
return ["auth-google"];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (authMode === "email-password+github+google") {
|
|
57
|
+
return ["auth-github", "auth-google"];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function mapDeployModules(deployTarget) {
|
|
64
|
+
if (deployTarget === "docker") {
|
|
65
|
+
return ["deploy-docker"];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mapAiDxModules(aiTools) {
|
|
72
|
+
if (!aiTools || aiTools.length === 0) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const modules = ["ai-dx"];
|
|
77
|
+
const toolToModule = {
|
|
78
|
+
cursor: "ai-dx-cursor",
|
|
79
|
+
claude: "ai-dx-claude",
|
|
80
|
+
gemini: "ai-dx-gemini"
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
for (const tool of aiTools) {
|
|
84
|
+
if (toolToModule[tool]) {
|
|
85
|
+
modules.push(toolToModule[tool]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return modules;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function collectModuleGraph(repoRoot, initialModuleNames) {
|
|
93
|
+
const visited = new Set();
|
|
94
|
+
const modules = new Map();
|
|
95
|
+
|
|
96
|
+
async function visit(moduleName) {
|
|
97
|
+
if (visited.has(moduleName)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
visited.add(moduleName);
|
|
102
|
+
const moduleDefinition = await loadModule(repoRoot, moduleName);
|
|
103
|
+
modules.set(moduleName, moduleDefinition);
|
|
104
|
+
|
|
105
|
+
for (const dependency of moduleDefinition.dependsOn ?? []) {
|
|
106
|
+
await visit(dependency);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const moduleName of initialModuleNames) {
|
|
111
|
+
await visit(moduleName);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return modules;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateConflicts(modules) {
|
|
118
|
+
for (const moduleDefinition of modules.values()) {
|
|
119
|
+
for (const conflict of moduleDefinition.conflictsWith ?? []) {
|
|
120
|
+
if (modules.has(conflict)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Module conflict: ${moduleDefinition.name} cannot be combined with ${conflict}.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function sortModules(modules) {
|
|
130
|
+
const sorted = [];
|
|
131
|
+
const visited = new Set();
|
|
132
|
+
|
|
133
|
+
function visit(moduleName) {
|
|
134
|
+
if (visited.has(moduleName)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
visited.add(moduleName);
|
|
139
|
+
const moduleDefinition = modules.get(moduleName);
|
|
140
|
+
|
|
141
|
+
for (const dependency of moduleDefinition.dependsOn ?? []) {
|
|
142
|
+
visit(dependency);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
sorted.push(moduleName);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const moduleName of modules.keys()) {
|
|
149
|
+
visit(moduleName);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return sorted;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function resolveScaffoldPlan(repoRoot, options) {
|
|
156
|
+
const preset = await loadPreset(repoRoot, options.template);
|
|
157
|
+
const initialModuleNames = [
|
|
158
|
+
...(preset.modules ?? []),
|
|
159
|
+
mapDatabaseModule(options.databaseDriver),
|
|
160
|
+
...mapAuthModules(options.authMode),
|
|
161
|
+
...mapBillingModules(options.billingProvider),
|
|
162
|
+
...mapEmailModules(options.emailProvider),
|
|
163
|
+
...mapDeployModules(options.deployTarget),
|
|
164
|
+
...mapAiDxModules(options.aiTools)
|
|
165
|
+
];
|
|
166
|
+
const uniqueModuleNames = [...new Set(initialModuleNames)];
|
|
167
|
+
const modules = await collectModuleGraph(repoRoot, uniqueModuleNames);
|
|
168
|
+
|
|
169
|
+
validateConflicts(modules);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
base: preset.base,
|
|
173
|
+
preset: preset.name,
|
|
174
|
+
template: preset.template,
|
|
175
|
+
overrideFiles: preset.overrideFiles ?? [],
|
|
176
|
+
tokenReplacements: preset.tokenReplacements ?? {},
|
|
177
|
+
modules: sortModules(modules)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Modules
|
|
2
|
+
|
|
3
|
+
This directory contains the first module registry for Skit.
|
|
4
|
+
|
|
5
|
+
Current status:
|
|
6
|
+
|
|
7
|
+
- modules are resolved and written into `skit.json`
|
|
8
|
+
- generation still uses the verified template flow
|
|
9
|
+
- modules can now contribute token replacements, pruning rules, and file overlays
|
|
10
|
+
- `dashboard-shell` is the first module that contributes actual app files, currently `/settings` and its dashboard visual theme layer
|
|
11
|
+
|
|
12
|
+
The first module slice is intentionally small:
|
|
13
|
+
|
|
14
|
+
- `quality-baseline`
|
|
15
|
+
- `testing-baseline`
|
|
16
|
+
- `auth-core`
|
|
17
|
+
- `db-pg`
|
|
18
|
+
- `db-postgresjs`
|
|
19
|
+
- `billing-stripe`
|
|
20
|
+
- `billing-polar`
|
|
21
|
+
- `email-resend`
|
|
22
|
+
- `dashboard-shell`
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
> Generated by Skit. Customize freely — this file is yours.
|
|
4
|
+
|
|
5
|
+
## Tech Stack
|
|
6
|
+
|
|
7
|
+
- **Framework**: Next.js 16 (App Router, React 19, server components by default)
|
|
8
|
+
- **Language**: TypeScript 5.9 (strict mode, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`)
|
|
9
|
+
- **Database**: Drizzle ORM 0.44 + PostgreSQL via `__DATABASE_DRIVER__`
|
|
10
|
+
- **Auth**: Better Auth 1.3 (email/password, session cookies, edge middleware)
|
|
11
|
+
- **Billing**: provider abstraction (`__BILLING_PROVIDER__`) — see `src/lib/billing/`
|
|
12
|
+
- **Email**: `__EMAIL_PROVIDER__` — see `src/lib/email/`
|
|
13
|
+
- **Testing**: Vitest 3.2 (unit, jsdom) + Playwright 1.55 (e2e)
|
|
14
|
+
- **Linting**: ESLint 9 flat config + Prettier (double quotes, no trailing commas, 88 chars)
|
|
15
|
+
- **CI**: GitHub Actions — lint, typecheck, test, build
|
|
16
|
+
|
|
17
|
+
## Next.js Docs
|
|
18
|
+
|
|
19
|
+
Before writing Next.js code, read the bundled docs at `node_modules/next/dist/docs/`. Your training data may be outdated — these docs match the installed version.
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm dev # start dev server
|
|
25
|
+
pnpm build # production build
|
|
26
|
+
pnpm lint # eslint (zero warnings allowed)
|
|
27
|
+
pnpm lint:fix # eslint --fix
|
|
28
|
+
pnpm typecheck # tsc --noEmit
|
|
29
|
+
pnpm test # vitest run
|
|
30
|
+
pnpm test:watch # vitest watch mode
|
|
31
|
+
pnpm test:e2e # playwright (starts dev server)
|
|
32
|
+
pnpm format # prettier --write
|
|
33
|
+
pnpm check # lint + typecheck + test in sequence
|
|
34
|
+
pnpm db:generate # drizzle-kit generate migration
|
|
35
|
+
pnpm db:migrate # drizzle-kit migrate
|
|
36
|
+
pnpm db:push # drizzle-kit push (skip migrations)
|
|
37
|
+
pnpm db:studio # drizzle-kit studio (GUI)
|
|
38
|
+
pnpm db:seed # run seed script
|
|
39
|
+
pnpm auth:generate # regenerate Better Auth schema
|
|
40
|
+
pnpm auth:secret # generate a new auth secret
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Verification
|
|
44
|
+
|
|
45
|
+
Always run after making changes:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpm typecheck && pnpm lint:fix && pnpm format
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## MUST DO
|
|
52
|
+
|
|
53
|
+
- Use `env` from `@/lib/env` for all environment variables — never `process.env`
|
|
54
|
+
- Use server components by default; add `"use client"` only when hooks or browser APIs are needed
|
|
55
|
+
- Keep route files thin — fetch data in server component, delegate logic to `src/lib/`
|
|
56
|
+
- Use `getSession()` from `@/lib/auth-session` for server-side session access
|
|
57
|
+
- Use `authClient` from `@/lib/auth-client` for client-side auth operations
|
|
58
|
+
- Use `type` imports: `import { type Foo }` (enforced by ESLint)
|
|
59
|
+
- Run `pnpm typecheck` after making changes
|
|
60
|
+
- Add new env vars to three places: Zod schema + parse block in `src/lib/env.ts`, and `.env.example`
|
|
61
|
+
|
|
62
|
+
## MUST NOT DO
|
|
63
|
+
|
|
64
|
+
- Never access `process.env` outside `src/lib/env.ts` and `drizzle.config.ts` (ESLint enforces this)
|
|
65
|
+
- Never import from `@/db` or `server-only` modules in client components
|
|
66
|
+
- Never use the `any` type
|
|
67
|
+
- Never commit `.env` files or secrets
|
|
68
|
+
- Never skip the pre-commit hook (`pnpm lint-staged && pnpm typecheck`)
|
|
69
|
+
|
|
70
|
+
## Where to Find Things
|
|
71
|
+
|
|
72
|
+
| Area | Location |
|
|
73
|
+
|------|----------|
|
|
74
|
+
| App routes | `src/app/` |
|
|
75
|
+
| Auth config | `src/lib/auth.ts` |
|
|
76
|
+
| Auth client | `src/lib/auth-client.ts` |
|
|
77
|
+
| Session helper | `src/lib/auth-session.ts` |
|
|
78
|
+
| Database connection | `src/db/index.ts` |
|
|
79
|
+
| DB schema | `src/db/schema/` |
|
|
80
|
+
| DB migrations | `src/db/migrations/` |
|
|
81
|
+
| DB seeds | `src/db/seeds/index.ts` |
|
|
82
|
+
| Billing providers | `src/lib/billing/` |
|
|
83
|
+
| Email service | `src/lib/email/` |
|
|
84
|
+
| Email templates | `src/lib/email/templates.ts` |
|
|
85
|
+
| Env validation | `src/lib/env.ts` |
|
|
86
|
+
| Route protection | `proxy.ts` (middleware) |
|
|
87
|
+
| ESLint config | `eslint.config.mjs` |
|
|
88
|
+
| Prettier config | `prettier.config.mjs` |
|
|
89
|
+
| CI workflow | `.github/workflows/ci.yml` |
|
|
90
|
+
| Unit tests | `src/**/*.test.{ts,tsx}` |
|
|
91
|
+
| E2E tests | `tests/e2e/` |
|
|
92
|
+
|
|
93
|
+
## Code Patterns
|
|
94
|
+
|
|
95
|
+
### Adding a new page
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
src/app/my-feature/page.tsx ← server component, thin
|
|
99
|
+
src/lib/my-feature.ts ← business logic
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Adding a new DB table
|
|
103
|
+
|
|
104
|
+
1. Create schema file: `src/db/schema/my-table.ts`
|
|
105
|
+
2. Re-export from `src/db/schema/index.ts`
|
|
106
|
+
3. Run `pnpm db:generate` then `pnpm db:migrate`
|
|
107
|
+
|
|
108
|
+
### Adding a protected route
|
|
109
|
+
|
|
110
|
+
Edit `proxy.ts` — add the path to both `__PROTECTED_ROUTE_CONDITION__` and `config.matcher`.
|
|
111
|
+
|
|
112
|
+
### Adding a new env var
|
|
113
|
+
|
|
114
|
+
1. Add Zod field in `src/lib/env.ts` schema
|
|
115
|
+
2. Add `process.env.MY_VAR` in `parseEnv()` block
|
|
116
|
+
3. Add default value in `.env.example`
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
> Generated by Skit. Customize freely.
|
|
4
|
+
|
|
5
|
+
## Directory Tree
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
├── proxy.ts # Edge middleware — route protection
|
|
9
|
+
├── drizzle.config.ts # Drizzle Kit config
|
|
10
|
+
├── eslint.config.mjs # ESLint 9 flat config
|
|
11
|
+
├── prettier.config.mjs # Prettier config
|
|
12
|
+
├── vitest.config.ts # Vitest config
|
|
13
|
+
├── playwright.config.ts # Playwright E2E config
|
|
14
|
+
├── .env.example # Environment variable template
|
|
15
|
+
├── .github/workflows/ci.yml # CI pipeline
|
|
16
|
+
│
|
|
17
|
+
├── src/
|
|
18
|
+
│ ├── app/ # Next.js App Router pages
|
|
19
|
+
│ │ ├── layout.tsx # Root layout
|
|
20
|
+
│ │ ├── page.tsx # Landing / home page
|
|
21
|
+
│ │ ├── loading.tsx # Global loading skeleton
|
|
22
|
+
│ │ ├── error.tsx # Global error boundary
|
|
23
|
+
│ │ ├── not-found.tsx # 404 page
|
|
24
|
+
│ │ ├── sign-in/ # Sign-in page
|
|
25
|
+
│ │ ├── sign-up/ # Sign-up page
|
|
26
|
+
│ │ ├── dashboard/ # Dashboard page
|
|
27
|
+
│ │ ├── billing/ # Billing page
|
|
28
|
+
│ │ ├── email/ # Email test page
|
|
29
|
+
│ │ └── api/
|
|
30
|
+
│ │ ├── auth/[...all]/ # Better Auth catch-all handler
|
|
31
|
+
│ │ ├── billing/ # Checkout + portal API routes
|
|
32
|
+
│ │ ├── email/ # Email test route
|
|
33
|
+
│ │ └── webhooks/ # Stripe / Polar webhook handlers
|
|
34
|
+
│ │
|
|
35
|
+
│ ├── components/ # Shared React components
|
|
36
|
+
│ │ └── auth/ # Auth form, sign-out button
|
|
37
|
+
│ │
|
|
38
|
+
│ ├── db/
|
|
39
|
+
│ │ ├── index.ts # Database connection (Drizzle + driver)
|
|
40
|
+
│ │ ├── schema/ # Drizzle table definitions
|
|
41
|
+
│ │ │ ├── index.ts # Re-exports all schemas
|
|
42
|
+
│ │ │ ├── auth.ts # Better Auth tables
|
|
43
|
+
│ │ │ └── projects.ts # Starter project table
|
|
44
|
+
│ │ ├── migrations/ # Generated SQL migrations
|
|
45
|
+
│ │ └── seeds/ # Seed scripts
|
|
46
|
+
│ │
|
|
47
|
+
│ └── lib/
|
|
48
|
+
│ ├── env.ts # Zod-validated env (single source of truth)
|
|
49
|
+
│ ├── auth.ts # Better Auth server config
|
|
50
|
+
│ ├── auth-client.ts # Client-side auth (useSession, signIn, etc.)
|
|
51
|
+
│ ├── auth-session.ts # Server-side getSession() helper
|
|
52
|
+
│ ├── billing/
|
|
53
|
+
│ │ ├── index.ts # Provider factory
|
|
54
|
+
│ │ ├── types.ts # BillingProvider interface
|
|
55
|
+
│ │ └── providers/ # Stripe, Polar implementations
|
|
56
|
+
│ └── email/
|
|
57
|
+
│ ├── index.ts # sendEmail function
|
|
58
|
+
│ └── templates.ts # Email HTML templates
|
|
59
|
+
│
|
|
60
|
+
└── tests/
|
|
61
|
+
└── e2e/ # Playwright E2E tests
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Request Flow
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Browser
|
|
68
|
+
│
|
|
69
|
+
▼
|
|
70
|
+
proxy.ts (Edge Middleware)
|
|
71
|
+
│ checks session cookie
|
|
72
|
+
│ redirects unauthenticated users to /sign-in
|
|
73
|
+
▼
|
|
74
|
+
src/app/**/page.tsx (Server Component)
|
|
75
|
+
│ calls getSession() for auth context
|
|
76
|
+
│ delegates to src/lib/* for business logic
|
|
77
|
+
▼
|
|
78
|
+
src/lib/* (Business Logic)
|
|
79
|
+
│ uses env from src/lib/env.ts
|
|
80
|
+
│ uses db from src/db/index.ts
|
|
81
|
+
▼
|
|
82
|
+
src/db/ (Drizzle ORM → PostgreSQL)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Key Boundaries
|
|
86
|
+
|
|
87
|
+
- **`src/lib/env.ts`** is the only file that may read `process.env`. All other files import `env` from it.
|
|
88
|
+
- **`src/db/`** is server-only. Never import it in client components.
|
|
89
|
+
- **`src/lib/auth.ts`** is server-only. Use `src/lib/auth-client.ts` on the client.
|
|
90
|
+
- **`proxy.ts`** runs at the edge. It checks session cookies — no DB access.
|
|
91
|
+
- **`src/lib/billing/types.ts`** defines the provider interface. Implementations live in `providers/`.
|
|
92
|
+
|
|
93
|
+
## Extension Points
|
|
94
|
+
|
|
95
|
+
| Want to... | Do this |
|
|
96
|
+
|------------|---------|
|
|
97
|
+
| Add a page | Create `src/app/my-page/page.tsx` (server component) |
|
|
98
|
+
| Add a DB table | New file in `src/db/schema/`, re-export from `index.ts`, run `pnpm db:generate` |
|
|
99
|
+
| Add an API route | Create `src/app/api/my-route/route.ts` |
|
|
100
|
+
| Add a billing provider | Implement `BillingProvider` in `src/lib/billing/providers/` |
|
|
101
|
+
| Add an email template | Add factory in `src/lib/email/templates.ts` |
|
|
102
|
+
| Protect a route | Update condition + matcher in `proxy.ts` |
|
|
103
|
+
| Add an env var | Schema + parse in `src/lib/env.ts`, default in `.env.example` |
|