@getjack/jack 0.1.19 → 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 (102) hide show
  1. package/package.json +1 -1
  2. package/src/commands/down.ts +11 -1
  3. package/src/commands/init.ts +19 -6
  4. package/src/lib/agents.ts +3 -1
  5. package/src/lib/auth/ensure-auth.test.ts +3 -3
  6. package/src/lib/control-plane.ts +15 -1
  7. package/src/lib/deploy-upload.ts +26 -1
  8. package/src/lib/hooks.ts +232 -1
  9. package/src/lib/managed-deploy.ts +13 -6
  10. package/src/lib/progress.ts +76 -5
  11. package/src/lib/project-list.ts +6 -1
  12. package/src/lib/project-operations.ts +14 -0
  13. package/src/lib/project-resolver.ts +1 -1
  14. package/src/lib/zip-packager.ts +36 -7
  15. package/src/templates/index.ts +1 -1
  16. package/src/templates/types.ts +16 -0
  17. package/templates/CLAUDE.md +103 -0
  18. package/templates/miniapp/.jack.json +1 -3
  19. package/templates/saas/.jack.json +154 -0
  20. package/templates/saas/AGENTS.md +333 -0
  21. package/templates/saas/bun.lock +925 -0
  22. package/templates/saas/components.json +21 -0
  23. package/templates/saas/index.html +12 -0
  24. package/templates/saas/package.json +75 -0
  25. package/templates/saas/public/icon.png +0 -0
  26. package/templates/saas/public/og.png +0 -0
  27. package/templates/saas/schema.sql +73 -0
  28. package/templates/saas/src/auth.ts +77 -0
  29. package/templates/saas/src/client/App.tsx +63 -0
  30. package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
  31. package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
  32. package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
  33. package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
  34. package/templates/saas/src/client/components/ui/alert.tsx +60 -0
  35. package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
  36. package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
  37. package/templates/saas/src/client/components/ui/badge.tsx +39 -0
  38. package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
  39. package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
  40. package/templates/saas/src/client/components/ui/button.tsx +60 -0
  41. package/templates/saas/src/client/components/ui/card.tsx +75 -0
  42. package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
  43. package/templates/saas/src/client/components/ui/chart.tsx +326 -0
  44. package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
  45. package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
  46. package/templates/saas/src/client/components/ui/command.tsx +159 -0
  47. package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
  48. package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
  49. package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
  50. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
  51. package/templates/saas/src/client/components/ui/empty.tsx +94 -0
  52. package/templates/saas/src/client/components/ui/field.tsx +232 -0
  53. package/templates/saas/src/client/components/ui/form.tsx +152 -0
  54. package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
  55. package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
  56. package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
  57. package/templates/saas/src/client/components/ui/input.tsx +21 -0
  58. package/templates/saas/src/client/components/ui/item.tsx +172 -0
  59. package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
  60. package/templates/saas/src/client/components/ui/label.tsx +21 -0
  61. package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
  62. package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
  63. package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
  64. package/templates/saas/src/client/components/ui/popover.tsx +42 -0
  65. package/templates/saas/src/client/components/ui/progress.tsx +26 -0
  66. package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
  67. package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
  68. package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
  69. package/templates/saas/src/client/components/ui/select.tsx +173 -0
  70. package/templates/saas/src/client/components/ui/separator.tsx +28 -0
  71. package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
  72. package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
  73. package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
  74. package/templates/saas/src/client/components/ui/slider.tsx +58 -0
  75. package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
  76. package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
  77. package/templates/saas/src/client/components/ui/switch.tsx +28 -0
  78. package/templates/saas/src/client/components/ui/table.tsx +90 -0
  79. package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
  80. package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
  81. package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
  82. package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
  83. package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
  84. package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
  85. package/templates/saas/src/client/hooks/useAuth.ts +14 -0
  86. package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
  87. package/templates/saas/src/client/index.css +165 -0
  88. package/templates/saas/src/client/lib/auth-client.ts +7 -0
  89. package/templates/saas/src/client/lib/plans.ts +82 -0
  90. package/templates/saas/src/client/lib/utils.ts +6 -0
  91. package/templates/saas/src/client/main.tsx +15 -0
  92. package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
  93. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
  94. package/templates/saas/src/client/pages/HomePage.tsx +285 -0
  95. package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
  96. package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
  97. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
  98. package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
  99. package/templates/saas/src/index.ts +208 -0
  100. package/templates/saas/tsconfig.json +18 -0
  101. package/templates/saas/vite.config.ts +14 -0
  102. package/templates/saas/wrangler.jsonc +20 -0
@@ -363,7 +363,11 @@ async function runParallelSetup(
363
363
  installSuccess: boolean;
364
364
  remoteResult: ManagedCreateResult;
365
365
  }> {
366
+ const setupStart = Date.now();
367
+ debug("Parallel setup started", { template: options.template, usePrebuilt: options.usePrebuilt });
368
+
366
369
  // Start both operations
370
+ const installStart = Date.now();
367
371
  const installPromise = (async () => {
368
372
  const install = Bun.spawn(["bun", "install", "--prefer-offline"], {
369
373
  cwd: targetDir,
@@ -371,15 +375,22 @@ async function runParallelSetup(
371
375
  stderr: "ignore",
372
376
  });
373
377
  await install.exited;
378
+ const duration = ((Date.now() - installStart) / 1000).toFixed(1);
379
+ debug(`bun install completed in ${duration}s (exit: ${install.exitCode})`);
374
380
  if (install.exitCode !== 0) {
375
381
  throw new Error("Dependency installation failed");
376
382
  }
377
383
  return true;
378
384
  })();
379
385
 
386
+ const remoteStart = Date.now();
380
387
  const remotePromise = createManagedProjectRemote(projectName, undefined, {
381
388
  template: options.template || "hello",
382
389
  usePrebuilt: options.usePrebuilt ?? true,
390
+ }).then((result) => {
391
+ const duration = ((Date.now() - remoteStart) / 1000).toFixed(1);
392
+ debug(`Remote project created in ${duration}s (status: ${result.status})`);
393
+ return result;
383
394
  });
384
395
 
385
396
  // Report URL as soon as remote is ready (don't wait for install)
@@ -434,6 +445,9 @@ async function runParallelSetup(
434
445
  throw new Error("Unexpected state: remote result not fulfilled");
435
446
  }
436
447
 
448
+ const totalDuration = ((Date.now() - setupStart) / 1000).toFixed(1);
449
+ debug(`Parallel setup completed in ${totalDuration}s`);
450
+
437
451
  return {
438
452
  installSuccess: true,
439
453
  remoteResult: remoteResult.value,
@@ -382,7 +382,7 @@ export async function listAllProjects(): Promise<ResolvedProject[]> {
382
382
  !project.sources.controlPlane
383
383
  ) {
384
384
  project.status = "error";
385
- project.errorMessage = "Project not found in jack cloud. Run: jack unlink && jack ship";
385
+ project.errorMessage = "Project not found in jack cloud";
386
386
  }
387
387
  }
388
388
  } catch {
@@ -48,17 +48,23 @@ export interface ManifestData {
48
48
  };
49
49
  }
50
50
 
51
+ export interface ZipProgressCallback {
52
+ (current: number, total: number): void;
53
+ }
54
+
51
55
  /**
52
56
  * Creates a ZIP archive from source directory
53
57
  * @param outputPath - Absolute path for output ZIP file
54
58
  * @param sourceDir - Absolute path to directory to archive
55
59
  * @param files - Optional list of specific files to include (relative to sourceDir)
60
+ * @param onProgress - Optional callback for file-count progress updates
56
61
  * @returns Promise that resolves when ZIP is created
57
62
  */
58
63
  async function createZipArchive(
59
64
  outputPath: string,
60
65
  sourceDir: string,
61
66
  files?: string[],
67
+ onProgress?: ZipProgressCallback,
62
68
  ): Promise<void> {
63
69
  return new Promise((resolve, reject) => {
64
70
  const output = createWriteStream(outputPath);
@@ -70,13 +76,16 @@ async function createZipArchive(
70
76
  archive.pipe(output);
71
77
 
72
78
  if (files) {
73
- // Add specific files
74
- 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];
75
83
  const filePath = join(sourceDir, file);
76
84
  archive.file(filePath, { name: file });
85
+ onProgress?.(i + 1, total);
77
86
  }
78
87
  } else {
79
- // Add entire directory
88
+ // Add entire directory (no per-file progress available)
80
89
  archive.directory(sourceDir, false);
81
90
  }
82
91
 
@@ -219,18 +228,38 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
219
228
  return Object.keys(bindings).length > 0 ? bindings : undefined;
220
229
  }
221
230
 
231
+ export interface PackageOptions {
232
+ projectPath: string;
233
+ buildOutput: BuildOutput;
234
+ config?: WranglerConfig;
235
+ onProgress?: ZipProgressCallback;
236
+ }
237
+
222
238
  /**
223
239
  * Packages a built project for deployment to jack cloud
224
- * @param projectPath - Absolute path to project directory
225
- * @param buildOutput - Build output from buildProject()
226
- * @param config - Optional wrangler config to extract binding intent
240
+ * @param options - Package options including paths, build output, config, and optional progress callback
227
241
  * @returns Package result with ZIP paths and cleanup function
228
242
  */
243
+ export async function packageForDeploy(options: PackageOptions): Promise<ZipPackageResult>;
244
+ /**
245
+ * @deprecated Use options object instead
246
+ */
229
247
  export async function packageForDeploy(
230
248
  projectPath: string,
231
249
  buildOutput: BuildOutput,
232
250
  config?: WranglerConfig,
251
+ ): Promise<ZipPackageResult>;
252
+ export async function packageForDeploy(
253
+ optionsOrPath: PackageOptions | string,
254
+ buildOutputArg?: BuildOutput,
255
+ configArg?: WranglerConfig,
233
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;
234
263
  // Create temp directory for package artifacts
235
264
  const packageId = `jack-package-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
236
265
  const packageDir = join(tmpdir(), packageId);
@@ -247,7 +276,7 @@ export async function packageForDeploy(
247
276
  // 2. Create source.zip from project files (filtered)
248
277
  const projectFiles = await scanProjectFiles(projectPath);
249
278
  const sourceFiles = projectFiles.map((f) => f.path);
250
- await createZipArchive(sourceZipPath, projectPath, sourceFiles);
279
+ await createZipArchive(sourceZipPath, projectPath, sourceFiles, onProgress);
251
280
 
252
281
  // 3. Create manifest.json
253
282
  const manifest: ManifestData = {
@@ -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
+ }