@getjack/jack 0.1.17 → 0.1.20

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 (111) hide show
  1. package/package.json +2 -5
  2. package/src/commands/clone.ts +62 -40
  3. package/src/commands/down.ts +11 -1
  4. package/src/commands/init.ts +21 -2
  5. package/src/commands/services.ts +85 -4
  6. package/src/commands/sync.ts +9 -0
  7. package/src/commands/update.ts +10 -0
  8. package/src/lib/agents.ts +3 -1
  9. package/src/lib/auth/ensure-auth.test.ts +3 -3
  10. package/src/lib/control-plane.ts +77 -1
  11. package/src/lib/deploy-upload.ts +26 -1
  12. package/src/lib/hooks.ts +232 -1
  13. package/src/lib/managed-deploy.ts +37 -6
  14. package/src/lib/output.ts +21 -3
  15. package/src/lib/progress.ts +231 -0
  16. package/src/lib/project-list.ts +6 -1
  17. package/src/lib/project-operations.ts +117 -62
  18. package/src/lib/project-resolver.ts +1 -1
  19. package/src/lib/services/db-create.ts +6 -3
  20. package/src/lib/version-check.ts +14 -0
  21. package/src/lib/wrangler-config.ts +190 -0
  22. package/src/lib/zip-packager.ts +74 -7
  23. package/src/lib/zip-utils.ts +38 -0
  24. package/src/templates/index.ts +1 -1
  25. package/src/templates/types.ts +16 -0
  26. package/templates/CLAUDE.md +103 -0
  27. package/templates/miniapp/.jack.json +1 -3
  28. package/templates/saas/.jack.json +154 -0
  29. package/templates/saas/AGENTS.md +333 -0
  30. package/templates/saas/bun.lock +925 -0
  31. package/templates/saas/components.json +21 -0
  32. package/templates/saas/index.html +12 -0
  33. package/templates/saas/package.json +75 -0
  34. package/templates/saas/public/icon.png +0 -0
  35. package/templates/saas/public/og.png +0 -0
  36. package/templates/saas/schema.sql +73 -0
  37. package/templates/saas/src/auth.ts +77 -0
  38. package/templates/saas/src/client/App.tsx +63 -0
  39. package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
  40. package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
  41. package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
  42. package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
  43. package/templates/saas/src/client/components/ui/alert.tsx +60 -0
  44. package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
  45. package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
  46. package/templates/saas/src/client/components/ui/badge.tsx +39 -0
  47. package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
  48. package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
  49. package/templates/saas/src/client/components/ui/button.tsx +60 -0
  50. package/templates/saas/src/client/components/ui/card.tsx +75 -0
  51. package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
  52. package/templates/saas/src/client/components/ui/chart.tsx +326 -0
  53. package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
  54. package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
  55. package/templates/saas/src/client/components/ui/command.tsx +159 -0
  56. package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
  57. package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
  58. package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
  59. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
  60. package/templates/saas/src/client/components/ui/empty.tsx +94 -0
  61. package/templates/saas/src/client/components/ui/field.tsx +232 -0
  62. package/templates/saas/src/client/components/ui/form.tsx +152 -0
  63. package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
  64. package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
  65. package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
  66. package/templates/saas/src/client/components/ui/input.tsx +21 -0
  67. package/templates/saas/src/client/components/ui/item.tsx +172 -0
  68. package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
  69. package/templates/saas/src/client/components/ui/label.tsx +21 -0
  70. package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
  71. package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
  72. package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
  73. package/templates/saas/src/client/components/ui/popover.tsx +42 -0
  74. package/templates/saas/src/client/components/ui/progress.tsx +26 -0
  75. package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
  76. package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
  77. package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
  78. package/templates/saas/src/client/components/ui/select.tsx +173 -0
  79. package/templates/saas/src/client/components/ui/separator.tsx +28 -0
  80. package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
  81. package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
  82. package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
  83. package/templates/saas/src/client/components/ui/slider.tsx +58 -0
  84. package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
  85. package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
  86. package/templates/saas/src/client/components/ui/switch.tsx +28 -0
  87. package/templates/saas/src/client/components/ui/table.tsx +90 -0
  88. package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
  89. package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
  90. package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
  91. package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
  92. package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
  93. package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
  94. package/templates/saas/src/client/hooks/useAuth.ts +14 -0
  95. package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
  96. package/templates/saas/src/client/index.css +165 -0
  97. package/templates/saas/src/client/lib/auth-client.ts +7 -0
  98. package/templates/saas/src/client/lib/plans.ts +82 -0
  99. package/templates/saas/src/client/lib/utils.ts +6 -0
  100. package/templates/saas/src/client/main.tsx +15 -0
  101. package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
  102. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
  103. package/templates/saas/src/client/pages/HomePage.tsx +285 -0
  104. package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
  105. package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
  106. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
  107. package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
  108. package/templates/saas/src/index.ts +208 -0
  109. package/templates/saas/tsconfig.json +18 -0
  110. package/templates/saas/vite.config.ts +14 -0
  111. package/templates/saas/wrangler.jsonc +20 -0
@@ -6,6 +6,8 @@ import { join, relative } from "node:path";
6
6
  import archiver from "archiver";
7
7
  import { type AssetManifest, computeAssetHash } from "./asset-hash.ts";
8
8
  import type { BuildOutput, WranglerConfig } from "./build-helper.ts";
9
+ import { debug } from "./debug.ts";
10
+ import { formatSize } from "./format.ts";
9
11
  import { scanProjectFiles } from "./storage/file-filter.ts";
10
12
 
11
13
  export interface ZipPackageResult {
@@ -46,17 +48,23 @@ export interface ManifestData {
46
48
  };
47
49
  }
48
50
 
51
+ export interface ZipProgressCallback {
52
+ (current: number, total: number): void;
53
+ }
54
+
49
55
  /**
50
56
  * Creates a ZIP archive from source directory
51
57
  * @param outputPath - Absolute path for output ZIP file
52
58
  * @param sourceDir - Absolute path to directory to archive
53
59
  * @param files - Optional list of specific files to include (relative to sourceDir)
60
+ * @param onProgress - Optional callback for file-count progress updates
54
61
  * @returns Promise that resolves when ZIP is created
55
62
  */
56
63
  async function createZipArchive(
57
64
  outputPath: string,
58
65
  sourceDir: string,
59
66
  files?: string[],
67
+ onProgress?: ZipProgressCallback,
60
68
  ): Promise<void> {
61
69
  return new Promise((resolve, reject) => {
62
70
  const output = createWriteStream(outputPath);
@@ -68,13 +76,16 @@ async function createZipArchive(
68
76
  archive.pipe(output);
69
77
 
70
78
  if (files) {
71
- // Add specific files
72
- for (const file of files) {
79
+ // Add specific files with progress tracking
80
+ const total = files.length;
81
+ for (let i = 0; i < files.length; i++) {
82
+ const file = files[i];
73
83
  const filePath = join(sourceDir, file);
74
84
  archive.file(filePath, { name: file });
85
+ onProgress?.(i + 1, total);
75
86
  }
76
87
  } else {
77
- // Add entire directory
88
+ // Add entire directory (no per-file progress available)
78
89
  archive.directory(sourceDir, false);
79
90
  }
80
91
 
@@ -82,6 +93,42 @@ async function createZipArchive(
82
93
  });
83
94
  }
84
95
 
96
+ /**
97
+ * Creates source.zip from project files without building.
98
+ * Used for prebuilt deployments that skip the full package flow.
99
+ * @param projectPath - Absolute path to project directory
100
+ * @returns Path to the created source.zip (caller responsible for cleanup)
101
+ */
102
+ export async function createSourceZip(projectPath: string): Promise<string> {
103
+ const packageDir = join(tmpdir(), `jack-source-${Date.now()}`);
104
+ await mkdir(packageDir, { recursive: true });
105
+
106
+ const sourceZipPath = join(packageDir, "source.zip");
107
+ const projectFiles = await scanProjectFiles(projectPath);
108
+
109
+ // Debug output for source file statistics
110
+ const totalSize = projectFiles.reduce((sum, f) => sum + f.size, 0);
111
+ const largest =
112
+ projectFiles.length > 0
113
+ ? projectFiles.reduce((max, f) => (f.size > max.size ? f : max), projectFiles[0])
114
+ : null;
115
+
116
+ debug(`Source: ${projectFiles.length} files, ${formatSize(totalSize)} uncompressed`);
117
+ if (largest) {
118
+ debug(`Largest: ${largest.path} (${formatSize(largest.size)})`);
119
+ }
120
+
121
+ const sourceFiles = projectFiles.map((f) => f.path);
122
+ await createZipArchive(sourceZipPath, projectPath, sourceFiles);
123
+
124
+ // Debug output for compression statistics
125
+ const zipStats = await stat(sourceZipPath);
126
+ const ratio = totalSize > 0 ? ((1 - zipStats.size / totalSize) * 100).toFixed(0) : 0;
127
+ debug(`Compressed: ${formatSize(zipStats.size)} (${ratio}% reduction)`);
128
+
129
+ return sourceZipPath;
130
+ }
131
+
85
132
  /**
86
133
  * Recursively collects all file paths in a directory
87
134
  */
@@ -181,18 +228,38 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
181
228
  return Object.keys(bindings).length > 0 ? bindings : undefined;
182
229
  }
183
230
 
231
+ export interface PackageOptions {
232
+ projectPath: string;
233
+ buildOutput: BuildOutput;
234
+ config?: WranglerConfig;
235
+ onProgress?: ZipProgressCallback;
236
+ }
237
+
184
238
  /**
185
239
  * Packages a built project for deployment to jack cloud
186
- * @param projectPath - Absolute path to project directory
187
- * @param buildOutput - Build output from buildProject()
188
- * @param config - Optional wrangler config to extract binding intent
240
+ * @param options - Package options including paths, build output, config, and optional progress callback
189
241
  * @returns Package result with ZIP paths and cleanup function
190
242
  */
243
+ export async function packageForDeploy(options: PackageOptions): Promise<ZipPackageResult>;
244
+ /**
245
+ * @deprecated Use options object instead
246
+ */
191
247
  export async function packageForDeploy(
192
248
  projectPath: string,
193
249
  buildOutput: BuildOutput,
194
250
  config?: WranglerConfig,
251
+ ): Promise<ZipPackageResult>;
252
+ export async function packageForDeploy(
253
+ optionsOrPath: PackageOptions | string,
254
+ buildOutputArg?: BuildOutput,
255
+ configArg?: WranglerConfig,
195
256
  ): Promise<ZipPackageResult> {
257
+ // Support both old and new signatures
258
+ const options: PackageOptions =
259
+ typeof optionsOrPath === "string"
260
+ ? { projectPath: optionsOrPath, buildOutput: buildOutputArg!, config: configArg }
261
+ : optionsOrPath;
262
+ const { projectPath, buildOutput, config, onProgress } = options;
196
263
  // Create temp directory for package artifacts
197
264
  const packageId = `jack-package-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
198
265
  const packageDir = join(tmpdir(), packageId);
@@ -209,7 +276,7 @@ export async function packageForDeploy(
209
276
  // 2. Create source.zip from project files (filtered)
210
277
  const projectFiles = await scanProjectFiles(projectPath);
211
278
  const sourceFiles = projectFiles.map((f) => f.path);
212
- await createZipArchive(sourceZipPath, projectPath, sourceFiles);
279
+ await createZipArchive(sourceZipPath, projectPath, sourceFiles, onProgress);
213
280
 
214
281
  // 3. Create manifest.json
215
282
  const manifest: ManifestData = {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Zip utility functions for extracting zip archives
3
+ */
4
+
5
+ import { mkdir, writeFile } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
+ import { unzipSync } from "fflate";
8
+
9
+ /**
10
+ * Extract a zip buffer to a target directory.
11
+ * Creates directories as needed and writes files preserving relative paths.
12
+ *
13
+ * @param zipBuffer - The zip file contents as a Buffer
14
+ * @param targetDir - The directory to extract files to
15
+ * @returns The number of files extracted
16
+ */
17
+ export async function extractZipToDirectory(zipBuffer: Buffer, targetDir: string): Promise<number> {
18
+ const unzipped = unzipSync(new Uint8Array(zipBuffer));
19
+ let fileCount = 0;
20
+
21
+ for (const [path, content] of Object.entries(unzipped)) {
22
+ // Skip directories (they end with /)
23
+ if (path.endsWith("/")) continue;
24
+
25
+ // Security: prevent path traversal by removing any .. segments
26
+ const normalizedPath = path.replace(/\.\./g, "");
27
+ const fullPath = join(targetDir, normalizedPath);
28
+
29
+ // Ensure directory exists
30
+ await mkdir(dirname(fullPath), { recursive: true });
31
+
32
+ // Write file
33
+ await writeFile(fullPath, content);
34
+ fileCount++;
35
+ }
36
+
37
+ return fileCount;
38
+ }
@@ -10,7 +10,7 @@ import type { Template } from "./types";
10
10
  // Resolve templates directory relative to this file (src/templates -> templates)
11
11
  const TEMPLATES_DIR = join(dirname(dirname(import.meta.dir)), "templates");
12
12
 
13
- export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs"];
13
+ export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs", "saas"];
14
14
 
15
15
  /**
16
16
  * Resolved template with origin tracking for lineage
@@ -11,6 +11,7 @@ export type HookAction =
11
11
  message: string;
12
12
  validate?: "json" | "accountAssociation";
13
13
  required?: boolean;
14
+ secret?: boolean; // Mask input (for sensitive values like API keys)
14
15
  successMessage?: string;
15
16
  writeJson?: {
16
17
  path: string;
@@ -31,9 +32,24 @@ export type HookAction =
31
32
  key: string;
32
33
  message?: string;
33
34
  setupUrl?: string;
35
+ onMissing?: "fail" | "prompt" | "generate";
36
+ promptMessage?: string;
37
+ generateCommand?: string;
38
+ }
39
+ | {
40
+ action: "stripe-setup";
41
+ message?: string;
42
+ plans: Array<{
43
+ name: string;
44
+ priceKey: string; // Secret key to store price ID (e.g., "STRIPE_PRO_PRICE_ID")
45
+ amount: number; // in cents
46
+ interval: "month" | "year";
47
+ description?: string;
48
+ }>;
34
49
  };
35
50
 
36
51
  export interface TemplateHooks {
52
+ preCreate?: HookAction[];
37
53
  preDeploy?: HookAction[];
38
54
  postDeploy?: HookAction[];
39
55
  }
@@ -205,6 +205,109 @@ These variables are substituted at runtime (different from template placeholders
205
205
  }
206
206
  ```
207
207
 
208
+ ### Proposed Hook Extensions
209
+
210
+ These extensions are planned to support more complex setup wizards (like SaaS templates with Stripe):
211
+
212
+ #### 1. `require` + `onMissing: "prompt"`
213
+
214
+ Currently `require` fails if a secret is missing. This extension allows prompting the user instead:
215
+
216
+ ```json
217
+ {
218
+ "action": "require",
219
+ "source": "secret",
220
+ "key": "STRIPE_SECRET_KEY",
221
+ "onMissing": "prompt",
222
+ "promptMessage": "Enter your Stripe Secret Key (sk_test_...):",
223
+ "setupUrl": "https://dashboard.stripe.com/apikeys"
224
+ }
225
+ ```
226
+
227
+ **Behavior:**
228
+ - If secret exists → continue (no change)
229
+ - If secret missing + interactive → prompt user, save to `.secrets.json`
230
+ - If secret missing + non-interactive → fail with setup instructions
231
+
232
+ #### 2. `shell` + `captureAs`
233
+
234
+ Run a command and save its output as a secret or variable:
235
+
236
+ ```json
237
+ {
238
+ "action": "shell",
239
+ "command": "stripe listen --print-secret",
240
+ "captureAs": "secret:STRIPE_WEBHOOK_SECRET",
241
+ "message": "Starting Stripe webhook listener..."
242
+ }
243
+ ```
244
+
245
+ **Use cases:**
246
+ - Capture Stripe CLI webhook signing secret
247
+ - Capture generated API keys or tokens
248
+ - Capture any CLI output needed for configuration
249
+
250
+ **`captureAs` syntax:**
251
+ - `secret:KEY_NAME` → saves to `.secrets.json`
252
+ - `var:NAME` → saves to hook variables for later hooks
253
+
254
+ #### 3. `prompt` + `saveAs`
255
+
256
+ Currently `prompt` only writes to JSON files via `writeJson`. This extension allows saving input as a secret:
257
+
258
+ ```json
259
+ {
260
+ "action": "prompt",
261
+ "message": "Enter your Stripe Webhook Secret (whsec_...):",
262
+ "saveAs": "secret:STRIPE_WEBHOOK_SECRET",
263
+ "validate": "startsWith:whsec_",
264
+ "successMessage": "Webhook secret saved"
265
+ }
266
+ ```
267
+
268
+ **Difference from `require+onMissing`:**
269
+ - `require+onMissing` checks first, prompts only if missing
270
+ - `prompt+saveAs` always prompts (for update flows or explicit input)
271
+
272
+ ### Design Principles for Hook Extensions
273
+
274
+ When extending the hook system:
275
+
276
+ 1. **Extend existing actions** - prefer `require+onMissing` over a new `requireOrPrompt` action
277
+ 2. **Reusable primitives** - `captureAs` works on any action that produces output
278
+ 3. **Consistent syntax** - `secret:KEY` pattern for writing to `.secrets.json`
279
+ 4. **Non-interactive fallback** - every interactive feature must degrade gracefully in CI/MCP
280
+
281
+ ### Example: Complex Setup Wizard
282
+
283
+ A SaaS template with Stripe might use these extensions:
284
+
285
+ ```json
286
+ {
287
+ "hooks": {
288
+ "preDeploy": [
289
+ {"action": "require", "source": "secret", "key": "BETTER_AUTH_SECRET", "onMissing": "prompt", "promptMessage": "Enter a random secret (32+ chars):"},
290
+ {"action": "require", "source": "secret", "key": "STRIPE_SECRET_KEY", "onMissing": "prompt", "promptMessage": "Enter Stripe Secret Key:", "setupUrl": "https://dashboard.stripe.com/apikeys"}
291
+ ],
292
+ "postDeploy": [
293
+ {"action": "box", "title": "Stripe Webhook Setup", "lines": ["1. Go to Stripe Dashboard → Webhooks", "2. Add endpoint: {{url}}/api/auth/stripe/webhook", "3. Select events: checkout.session.completed, customer.subscription.*"]},
294
+ {"action": "url", "url": "https://dashboard.stripe.com/webhooks/create?endpoint_url={{url}}/api/auth/stripe/webhook", "label": "Create webhook"},
295
+ {"action": "prompt", "message": "Paste webhook signing secret (whsec_...):", "saveAs": "secret:STRIPE_WEBHOOK_SECRET", "validate": "startsWith:whsec_"},
296
+ {"action": "message", "text": "Re-deploying with webhook secret..."},
297
+ {"action": "shell", "command": "jack ship --quiet"}
298
+ ]
299
+ }
300
+ }
301
+ ```
302
+
303
+ This creates a guided wizard that:
304
+ 1. Ensures auth secret exists (prompts if missing)
305
+ 2. Ensures Stripe key exists (prompts if missing, with setup link)
306
+ 3. Deploys the app
307
+ 4. Guides user through webhook setup with direct link
308
+ 5. Captures webhook secret
309
+ 6. Re-deploys with complete configuration
310
+
208
311
  ## Farcaster Miniapp Embeds
209
312
 
210
313
  When a cast includes a URL, Farcaster scrapes it for `fc:miniapp` meta tags to render a rich embed.
@@ -38,9 +38,7 @@
38
38
  {
39
39
  "action": "box",
40
40
  "title": "Deployed: {{name}}",
41
- "lines": [
42
- "{{url}}"
43
- ]
41
+ "lines": ["{{url}}"]
44
42
  },
45
43
  {
46
44
  "action": "writeJson",
@@ -0,0 +1,154 @@
1
+ {
2
+ "name": "saas",
3
+ "description": "SaaS starter (Auth + Payments + React)",
4
+ "secrets": ["STRIPE_SECRET_KEY", "BETTER_AUTH_SECRET", "STRIPE_PRO_PRICE_ID", "STRIPE_ENTERPRISE_PRICE_ID"],
5
+ "capabilities": ["db"],
6
+ "requires": ["DB"],
7
+ "intent": {
8
+ "keywords": ["saas", "subscription", "auth", "payment", "stripe", "membership"],
9
+ "examples": ["subscription app", "paid membership site", "saas product"]
10
+ },
11
+ "agentContext": {
12
+ "summary": "A SaaS starter with Better Auth authentication, Stripe payments, React + Vite frontend, and Hono API on Cloudflare Workers with D1 SQLite database.",
13
+ "full_text": "## Project Structure\n\n- `src/index.ts` - Hono API entry point with auth and payment routes\n- `src/auth.ts` - Better Auth configuration with Stripe plugin\n- `src/client/App.tsx` - React application entry point\n- `src/client/lib/auth-client.ts` - Better Auth client\n- `src/client/hooks/` - useAuth, useSubscription hooks\n- `src/client/pages/` - Page components (HomePage, DashboardPage, etc.)\n- `src/client/components/ui/` - shadcn/ui components\n- `schema.sql` - D1 database schema (user, session, account, subscription)\n- `wrangler.jsonc` - Cloudflare Workers configuration\n\n## Authentication\n\nUses Better Auth with email/password. Auth routes are handled at `/api/auth/*`.\n\n### Client-side\n```tsx\nimport { authClient } from './lib/auth-client';\n\n// Sign up\nawait authClient.signUp.email({ email, password, name });\n\n// Sign in\nawait authClient.signIn.email({ email, password });\n\n// Get session\nconst { data: session } = await authClient.getSession();\n```\n\n## Payments (Stripe)\n\nUses Better Auth Stripe plugin for subscription management.\n\n### Upgrade Flow\n```tsx\nimport { authClient } from './lib/auth-client';\n\nasync function handleUpgrade(plan: 'pro' | 'enterprise') {\n const { data, error } = await authClient.subscription.upgrade({\n plan,\n successUrl: '/dashboard?upgraded=true',\n cancelUrl: '/pricing',\n });\n\n if (data?.url) {\n window.location.href = data.url; // Redirect to Stripe Checkout\n }\n}\n```\n\n### Check Subscription\n```tsx\nconst { data: subscriptions } = await authClient.subscription.list();\nconst activeSubscription = subscriptions?.find(s =>\n s.status === 'active' || s.status === 'trialing'\n);\nconst plan = activeSubscription?.plan || 'free';\n```\n\n## Webhook Setup\n\nStripe webhooks are handled at `/api/auth/stripe/webhook`. Required events:\n- `checkout.session.completed`\n- `customer.subscription.created`\n- `customer.subscription.updated`\n- `customer.subscription.deleted`\n\n## Database Schema\n\nUses Better Auth's default schema (singular table names, camelCase columns):\n- `user` - User accounts\n- `session` - Active sessions\n- `account` - OAuth/password credentials\n- `verification` - Email verification tokens\n- `subscription` - Stripe subscriptions\n\n## Environment Variables\n\n- `BETTER_AUTH_SECRET` - Random secret for auth tokens (generate with: openssl rand -base64 32)\n- `STRIPE_SECRET_KEY` - Stripe API secret key\n- `STRIPE_WEBHOOK_SECRET` - Stripe webhook signing secret (whsec_...)\n- `STRIPE_PRO_PRICE_ID` - Stripe price ID for Pro plan\n- `STRIPE_ENTERPRISE_PRICE_ID` - Stripe price ID for Enterprise plan\n\n## Resources\n\n- [Better Auth Docs](https://www.betterauth.com/docs)\n- [Better Auth Stripe Plugin](https://www.betterauth.com/docs/plugins/stripe)\n- [Stripe Subscriptions](https://docs.stripe.com/billing/subscriptions)\n- [Hono Documentation](https://hono.dev)\n- [Cloudflare D1 Docs](https://developers.cloudflare.com/d1)"
14
+ },
15
+ "hooks": {
16
+ "preCreate": [
17
+ {
18
+ "action": "require",
19
+ "source": "secret",
20
+ "key": "STRIPE_SECRET_KEY",
21
+ "message": "Stripe API key required for payments",
22
+ "setupUrl": "https://dashboard.stripe.com/apikeys",
23
+ "onMissing": "prompt",
24
+ "promptMessage": "Enter your Stripe Secret Key (sk_test_... or sk_live_...):"
25
+ },
26
+ {
27
+ "action": "require",
28
+ "source": "secret",
29
+ "key": "BETTER_AUTH_SECRET",
30
+ "message": "Generating authentication secret...",
31
+ "onMissing": "generate",
32
+ "generateCommand": "openssl rand -base64 32"
33
+ },
34
+ {
35
+ "action": "stripe-setup",
36
+ "message": "Setting up Stripe subscription plans...",
37
+ "plans": [
38
+ {
39
+ "name": "Pro",
40
+ "priceKey": "STRIPE_PRO_PRICE_ID",
41
+ "amount": 1900,
42
+ "interval": "month",
43
+ "description": "Pro monthly subscription"
44
+ },
45
+ {
46
+ "name": "Enterprise",
47
+ "priceKey": "STRIPE_ENTERPRISE_PRICE_ID",
48
+ "amount": 9900,
49
+ "interval": "month",
50
+ "description": "Enterprise monthly subscription"
51
+ }
52
+ ]
53
+ }
54
+ ],
55
+ "preDeploy": [
56
+ {
57
+ "action": "require",
58
+ "source": "secret",
59
+ "key": "STRIPE_SECRET_KEY"
60
+ }
61
+ ],
62
+ "postDeploy": [
63
+ {
64
+ "action": "box",
65
+ "title": "Your SaaS is live!",
66
+ "lines": ["{{url}}"]
67
+ },
68
+ {
69
+ "action": "clipboard",
70
+ "text": "{{url}}/api/auth/stripe/webhook",
71
+ "message": "Webhook URL copied to clipboard"
72
+ },
73
+ {
74
+ "action": "message",
75
+ "text": ""
76
+ },
77
+ {
78
+ "action": "message",
79
+ "text": "━━━ Stripe Webhook Setup ━━━"
80
+ },
81
+ {
82
+ "action": "message",
83
+ "text": ""
84
+ },
85
+ {
86
+ "action": "message",
87
+ "text": "1. Create webhook endpoint in Stripe"
88
+ },
89
+ {
90
+ "action": "message",
91
+ "text": "2. Set endpoint URL to: {{url}}/api/auth/stripe/webhook"
92
+ },
93
+ {
94
+ "action": "message",
95
+ "text": "3. Select these events:"
96
+ },
97
+ {
98
+ "action": "message",
99
+ "text": " • checkout.session.completed"
100
+ },
101
+ {
102
+ "action": "message",
103
+ "text": " • customer.subscription.created"
104
+ },
105
+ {
106
+ "action": "message",
107
+ "text": " • customer.subscription.updated"
108
+ },
109
+ {
110
+ "action": "message",
111
+ "text": " • customer.subscription.deleted"
112
+ },
113
+ {
114
+ "action": "message",
115
+ "text": ""
116
+ },
117
+ {
118
+ "action": "url",
119
+ "url": "https://dashboard.stripe.com/webhooks",
120
+ "label": "Open Stripe webhooks",
121
+ "prompt": true
122
+ },
123
+ {
124
+ "action": "prompt",
125
+ "message": "Paste your webhook signing secret (whsec_...):",
126
+ "required": false,
127
+ "secret": true,
128
+ "successMessage": "Webhook secret saved!",
129
+ "deployAfter": true,
130
+ "deployMessage": "Deploying with webhook support...",
131
+ "writeJson": {
132
+ "path": ".secrets.json",
133
+ "set": { "STRIPE_WEBHOOK_SECRET": { "from": "input" } }
134
+ }
135
+ },
136
+ {
137
+ "action": "message",
138
+ "text": ""
139
+ },
140
+ {
141
+ "action": "message",
142
+ "text": "━━━ Next Steps ━━━"
143
+ },
144
+ {
145
+ "action": "message",
146
+ "text": "• Test card: 4242 4242 4242 4242 (any future date, any CVC)"
147
+ },
148
+ {
149
+ "action": "message",
150
+ "text": "• Customize theme: https://ui.shadcn.com/create"
151
+ }
152
+ ]
153
+ }
154
+ }