@primstack/cli 0.0.1

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 (210) hide show
  1. package/dist/generators/crud/templates/drizzle-table.ts.template +12 -0
  2. package/dist/generators/crud/templates/handlers.ts.template +136 -0
  3. package/dist/generators/crud/templates/routes.ts.template +21 -0
  4. package/dist/generators/crud/templates/schema.ts.template +20 -0
  5. package/dist/index.js +9618 -0
  6. package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/index.ts.template +79 -0
  7. package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/types.ts.template +12 -0
  8. package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/index.ts.template +62 -0
  9. package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/types.ts.template +12 -0
  10. package/dist/integrations/analytics/providers/posthog/templates/src/analytics/index.ts.template +67 -0
  11. package/dist/integrations/analytics/providers/posthog/templates/src/analytics/types.ts.template +12 -0
  12. package/dist/integrations/auth/providers/authjoy/templates/api/src/middleware/auth.ts.template +89 -0
  13. package/dist/integrations/auth/providers/authjoy/templates/api/src/routes/auth.ts.template +27 -0
  14. package/dist/integrations/auth/providers/authjoy/templates/web/src/components/AuthProvider.tsx.template +40 -0
  15. package/dist/integrations/auth/providers/authjoy/templates/web/src/hooks/use-auth.ts.template +71 -0
  16. package/dist/integrations/auth/providers/authjoy/templates/web/src/lib/auth.ts.template +59 -0
  17. package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/account.tsx.template +84 -0
  18. package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/login.tsx.template +73 -0
  19. package/dist/integrations/cache/providers/memory/templates/src/cache/index.ts.template +43 -0
  20. package/dist/integrations/cache/providers/memory/templates/src/cache/types.ts.template +3 -0
  21. package/dist/integrations/cache/providers/redis/templates/src/cache/index.ts.template +37 -0
  22. package/dist/integrations/cache/providers/redis/templates/src/cache/types.ts.template +3 -0
  23. package/dist/integrations/cache/providers/valkey/templates/src/cache/index.ts.template +38 -0
  24. package/dist/integrations/cache/providers/valkey/templates/src/cache/types.ts.template +3 -0
  25. package/dist/integrations/db/providers/postgres/templates/drizzle.config.ts.template +10 -0
  26. package/dist/integrations/db/providers/postgres/templates/src/db/index.ts.template +13 -0
  27. package/dist/integrations/db/providers/postgres/templates/src/db/migrate.ts.template +19 -0
  28. package/dist/integrations/db/providers/postgres/templates/src/db/schema/index.ts.template +1 -0
  29. package/dist/integrations/db/providers/postgres/templates/src/db/schema/users.ts.template +12 -0
  30. package/dist/integrations/db/providers/postgres/templates/src/db/seed.ts.template +28 -0
  31. package/dist/integrations/db/providers/sqlite/templates/drizzle.config.ts.template +10 -0
  32. package/dist/integrations/db/providers/sqlite/templates/src/db/index.ts.template +10 -0
  33. package/dist/integrations/db/providers/sqlite/templates/src/db/schema/index.ts.template +1 -0
  34. package/dist/integrations/db/providers/sqlite/templates/src/db/schema/users.ts.template +12 -0
  35. package/dist/integrations/db/providers/sqlite/templates/src/db/seed.ts.template +28 -0
  36. package/dist/integrations/db/providers/supabase/templates/drizzle.config.ts.template +10 -0
  37. package/dist/integrations/db/providers/supabase/templates/src/db/index.ts.template +13 -0
  38. package/dist/integrations/db/providers/supabase/templates/src/db/migrate.ts.template +19 -0
  39. package/dist/integrations/db/providers/supabase/templates/src/db/schema/index.ts.template +1 -0
  40. package/dist/integrations/db/providers/supabase/templates/src/db/schema/users.ts.template +12 -0
  41. package/dist/integrations/db/providers/supabase/templates/src/db/seed.ts.template +28 -0
  42. package/dist/integrations/db/providers/turso/templates/drizzle.config.ts.template +11 -0
  43. package/dist/integrations/db/providers/turso/templates/src/db/index.ts.template +14 -0
  44. package/dist/integrations/db/providers/turso/templates/src/db/schema/index.ts.template +1 -0
  45. package/dist/integrations/db/providers/turso/templates/src/db/schema/users.ts.template +12 -0
  46. package/dist/integrations/db/providers/turso/templates/src/db/seed.ts.template +28 -0
  47. package/dist/integrations/email/providers/nodemailer/templates/src/email/index.ts.template +24 -0
  48. package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/index.ts.template +1 -0
  49. package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/welcome.ts.template +7 -0
  50. package/dist/integrations/email/providers/nodemailer/templates/src/email/types.ts.template +7 -0
  51. package/dist/integrations/email/providers/resend/templates/src/email/index.ts.template +18 -0
  52. package/dist/integrations/email/providers/resend/templates/src/email/templates/index.ts.template +1 -0
  53. package/dist/integrations/email/providers/resend/templates/src/email/templates/welcome.ts.template +7 -0
  54. package/dist/integrations/email/providers/resend/templates/src/email/types.ts.template +7 -0
  55. package/dist/integrations/email/providers/sendgrid/templates/src/email/index.ts.template +16 -0
  56. package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/index.ts.template +1 -0
  57. package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/welcome.ts.template +7 -0
  58. package/dist/integrations/email/providers/sendgrid/templates/src/email/types.ts.template +7 -0
  59. package/dist/integrations/flags/providers/local/templates/api/src/lib/flags.ts.template +97 -0
  60. package/dist/integrations/flags/providers/local/templates/api/src/routes/flags.ts.template +36 -0
  61. package/dist/integrations/flags/providers/local/templates/flags.json.template +8 -0
  62. package/dist/integrations/flags/providers/local/templates/web/src/hooks/use-flag.ts.template +60 -0
  63. package/dist/integrations/logging/providers/axiom/templates/src/logging/index.ts.template +56 -0
  64. package/dist/integrations/logging/providers/axiom/templates/src/logging/types.ts.template +5 -0
  65. package/dist/integrations/logging/providers/pino/templates/src/logging/index.ts.template +21 -0
  66. package/dist/integrations/logging/providers/pino/templates/src/logging/types.ts.template +5 -0
  67. package/dist/integrations/logging/providers/winston/templates/src/logging/index.ts.template +30 -0
  68. package/dist/integrations/logging/providers/winston/templates/src/logging/types.ts.template +5 -0
  69. package/dist/integrations/monitor/providers/datadog/templates/src/monitor/index.ts.template +78 -0
  70. package/dist/integrations/monitor/providers/datadog/templates/src/monitor/types.ts.template +12 -0
  71. package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/index.ts.template +60 -0
  72. package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/types.ts.template +12 -0
  73. package/dist/integrations/monitor/providers/sentry/templates/src/monitor/index.ts.template +70 -0
  74. package/dist/integrations/monitor/providers/sentry/templates/src/monitor/types.ts.template +12 -0
  75. package/dist/integrations/queue/providers/bullmq/templates/src/queue/index.ts.template +56 -0
  76. package/dist/integrations/queue/providers/bullmq/templates/src/queue/types.ts.template +10 -0
  77. package/dist/integrations/queue/providers/memory/templates/src/queue/index.ts.template +73 -0
  78. package/dist/integrations/queue/providers/memory/templates/src/queue/types.ts.template +10 -0
  79. package/dist/integrations/queue/providers/pgboss/templates/src/queue/index.ts.template +34 -0
  80. package/dist/integrations/queue/providers/pgboss/templates/src/queue/types.ts.template +10 -0
  81. package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/index.ts.template +95 -0
  82. package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/types.ts.template +12 -0
  83. package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/index.ts.template +80 -0
  84. package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/types.ts.template +12 -0
  85. package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/index.ts.template +67 -0
  86. package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/types.ts.template +12 -0
  87. package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/index.ts.template +81 -0
  88. package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/types.ts.template +10 -0
  89. package/dist/integrations/schedule/providers/croner/templates/src/schedule/index.ts.template +47 -0
  90. package/dist/integrations/schedule/providers/croner/templates/src/schedule/types.ts.template +10 -0
  91. package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/index.ts.template +45 -0
  92. package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/types.ts.template +10 -0
  93. package/dist/integrations/search/providers/algolia/templates/src/search/index.ts.template +52 -0
  94. package/dist/integrations/search/providers/algolia/templates/src/search/types.ts.template +18 -0
  95. package/dist/integrations/search/providers/meilisearch/templates/src/search/index.ts.template +49 -0
  96. package/dist/integrations/search/providers/meilisearch/templates/src/search/types.ts.template +18 -0
  97. package/dist/integrations/search/providers/typesense/templates/src/search/index.ts.template +71 -0
  98. package/dist/integrations/search/providers/typesense/templates/src/search/types.ts.template +35 -0
  99. package/dist/integrations/storage/providers/local/templates/src/storage/index.ts.template +69 -0
  100. package/dist/integrations/storage/providers/local/templates/src/storage/types.ts.template +12 -0
  101. package/dist/integrations/storage/providers/r2/templates/src/storage/index.ts.template +80 -0
  102. package/dist/integrations/storage/providers/r2/templates/src/storage/types.ts.template +12 -0
  103. package/dist/integrations/storage/providers/s3/templates/src/storage/index.ts.template +78 -0
  104. package/dist/integrations/storage/providers/s3/templates/src/storage/types.ts.template +12 -0
  105. package/dist/integrations/stripe/templates/api/src/lib/stripe.ts.template +259 -0
  106. package/dist/integrations/stripe/templates/api/src/routes/stripe-webhooks.ts.template +284 -0
  107. package/dist/integrations/stripe/templates/api/stripe.config.ts.template +178 -0
  108. package/dist/integrations/stripe/templates/shared/src/pricing.ts.template +117 -0
  109. package/dist/integrations/stripe/templates/shared/src/stripe-types.ts.template +133 -0
  110. package/dist/integrations/stripe/templates/web/src/components/billing-settings.tsx.template +123 -0
  111. package/dist/integrations/stripe/templates/web/src/components/pricing-cards.tsx.template +115 -0
  112. package/dist/integrations/stripe/templates/web/src/pages/pricing.tsx.template +95 -0
  113. package/dist/templates/api/fastify/.env.example.template +7 -0
  114. package/dist/templates/api/fastify/.gitignore.template +24 -0
  115. package/dist/templates/api/fastify/package.json.template +23 -0
  116. package/dist/templates/api/fastify/src/index.ts.template +52 -0
  117. package/dist/templates/api/fastify/src/lib/env.ts.template +20 -0
  118. package/dist/templates/api/fastify/src/routes/health.ts.template +12 -0
  119. package/dist/templates/api/fastify/tsconfig.json.template +18 -0
  120. package/dist/templates/api/fastify-postgres/.env.example.template +10 -0
  121. package/dist/templates/api/fastify-postgres/drizzle.config.ts.template +10 -0
  122. package/dist/templates/api/fastify-postgres/package.json.template +16 -0
  123. package/dist/templates/api/fastify-postgres/src/db/index.ts.template +9 -0
  124. package/dist/templates/api/fastify-postgres/src/db/schema/users.ts.template +12 -0
  125. package/dist/templates/api/fastify-sqlite/.env.example.template +10 -0
  126. package/dist/templates/api/fastify-sqlite/drizzle.config.ts.template +10 -0
  127. package/dist/templates/api/fastify-sqlite/package.json.template +16 -0
  128. package/dist/templates/api/fastify-sqlite/src/db/index.ts.template +6 -0
  129. package/dist/templates/api/fastify-sqlite/src/db/schema/users.ts.template +12 -0
  130. package/dist/templates/api/fastify-supabase/.env.example.template +10 -0
  131. package/dist/templates/api/fastify-supabase/drizzle.config.ts.template +10 -0
  132. package/dist/templates/api/fastify-supabase/package.json.template +15 -0
  133. package/dist/templates/api/fastify-supabase/src/db/index.ts.template +9 -0
  134. package/dist/templates/api/fastify-supabase/src/db/schema/users.ts.template +12 -0
  135. package/dist/templates/api/fastify-turso/.env.example.template +11 -0
  136. package/dist/templates/api/fastify-turso/drizzle.config.ts.template +11 -0
  137. package/dist/templates/api/fastify-turso/package.json.template +15 -0
  138. package/dist/templates/api/fastify-turso/src/db/index.ts.template +10 -0
  139. package/dist/templates/api/fastify-turso/src/db/schema/users.ts.template +12 -0
  140. package/dist/templates/fullstack/api/.env.example.template +10 -0
  141. package/dist/templates/fullstack/api/.gitignore.template +4 -0
  142. package/dist/templates/fullstack/api/drizzle.config.ts.template +14 -0
  143. package/dist/templates/fullstack/api/package.json.template +33 -0
  144. package/dist/templates/fullstack/api/src/db/index.ts.template +13 -0
  145. package/dist/templates/fullstack/api/src/db/schema/api-keys.ts.template +19 -0
  146. package/dist/templates/fullstack/api/src/db/schema/audit-logs.ts.template +23 -0
  147. package/dist/templates/fullstack/api/src/db/schema/index.ts.template +8 -0
  148. package/dist/templates/fullstack/api/src/db/schema/invites.ts.template +19 -0
  149. package/dist/templates/fullstack/api/src/db/schema/memberships.ts.template +16 -0
  150. package/dist/templates/fullstack/api/src/db/schema/organizations.ts.template +13 -0
  151. package/dist/templates/fullstack/api/src/db/schema/plans.ts.template +29 -0
  152. package/dist/templates/fullstack/api/src/db/schema/subscriptions.ts.template +38 -0
  153. package/dist/templates/fullstack/api/src/db/schema/users.ts.template +14 -0
  154. package/dist/templates/fullstack/api/src/index.ts.template +54 -0
  155. package/dist/templates/fullstack/api/src/lib/env.ts.template +22 -0
  156. package/dist/templates/fullstack/api/src/routes/health.ts.template +14 -0
  157. package/dist/templates/fullstack/api/tsconfig.json.template +15 -0
  158. package/dist/templates/fullstack/root/.gitignore.template +26 -0
  159. package/dist/templates/fullstack/root/package.json.template +15 -0
  160. package/dist/templates/fullstack/root/pnpm-workspace.yaml.template +3 -0
  161. package/dist/templates/fullstack/root/turbo.json.template +17 -0
  162. package/dist/templates/fullstack/shared/package.json.template +36 -0
  163. package/dist/templates/fullstack/shared/src/index.ts.template +8 -0
  164. package/dist/templates/fullstack/shared/src/schemas/api-key.ts.template +28 -0
  165. package/dist/templates/fullstack/shared/src/schemas/audit-log.ts.template +41 -0
  166. package/dist/templates/fullstack/shared/src/schemas/index.ts.template +8 -0
  167. package/dist/templates/fullstack/shared/src/schemas/invite.ts.template +25 -0
  168. package/dist/templates/fullstack/shared/src/schemas/membership.ts.template +20 -0
  169. package/dist/templates/fullstack/shared/src/schemas/organization.ts.template +18 -0
  170. package/dist/templates/fullstack/shared/src/schemas/plan.ts.template +38 -0
  171. package/dist/templates/fullstack/shared/src/schemas/subscription.ts.template +56 -0
  172. package/dist/templates/fullstack/shared/src/schemas/user.ts.template +21 -0
  173. package/dist/templates/fullstack/shared/src/types/index.ts.template +75 -0
  174. package/dist/templates/fullstack/shared/src/validators/index.ts.template +53 -0
  175. package/dist/templates/fullstack/shared/tsconfig.json.template +17 -0
  176. package/dist/templates/fullstack/web/.gitignore.template +3 -0
  177. package/dist/templates/fullstack/web/index.html.template +13 -0
  178. package/dist/templates/fullstack/web/package.json.template +23 -0
  179. package/dist/templates/fullstack/web/src/App.tsx.template +47 -0
  180. package/dist/templates/fullstack/web/src/index.css.template +54 -0
  181. package/dist/templates/fullstack/web/src/main.tsx.template +10 -0
  182. package/dist/templates/fullstack/web/src/vite-env.d.ts.template +1 -0
  183. package/dist/templates/fullstack/web/tsconfig.json.template +21 -0
  184. package/dist/templates/fullstack/web/tsconfig.node.json.template +11 -0
  185. package/dist/templates/fullstack/web/vite.config.ts.template +15 -0
  186. package/dist/templates/hosted/root/.env.local.template +13 -0
  187. package/dist/templates/hosted/root/.gitignore.template +32 -0
  188. package/dist/templates/hosted/root/CLAUDE.md.template +139 -0
  189. package/dist/templates/hosted/root/drizzle.config.ts.template +10 -0
  190. package/dist/templates/hosted/root/next.config.ts.template +15 -0
  191. package/dist/templates/hosted/root/package.json.template +40 -0
  192. package/dist/templates/hosted/root/postcss.config.mjs.template +9 -0
  193. package/dist/templates/hosted/root/primstack.config.json.template +5 -0
  194. package/dist/templates/hosted/root/tailwind.config.ts.template +14 -0
  195. package/dist/templates/hosted/root/tsconfig.json.template +25 -0
  196. package/dist/templates/hosted/root/wrangler.toml.template +9 -0
  197. package/dist/templates/hosted/src/app/actions/example.ts.template +50 -0
  198. package/dist/templates/hosted/src/app/api/health/route.ts.template +5 -0
  199. package/dist/templates/hosted/src/app/auth/login/page.tsx.template +32 -0
  200. package/dist/templates/hosted/src/app/globals.css.template +59 -0
  201. package/dist/templates/hosted/src/app/layout.tsx.template +24 -0
  202. package/dist/templates/hosted/src/app/page.tsx.template +34 -0
  203. package/dist/templates/hosted/src/db/migrations/0000_initial.sql.template +43 -0
  204. package/dist/templates/hosted/src/db/schema.ts.template +52 -0
  205. package/dist/templates/hosted/src/env.d.ts.template +10 -0
  206. package/dist/templates/hosted/src/instrumentation.ts.template +6 -0
  207. package/dist/templates/hosted/src/lib/auth.ts.template +35 -0
  208. package/dist/templates/hosted/src/lib/db.ts.template +17 -0
  209. package/dist/templates/hosted/src/middleware.ts.template +6 -0
  210. package/package.json +46 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Stripe Webhook Handler
3
+ *
4
+ * This route handles incoming Stripe webhooks for subscription events.
5
+ *
6
+ * Setup:
7
+ * 1. Register this route in your Fastify app
8
+ * 2. Configure webhook endpoint in Stripe Dashboard: /webhooks/stripe
9
+ * 3. For local development: stripe listen --forward-to localhost:3001/webhooks/stripe
10
+ */
11
+
12
+ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
13
+ import { constructWebhookEvent } from '../lib/stripe.js';
14
+ import type Stripe from 'stripe';
15
+
16
+ // Import your database client
17
+ // import { db } from '../db/index.js';
18
+ // import { subscriptions, organizations } from '../db/schema/index.js';
19
+
20
+ /**
21
+ * Custom error class for permanent webhook failures
22
+ * (should not be retried by Stripe)
23
+ */
24
+ class PermanentWebhookError extends Error {
25
+ constructor(message: string) {
26
+ super(message);
27
+ this.name = 'PermanentWebhookError';
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Register Stripe webhook routes
33
+ */
34
+ export async function stripeWebhookRoutes(fastify: FastifyInstance): Promise<void> {
35
+ // Register a raw body parser ONLY for this route
36
+ // This doesn't affect other routes since we're using a route-specific hook
37
+ fastify.post('/webhooks/stripe', {
38
+ config: {
39
+ // Skip auth for webhooks - we verify via Stripe signature
40
+ skipAuth: true,
41
+ rawBody: true, // Request raw body for signature verification
42
+ },
43
+ // Use preValidation to get raw body before parsing
44
+ preValidation: async (request) => {
45
+ // Store raw body for webhook verification
46
+ // @ts-ignore - Adding custom property
47
+ request.rawBody = request.body;
48
+ },
49
+ handler: async (request: FastifyRequest, reply: FastifyReply) => {
50
+ // Handle stripe-signature header (could be string or array)
51
+ const signatureHeader = request.headers['stripe-signature'];
52
+ const signature = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
53
+
54
+ if (!signature) {
55
+ return reply.status(400).send({ error: 'Missing stripe-signature header' });
56
+ }
57
+
58
+ let event: Stripe.Event;
59
+
60
+ try {
61
+ // @ts-ignore - Using rawBody property
62
+ const body = request.rawBody || request.body;
63
+ event = constructWebhookEvent(body as Buffer, signature);
64
+ } catch (err) {
65
+ const message = err instanceof Error ? err.message : 'Unknown error';
66
+ fastify.log.error(`Webhook signature verification failed: ${message}`);
67
+ return reply.status(400).send({ error: `Webhook Error: ${message}` });
68
+ }
69
+
70
+ // Handle the event
71
+ try {
72
+ await handleStripeEvent(event, fastify);
73
+ return reply.status(200).send({ received: true });
74
+ } catch (err) {
75
+ fastify.log.error(`Error handling webhook ${event.type}: ${err}`);
76
+
77
+ // Permanent errors should return 200 to prevent retries
78
+ // Transient errors (DB down, network issues) should return 500 for retry
79
+ if (err instanceof PermanentWebhookError) {
80
+ fastify.log.warn(`Permanent webhook error (no retry): ${err.message}`);
81
+ return reply.status(200).send({ received: true, error: err.message });
82
+ }
83
+
84
+ // Return 500 for transient errors so Stripe retries
85
+ return reply.status(500).send({ error: 'Webhook handler failed, will retry' });
86
+ }
87
+ },
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Handle Stripe webhook events
93
+ */
94
+ async function handleStripeEvent(
95
+ event: Stripe.Event,
96
+ fastify: FastifyInstance
97
+ ): Promise<void> {
98
+ fastify.log.info(`Processing Stripe event: ${event.type}`);
99
+
100
+ switch (event.type) {
101
+ case 'checkout.session.completed':
102
+ await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session, fastify);
103
+ break;
104
+
105
+ case 'customer.subscription.created':
106
+ await handleSubscriptionCreated(event.data.object as Stripe.Subscription, fastify);
107
+ break;
108
+
109
+ case 'customer.subscription.updated':
110
+ await handleSubscriptionUpdated(event.data.object as Stripe.Subscription, fastify);
111
+ break;
112
+
113
+ case 'customer.subscription.deleted':
114
+ await handleSubscriptionDeleted(event.data.object as Stripe.Subscription, fastify);
115
+ break;
116
+
117
+ case 'invoice.payment_succeeded':
118
+ await handleInvoicePaymentSucceeded(event.data.object as Stripe.Invoice, fastify);
119
+ break;
120
+
121
+ case 'invoice.payment_failed':
122
+ await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice, fastify);
123
+ break;
124
+
125
+ default:
126
+ fastify.log.info(`Unhandled event type: ${event.type}`);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Handle checkout.session.completed
132
+ * Called when a customer completes checkout
133
+ */
134
+ async function handleCheckoutCompleted(
135
+ session: Stripe.Checkout.Session,
136
+ fastify: FastifyInstance
137
+ ): Promise<void> {
138
+ fastify.log.info(`Checkout completed: ${session.id}`);
139
+
140
+ // Get metadata from the session
141
+ const organizationId = session.metadata?.organizationId;
142
+ const userId = session.metadata?.userId;
143
+
144
+ if (!organizationId) {
145
+ fastify.log.warn('Checkout session missing organizationId in metadata');
146
+ return;
147
+ }
148
+
149
+ // TODO: Update your database
150
+ // Example:
151
+ // await db.update(organizations)
152
+ // .set({
153
+ // stripeCustomerId: session.customer as string,
154
+ // stripeSubscriptionId: session.subscription as string,
155
+ // })
156
+ // .where(eq(organizations.id, organizationId));
157
+ }
158
+
159
+ /**
160
+ * Handle customer.subscription.created
161
+ */
162
+ async function handleSubscriptionCreated(
163
+ subscription: Stripe.Subscription,
164
+ fastify: FastifyInstance
165
+ ): Promise<void> {
166
+ fastify.log.info(`Subscription created: ${subscription.id}`);
167
+
168
+ const customerId = subscription.customer as string;
169
+ const priceId = subscription.items.data[0]?.price.id;
170
+ const lookupKey = subscription.items.data[0]?.price.lookup_key;
171
+
172
+ // TODO: Create subscription record in your database
173
+ // Example:
174
+ // await db.insert(subscriptions).values({
175
+ // stripeSubscriptionId: subscription.id,
176
+ // stripeCustomerId: customerId,
177
+ // status: subscription.status,
178
+ // priceLookupKey: lookupKey,
179
+ // currentPeriodStart: new Date(subscription.current_period_start * 1000),
180
+ // currentPeriodEnd: new Date(subscription.current_period_end * 1000),
181
+ // });
182
+ }
183
+
184
+ /**
185
+ * Handle customer.subscription.updated
186
+ */
187
+ async function handleSubscriptionUpdated(
188
+ subscription: Stripe.Subscription,
189
+ fastify: FastifyInstance
190
+ ): Promise<void> {
191
+ fastify.log.info(`Subscription updated: ${subscription.id}`);
192
+
193
+ // Common updates:
194
+ // - Status change (active, past_due, canceled, etc.)
195
+ // - Plan change (different price)
196
+ // - Cancel at period end
197
+
198
+ // TODO: Update subscription in your database
199
+ // Example:
200
+ // await db.update(subscriptions)
201
+ // .set({
202
+ // status: subscription.status,
203
+ // priceLookupKey: subscription.items.data[0]?.price.lookup_key,
204
+ // cancelAtPeriodEnd: subscription.cancel_at_period_end,
205
+ // currentPeriodEnd: new Date(subscription.current_period_end * 1000),
206
+ // })
207
+ // .where(eq(subscriptions.stripeSubscriptionId, subscription.id));
208
+ }
209
+
210
+ /**
211
+ * Handle customer.subscription.deleted
212
+ */
213
+ async function handleSubscriptionDeleted(
214
+ subscription: Stripe.Subscription,
215
+ fastify: FastifyInstance
216
+ ): Promise<void> {
217
+ fastify.log.info(`Subscription deleted: ${subscription.id}`);
218
+
219
+ // TODO: Update subscription status in your database
220
+ // Example:
221
+ // await db.update(subscriptions)
222
+ // .set({
223
+ // status: 'canceled',
224
+ // canceledAt: new Date(),
225
+ // })
226
+ // .where(eq(subscriptions.stripeSubscriptionId, subscription.id));
227
+
228
+ // You might want to:
229
+ // - Downgrade the organization to free plan
230
+ // - Send a cancellation email
231
+ // - Revoke premium features
232
+ }
233
+
234
+ /**
235
+ * Handle invoice.payment_succeeded
236
+ */
237
+ async function handleInvoicePaymentSucceeded(
238
+ invoice: Stripe.Invoice,
239
+ fastify: FastifyInstance
240
+ ): Promise<void> {
241
+ fastify.log.info(`Invoice payment succeeded: ${invoice.id}`);
242
+
243
+ // This is called for:
244
+ // - Successful subscription renewals
245
+ // - Successful payment after failed payment
246
+
247
+ // TODO: You might want to:
248
+ // - Update subscription status if it was past_due
249
+ // - Send a receipt email
250
+ // - Log the payment for accounting
251
+ }
252
+
253
+ /**
254
+ * Handle invoice.payment_failed
255
+ */
256
+ async function handleInvoicePaymentFailed(
257
+ invoice: Stripe.Invoice,
258
+ fastify: FastifyInstance
259
+ ): Promise<void> {
260
+ fastify.log.info(`Invoice payment failed: ${invoice.id}`);
261
+
262
+ const customerId = invoice.customer as string;
263
+ const subscriptionId = invoice.subscription as string;
264
+
265
+ // TODO: Handle failed payment
266
+ // - Update subscription status to past_due
267
+ // - Send a failed payment email
268
+ // - Notify admin
269
+
270
+ // Example:
271
+ // if (subscriptionId) {
272
+ // await db.update(subscriptions)
273
+ // .set({ status: 'past_due' })
274
+ // .where(eq(subscriptions.stripeSubscriptionId, subscriptionId));
275
+ // }
276
+
277
+ // Get customer email for notification
278
+ // const customer = await stripe.customers.retrieve(customerId);
279
+ // if (customer && !customer.deleted) {
280
+ // await sendPaymentFailedEmail(customer.email);
281
+ // }
282
+ }
283
+
284
+ export default stripeWebhookRoutes;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Stripe Configuration
3
+ *
4
+ * This file contains all Stripe-related configuration including:
5
+ * - API credentials (per environment)
6
+ * - Product definitions with versioned pricing
7
+ * - Usage meters for metered billing
8
+ *
9
+ * IMPORTANT: This file contains sensitive information.
10
+ * Never commit API keys - use environment variables instead.
11
+ */
12
+
13
+ import type { StripeConfig } from '@{{PROJECT_NAME}}/shared/stripe-types';
14
+
15
+ export const stripeConfig: StripeConfig = {
16
+ /**
17
+ * Environment credentials
18
+ * Keys are loaded from environment variables
19
+ */
20
+ environments: {
21
+ test: {
22
+ secretKey: process.env.STRIPE_TEST_SECRET_KEY || '',
23
+ publishableKey: process.env.STRIPE_TEST_PUBLISHABLE_KEY || '',
24
+ webhookSecret: process.env.STRIPE_TEST_WEBHOOK_SECRET || '',
25
+ },
26
+ live: {
27
+ secretKey: process.env.STRIPE_LIVE_SECRET_KEY || '',
28
+ publishableKey: process.env.STRIPE_LIVE_PUBLISHABLE_KEY || '',
29
+ webhookSecret: process.env.STRIPE_LIVE_WEBHOOK_SECRET || '',
30
+ },
31
+ },
32
+
33
+ /**
34
+ * Product definitions
35
+ *
36
+ * Each product has:
37
+ * - Versioned pricing (for grandfathering existing customers)
38
+ * - Feature flags and limits
39
+ * - Stripe product IDs (populated by `prim stripe sync`)
40
+ *
41
+ * Price lookup keys follow the format: {slug}_{interval}_v{version}
42
+ * Example: pro_monthly_v1, pro_yearly_v2
43
+ */
44
+ products: {
45
+ free: {
46
+ name: 'Free',
47
+ slug: 'free',
48
+ description: 'Perfect for getting started',
49
+ features: [
50
+ '1 project',
51
+ '100 API calls/month',
52
+ 'Community support',
53
+ ],
54
+ limits: {
55
+ projects: 1,
56
+ apiCalls: 100,
57
+ storage: 100_000_000, // 100 MB
58
+ seats: 1,
59
+ },
60
+ prices: {
61
+ monthly: [
62
+ {
63
+ version: 1,
64
+ lookupKey: 'free_monthly_v1',
65
+ amountCents: 0,
66
+ isDefault: true,
67
+ },
68
+ ],
69
+ yearly: [
70
+ {
71
+ version: 1,
72
+ lookupKey: 'free_yearly_v1',
73
+ amountCents: 0,
74
+ isDefault: true,
75
+ },
76
+ ],
77
+ },
78
+ },
79
+
80
+ pro: {
81
+ name: 'Pro',
82
+ slug: 'pro',
83
+ description: 'For professionals and small teams',
84
+ features: [
85
+ 'Unlimited projects',
86
+ '10,000 API calls/month',
87
+ 'Priority support',
88
+ '10 team members',
89
+ ],
90
+ limits: {
91
+ projects: -1, // unlimited
92
+ apiCalls: 10_000,
93
+ storage: 10_000_000_000, // 10 GB
94
+ seats: 10,
95
+ },
96
+ highlighted: true,
97
+ prices: {
98
+ monthly: [
99
+ {
100
+ version: 1,
101
+ lookupKey: 'pro_monthly_v1',
102
+ amountCents: 2900, // $29/mo
103
+ isDefault: true,
104
+ },
105
+ ],
106
+ yearly: [
107
+ {
108
+ version: 1,
109
+ lookupKey: 'pro_yearly_v1',
110
+ amountCents: 29000, // $290/yr (2 months free)
111
+ isDefault: true,
112
+ },
113
+ ],
114
+ },
115
+ },
116
+
117
+ enterprise: {
118
+ name: 'Enterprise',
119
+ slug: 'enterprise',
120
+ description: 'For large organizations',
121
+ features: [
122
+ 'Everything in Pro',
123
+ 'Unlimited API calls',
124
+ 'Dedicated support',
125
+ 'Unlimited team members',
126
+ 'Custom integrations',
127
+ 'SLA guarantee',
128
+ ],
129
+ limits: {
130
+ projects: -1,
131
+ apiCalls: -1,
132
+ storage: -1,
133
+ seats: -1,
134
+ },
135
+ prices: {
136
+ monthly: [
137
+ {
138
+ version: 1,
139
+ lookupKey: 'enterprise_monthly_v1',
140
+ amountCents: 9900, // $99/mo
141
+ isDefault: true,
142
+ },
143
+ ],
144
+ yearly: [
145
+ {
146
+ version: 1,
147
+ lookupKey: 'enterprise_yearly_v1',
148
+ amountCents: 99000, // $990/yr
149
+ isDefault: true,
150
+ },
151
+ ],
152
+ },
153
+ },
154
+ },
155
+
156
+ /**
157
+ * Usage meters (for usage-based billing)
158
+ * These are optional and require Stripe Billing Meters
159
+ */
160
+ meters: {
161
+ // apiCalls: {
162
+ // displayName: 'API Calls',
163
+ // stripeMeterEventName: 'api_call',
164
+ // aggregation: 'sum',
165
+ // },
166
+ },
167
+
168
+ /**
169
+ * Customer portal configuration
170
+ */
171
+ customerPortal: {
172
+ allowCancellations: true,
173
+ allowPlanChanges: true,
174
+ allowPaymentMethodUpdates: true,
175
+ },
176
+ };
177
+
178
+ export default stripeConfig;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Public Pricing Data
3
+ *
4
+ * This file contains ONLY public pricing information that is safe to expose
5
+ * to the frontend. It does NOT include:
6
+ * - Stripe IDs
7
+ * - Price versions (only current default prices)
8
+ * - Internal limits
9
+ *
10
+ * For the full config with Stripe IDs and versioning, see apps/api/stripe.config.ts
11
+ */
12
+
13
+ export interface PublicPlan {
14
+ slug: string;
15
+ name: string;
16
+ description: string;
17
+ priceMonthly: number; // cents
18
+ priceYearly: number; // cents
19
+ features: string[];
20
+ highlighted?: boolean;
21
+ cta?: string;
22
+ }
23
+
24
+ /**
25
+ * Public pricing plans for display
26
+ *
27
+ * This is the SINGLE SOURCE OF TRUTH for pricing display.
28
+ * Update this when prices change.
29
+ */
30
+ export const PRICING_PLANS: PublicPlan[] = [
31
+ {
32
+ slug: 'free',
33
+ name: 'Free',
34
+ description: 'Perfect for getting started',
35
+ priceMonthly: 0,
36
+ priceYearly: 0,
37
+ features: [
38
+ '1 project',
39
+ '100 API calls/month',
40
+ 'Community support',
41
+ ],
42
+ cta: 'Get Started',
43
+ },
44
+ {
45
+ slug: 'pro',
46
+ name: 'Pro',
47
+ description: 'For professionals and small teams',
48
+ priceMonthly: 2900, // $29
49
+ priceYearly: 29000, // $290 (2 months free)
50
+ features: [
51
+ 'Unlimited projects',
52
+ '10,000 API calls/month',
53
+ 'Priority support',
54
+ '10 team members',
55
+ ],
56
+ highlighted: true,
57
+ cta: 'Start Free Trial',
58
+ },
59
+ {
60
+ slug: 'enterprise',
61
+ name: 'Enterprise',
62
+ description: 'For large organizations',
63
+ priceMonthly: 9900, // $99
64
+ priceYearly: 99000, // $990
65
+ features: [
66
+ 'Everything in Pro',
67
+ 'Unlimited API calls',
68
+ 'Dedicated support',
69
+ 'Unlimited team members',
70
+ 'Custom integrations',
71
+ 'SLA guarantee',
72
+ ],
73
+ cta: 'Contact Sales',
74
+ },
75
+ ];
76
+
77
+ /**
78
+ * Format a price in cents as a currency string
79
+ */
80
+ export function formatPrice(cents: number, currency = 'USD'): string {
81
+ if (cents === 0) return 'Free';
82
+
83
+ return new Intl.NumberFormat('en-US', {
84
+ style: 'currency',
85
+ currency,
86
+ minimumFractionDigits: 0,
87
+ maximumFractionDigits: 0,
88
+ }).format(cents / 100);
89
+ }
90
+
91
+ /**
92
+ * Get a plan by slug
93
+ */
94
+ export function getPlanBySlug(slug: string): PublicPlan | undefined {
95
+ return PRICING_PLANS.find((plan) => plan.slug === slug);
96
+ }
97
+
98
+ /**
99
+ * Get the monthly/yearly savings percentage
100
+ */
101
+ export function getYearlySavingsPercent(plan: PublicPlan): number {
102
+ if (plan.priceMonthly === 0) return 0;
103
+ const yearlyFromMonthly = plan.priceMonthly * 12;
104
+ const savings = yearlyFromMonthly - plan.priceYearly;
105
+ return Math.round((savings / yearlyFromMonthly) * 100);
106
+ }
107
+
108
+ /**
109
+ * Generate lookup key for checkout
110
+ */
111
+ export function getPriceLookupKey(
112
+ slug: string,
113
+ interval: 'monthly' | 'yearly',
114
+ version = 1
115
+ ): string {
116
+ return `${slug}_${interval}_v${version}`;
117
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Stripe Types
3
+ *
4
+ * Shared type definitions for Stripe configuration and billing.
5
+ */
6
+
7
+ export type StripeEnvironment = 'test' | 'live';
8
+ export type BillingInterval = 'monthly' | 'yearly';
9
+
10
+ /**
11
+ * Stripe credentials for an environment
12
+ */
13
+ export interface StripeCredentials {
14
+ secretKey: string;
15
+ publishableKey: string;
16
+ webhookSecret: string;
17
+ }
18
+
19
+ /**
20
+ * Price version for versioned pricing
21
+ *
22
+ * When you change prices, existing customers keep their old prices.
23
+ * New customers get the latest (isDefault: true) price.
24
+ */
25
+ export interface PriceVersion {
26
+ /** Version number (increment for each price change) */
27
+ version: number;
28
+
29
+ /** Lookup key for Stripe (format: {slug}_{interval}_v{version}) */
30
+ lookupKey: string;
31
+
32
+ /** Price in cents */
33
+ amountCents: number;
34
+
35
+ /** Whether this is the current default price for new customers */
36
+ isDefault?: boolean;
37
+
38
+ /** Date when this price was deprecated */
39
+ deprecatedAt?: string;
40
+
41
+ /** Stripe price ID (populated by `prim stripe sync`) */
42
+ stripePriceId?: Record<StripeEnvironment, string>;
43
+ }
44
+
45
+ /**
46
+ * Plan/feature limits
47
+ * Use -1 for unlimited
48
+ */
49
+ export interface PlanLimits {
50
+ projects: number;
51
+ apiCalls: number;
52
+ storage: number;
53
+ seats: number;
54
+ [key: string]: number;
55
+ }
56
+
57
+ /**
58
+ * Product configuration
59
+ */
60
+ export interface ProductConfig {
61
+ name: string;
62
+ slug: string;
63
+ description: string;
64
+ features: string[];
65
+ limits: PlanLimits;
66
+ highlighted?: boolean;
67
+
68
+ /** Stripe product ID (populated by `prim stripe sync`) */
69
+ stripeProductId?: Record<StripeEnvironment, string>;
70
+
71
+ /** Versioned prices per billing interval */
72
+ prices: {
73
+ monthly: PriceVersion[];
74
+ yearly: PriceVersion[];
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Usage meter configuration
80
+ */
81
+ export interface MeterConfig {
82
+ displayName: string;
83
+ stripeMeterEventName: string;
84
+ aggregation: 'sum' | 'max' | 'last';
85
+ }
86
+
87
+ /**
88
+ * Customer portal configuration
89
+ */
90
+ export interface CustomerPortalConfig {
91
+ allowCancellations: boolean;
92
+ allowPlanChanges: boolean;
93
+ allowPaymentMethodUpdates: boolean;
94
+ }
95
+
96
+ /**
97
+ * Full Stripe configuration
98
+ */
99
+ export interface StripeConfig {
100
+ environments: Record<StripeEnvironment, StripeCredentials>;
101
+ products: Record<string, ProductConfig>;
102
+ meters?: Record<string, MeterConfig>;
103
+ customerPortal?: CustomerPortalConfig;
104
+ }
105
+
106
+ /**
107
+ * Get the default price for a product and interval
108
+ */
109
+ export function getDefaultPrice(
110
+ product: ProductConfig,
111
+ interval: BillingInterval
112
+ ): PriceVersion | undefined {
113
+ const prices = product.prices[interval];
114
+ return prices.find((p) => p.isDefault) || prices[prices.length - 1];
115
+ }
116
+
117
+ /**
118
+ * Get price by lookup key from all products
119
+ */
120
+ export function findPriceByLookupKey(
121
+ config: StripeConfig,
122
+ lookupKey: string
123
+ ): { product: ProductConfig; price: PriceVersion; interval: BillingInterval } | undefined {
124
+ for (const product of Object.values(config.products)) {
125
+ for (const interval of ['monthly', 'yearly'] as const) {
126
+ const price = product.prices[interval].find((p) => p.lookupKey === lookupKey);
127
+ if (price) {
128
+ return { product, price, interval };
129
+ }
130
+ }
131
+ }
132
+ return undefined;
133
+ }