@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,123 @@
1
+ 'use client';
2
+
3
+ import { Card, CardHeader, CardContent } from '@primstack/ui/card';
4
+ import { Badge } from '@primstack/ui/badge';
5
+ import { Button } from '@primstack/ui/button';
6
+ import { Separator } from '@primstack/ui/separator';
7
+ import { Progress } from '@primstack/ui/progress';
8
+ import { formatPrice } from '@{{PROJECT_NAME}}/shared/pricing';
9
+
10
+ interface BillingSettingsProps {
11
+ plan: {
12
+ name: string;
13
+ slug: string;
14
+ /** The billed price in cents for the current interval */
15
+ price: number;
16
+ interval: 'monthly' | 'yearly';
17
+ };
18
+ status: 'active' | 'trialing' | 'canceled' | 'past_due';
19
+ currentPeriodEnd: string;
20
+ usage?: {
21
+ label: string;
22
+ current: number;
23
+ limit: number;
24
+ }[];
25
+ portalUrl: string;
26
+ onChangePlan?: () => void;
27
+ }
28
+
29
+ const statusVariant: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
30
+ active: 'default',
31
+ trialing: 'secondary',
32
+ canceled: 'destructive',
33
+ past_due: 'destructive',
34
+ };
35
+
36
+ const statusLabel: Record<string, string> = {
37
+ active: 'Active',
38
+ trialing: 'Trial',
39
+ canceled: 'Canceled',
40
+ past_due: 'Past Due',
41
+ };
42
+
43
+ export function BillingSettings({
44
+ plan,
45
+ status,
46
+ currentPeriodEnd,
47
+ usage,
48
+ portalUrl,
49
+ onChangePlan,
50
+ }: BillingSettingsProps) {
51
+ return (
52
+ <div className="space-y-6">
53
+ <Card>
54
+ <CardHeader>
55
+ <div className="flex items-center justify-between">
56
+ <h3 className="text-lg font-semibold">Current Plan</h3>
57
+ <Badge variant={statusVariant[status] ?? 'outline'}>
58
+ {statusLabel[status] ?? status}
59
+ </Badge>
60
+ </div>
61
+ </CardHeader>
62
+ <CardContent className="space-y-4">
63
+ <div className="flex items-baseline justify-between">
64
+ <div>
65
+ <p className="text-2xl font-bold">{plan.name}</p>
66
+ <p className="text-sm text-muted-foreground">
67
+ {formatPrice(plan.price)}/{plan.interval === 'yearly' ? 'yr' : 'mo'}
68
+ </p>
69
+ </div>
70
+ {onChangePlan && (
71
+ <Button variant="outline" onClick={onChangePlan}>
72
+ Change Plan
73
+ </Button>
74
+ )}
75
+ </div>
76
+
77
+ <Separator />
78
+
79
+ <div className="flex items-center justify-between text-sm">
80
+ <span className="text-muted-foreground">Next billing date</span>
81
+ <span>
82
+ {new Date(currentPeriodEnd).toLocaleDateString('en-US', {
83
+ month: 'long',
84
+ day: 'numeric',
85
+ year: 'numeric',
86
+ })}
87
+ </span>
88
+ </div>
89
+
90
+ <div>
91
+ <Button variant="outline" className="w-full" asChild>
92
+ <a href={portalUrl}>Manage Billing</a>
93
+ </Button>
94
+ </div>
95
+ </CardContent>
96
+ </Card>
97
+
98
+ {usage && usage.length > 0 && (
99
+ <Card>
100
+ <CardHeader>
101
+ <h3 className="text-lg font-semibold">Usage</h3>
102
+ </CardHeader>
103
+ <CardContent className="space-y-4">
104
+ {usage.map((item) => {
105
+ const percent = item.limit > 0 ? Math.round((item.current / item.limit) * 100) : 0;
106
+ return (
107
+ <div key={item.label}>
108
+ <div className="mb-1 flex justify-between text-sm">
109
+ <span>{item.label}</span>
110
+ <span className="text-muted-foreground">
111
+ {item.current.toLocaleString()} / {item.limit.toLocaleString()}
112
+ </span>
113
+ </div>
114
+ <Progress value={percent} />
115
+ </div>
116
+ );
117
+ })}
118
+ </CardContent>
119
+ </Card>
120
+ )}
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+
3
+ import { Card, CardHeader, CardContent, CardFooter } from '@primstack/ui/card';
4
+ import { Badge } from '@primstack/ui/badge';
5
+ import { Button } from '@primstack/ui/button';
6
+ import { Switch } from '@primstack/ui/switch';
7
+ import { Separator } from '@primstack/ui/separator';
8
+ import {
9
+ PRICING_PLANS,
10
+ formatPrice,
11
+ getYearlySavingsPercent,
12
+ } from '@{{PROJECT_NAME}}/shared/pricing';
13
+ import { useState } from 'react';
14
+
15
+ interface PricingCardsProps {
16
+ onSelectPlan: (slug: string, interval: 'monthly' | 'yearly') => void;
17
+ }
18
+
19
+ export function PricingCards({ onSelectPlan }: PricingCardsProps) {
20
+ const [yearly, setYearly] = useState(false);
21
+ const interval = yearly ? 'yearly' : 'monthly';
22
+
23
+ return (
24
+ <div className="space-y-8">
25
+ <div className="flex items-center justify-center gap-3">
26
+ <span className={yearly ? 'text-muted-foreground' : 'font-medium'}>Monthly</span>
27
+ <Switch checked={yearly} onCheckedChange={setYearly} />
28
+ <span className={yearly ? 'font-medium' : 'text-muted-foreground'}>Yearly</span>
29
+ {yearly && (
30
+ <Badge variant="secondary" className="ml-1">
31
+ Save up to {Math.max(...PRICING_PLANS.map(getYearlySavingsPercent))}%
32
+ </Badge>
33
+ )}
34
+ </div>
35
+
36
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
37
+ {PRICING_PLANS.map((plan) => {
38
+ const price = yearly ? plan.priceYearly : plan.priceMonthly;
39
+ const savings = getYearlySavingsPercent(plan);
40
+
41
+ return (
42
+ <Card
43
+ key={plan.slug}
44
+ className={
45
+ plan.highlighted
46
+ ? 'relative border-primary ring-2 ring-primary'
47
+ : ''
48
+ }
49
+ >
50
+ {plan.highlighted && (
51
+ <div className="absolute -top-3 left-1/2 -translate-x-1/2">
52
+ <Badge>Popular</Badge>
53
+ </div>
54
+ )}
55
+ <CardHeader className="text-center">
56
+ <h3 className="text-lg font-semibold">{plan.name}</h3>
57
+ <p className="text-sm text-muted-foreground">{plan.description}</p>
58
+ <div className="mt-4">
59
+ <span className="text-4xl font-bold">
60
+ {formatPrice(price)}
61
+ </span>
62
+ {price > 0 && (
63
+ <span className="text-muted-foreground">
64
+ /{yearly ? 'yr' : 'mo'}
65
+ </span>
66
+ )}
67
+ {yearly && savings > 0 && (
68
+ <p className="mt-1 text-sm text-primary">
69
+ Save {savings}% vs monthly
70
+ </p>
71
+ )}
72
+ </div>
73
+ </CardHeader>
74
+
75
+ <Separator />
76
+
77
+ <CardContent className="pt-6">
78
+ <ul className="space-y-3">
79
+ {plan.features.map((feature) => (
80
+ <li key={feature} className="flex items-start gap-2 text-sm">
81
+ <svg
82
+ className="mt-0.5 h-4 w-4 shrink-0 text-primary"
83
+ fill="none"
84
+ viewBox="0 0 24 24"
85
+ stroke="currentColor"
86
+ strokeWidth={2}
87
+ >
88
+ <path
89
+ strokeLinecap="round"
90
+ strokeLinejoin="round"
91
+ d="M5 13l4 4L19 7"
92
+ />
93
+ </svg>
94
+ {feature}
95
+ </li>
96
+ ))}
97
+ </ul>
98
+ </CardContent>
99
+
100
+ <CardFooter>
101
+ <Button
102
+ className="w-full"
103
+ variant={plan.highlighted ? 'default' : 'outline'}
104
+ onClick={() => onSelectPlan(plan.slug, interval)}
105
+ >
106
+ {plan.cta || 'Get Started'}
107
+ </Button>
108
+ </CardFooter>
109
+ </Card>
110
+ );
111
+ })}
112
+ </div>
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+
3
+ import { PricingCards } from '../components/pricing-cards';
4
+ import {
5
+ Accordion,
6
+ AccordionItem,
7
+ AccordionTrigger,
8
+ AccordionContent,
9
+ } from '@primstack/ui/accordion';
10
+ import { Separator } from '@primstack/ui/separator';
11
+
12
+ const faqs = [
13
+ {
14
+ question: 'Can I change my plan later?',
15
+ answer:
16
+ 'Yes! You can upgrade or downgrade your plan at any time. When upgrading, you'll be prorated for the remainder of your billing cycle. When downgrading, the new rate applies at the next billing period.',
17
+ },
18
+ {
19
+ question: 'What happens when my trial ends?',
20
+ answer:
21
+ 'When your trial ends, you'll be automatically subscribed to the plan you selected. You can cancel at any time before the trial ends to avoid being charged.',
22
+ },
23
+ {
24
+ question: 'Do you offer refunds?',
25
+ answer:
26
+ 'We offer a 14-day money-back guarantee on all paid plans. If you're not satisfied, contact support for a full refund.',
27
+ },
28
+ {
29
+ question: 'What payment methods do you accept?',
30
+ answer:
31
+ 'We accept all major credit cards (Visa, Mastercard, American Express) as well as ACH direct debit for annual plans. All payments are securely processed through Stripe.',
32
+ },
33
+ ];
34
+
35
+ export default function PricingPage() {
36
+ const handleSelectPlan = async (slug: string, interval: 'monthly' | 'yearly') => {
37
+ if (slug === 'free') {
38
+ window.location.href = '/signup';
39
+ return;
40
+ }
41
+
42
+ try {
43
+ const res = await fetch('/api/stripe/checkout', {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify({ planSlug: slug, interval }),
47
+ });
48
+
49
+ if (!res.ok) {
50
+ throw new Error('Checkout request failed');
51
+ }
52
+
53
+ const { url } = await res.json();
54
+ if (url) {
55
+ window.location.href = url;
56
+ }
57
+ } catch (err) {
58
+ console.error('Failed to create checkout session:', err);
59
+ // TODO: Show user-facing error (e.g. toast notification)
60
+ }
61
+ };
62
+
63
+ return (
64
+ <div className="mx-auto max-w-5xl px-4 py-16">
65
+ <div className="mb-12 text-center">
66
+ <h1 className="text-4xl font-bold tracking-tight">
67
+ Simple, transparent pricing
68
+ </h1>
69
+ <p className="mt-4 text-lg text-muted-foreground">
70
+ Choose the plan that fits your needs. No hidden fees.
71
+ </p>
72
+ </div>
73
+
74
+ <PricingCards onSelectPlan={handleSelectPlan} />
75
+
76
+ <Separator className="my-16" />
77
+
78
+ <div className="mx-auto max-w-2xl">
79
+ <h2 className="mb-6 text-center text-2xl font-bold">
80
+ Frequently Asked Questions
81
+ </h2>
82
+ <Accordion type="single" collapsible className="w-full">
83
+ {faqs.map((faq, i) => (
84
+ <AccordionItem key={i} value={`faq-${i}`}>
85
+ <AccordionTrigger>{faq.question}</AccordionTrigger>
86
+ <AccordionContent>
87
+ <p className="text-muted-foreground">{faq.answer}</p>
88
+ </AccordionContent>
89
+ </AccordionItem>
90
+ ))}
91
+ </Accordion>
92
+ </div>
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,7 @@
1
+ # Server configuration
2
+ PORT={{PORT}}
3
+ HOST=0.0.0.0
4
+ NODE_ENV=development
5
+
6
+ # CORS (optional, defaults to allow all in development)
7
+ # CORS_ORIGIN=https://your-frontend.com
@@ -0,0 +1,24 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+
7
+ # Environment
8
+ .env
9
+ .env.local
10
+ .env.*.local
11
+
12
+ # IDE
13
+ .idea/
14
+ .vscode/
15
+ *.swp
16
+ *.swo
17
+
18
+ # OS
19
+ .DS_Store
20
+ Thumbs.db
21
+
22
+ # Logs
23
+ *.log
24
+ npm-debug.log*
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx watch src/index.ts",
7
+ "build": "tsc",
8
+ "start": "node dist/index.js",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@fastify/cors": "^10.0.0",
13
+ "@fastify/helmet": "^13.0.0",
14
+ "@fastify/sensible": "^6.0.0",
15
+ "fastify": "^5.0.0",
16
+ "zod": "^3.23.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.0.0",
20
+ "tsx": "^4.0.0",
21
+ "typescript": "^5.7.0"
22
+ }
23
+ }
@@ -0,0 +1,52 @@
1
+ import Fastify from 'fastify';
2
+ import cors from '@fastify/cors';
3
+ import helmet from '@fastify/helmet';
4
+ import sensible from '@fastify/sensible';
5
+ import { env } from './lib/env.js';
6
+ import healthRoutes from './routes/health.js';
7
+
8
+ const app = Fastify({
9
+ logger: {
10
+ level: env.NODE_ENV === 'development' ? 'info' : 'warn',
11
+ },
12
+ });
13
+
14
+ // Plugins
15
+ app.register(helmet);
16
+ app.register(cors, {
17
+ origin: env.NODE_ENV === 'development'
18
+ ? true
19
+ : (env.CORS_ORIGIN?.split(',').map((o) => o.trim()).filter(Boolean) ?? false),
20
+ });
21
+ app.register(sensible);
22
+
23
+ // Routes
24
+ app.register(healthRoutes, { prefix: '/health' });
25
+
26
+ // Graceful shutdown
27
+ const shutdown = async (signal: string) => {
28
+ app.log.info(`Received ${signal}, shutting down gracefully...`);
29
+ try {
30
+ await app.close();
31
+ process.exit(0);
32
+ } catch (err) {
33
+ app.log.error('Error during shutdown:', err);
34
+ process.exit(1);
35
+ }
36
+ };
37
+
38
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
39
+ process.on('SIGINT', () => shutdown('SIGINT'));
40
+
41
+ // Start server
42
+ const start = async () => {
43
+ try {
44
+ await app.listen({ port: env.PORT, host: env.HOST });
45
+ app.log.info(`Server running at http://${env.HOST}:${env.PORT}`);
46
+ } catch (err) {
47
+ app.log.error(err);
48
+ process.exit(1);
49
+ }
50
+ };
51
+
52
+ start();
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+
3
+ const envSchema = z.object({
4
+ PORT: z.coerce.number().default({{PORT}}),
5
+ HOST: z.string().default('0.0.0.0'),
6
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
7
+ CORS_ORIGIN: z.string().min(1).optional(),
8
+ });
9
+
10
+ export const env = envSchema
11
+ .superRefine((val, ctx) => {
12
+ if (val.NODE_ENV === 'production' && !val.CORS_ORIGIN) {
13
+ ctx.addIssue({
14
+ code: z.ZodIssueCode.custom,
15
+ message: 'CORS_ORIGIN is required when NODE_ENV=production',
16
+ path: ['CORS_ORIGIN'],
17
+ });
18
+ }
19
+ })
20
+ .parse(process.env);
@@ -0,0 +1,12 @@
1
+ import type { FastifyPluginAsync } from 'fastify';
2
+
3
+ const healthRoutes: FastifyPluginAsync = async (fastify) => {
4
+ fastify.get('/', async () => {
5
+ return {
6
+ status: 'ok',
7
+ timestamp: new Date().toISOString(),
8
+ };
9
+ });
10
+ };
11
+
12
+ export default healthRoutes;
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }
@@ -0,0 +1,10 @@
1
+ # Server configuration
2
+ PORT={{PORT}}
3
+ HOST=0.0.0.0
4
+ NODE_ENV=development
5
+
6
+ # CORS (optional, defaults to allow all in development)
7
+ # CORS_ORIGIN=https://your-frontend.com
8
+
9
+ # Database
10
+ DATABASE_URL=postgresql://user:password@localhost:5432/{{PROJECT_NAME}}
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ export default defineConfig({
4
+ schema: './src/db/schema',
5
+ out: './src/db/migrations',
6
+ dialect: 'postgresql',
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL!,
9
+ },
10
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "scripts": {
3
+ "db:generate": "drizzle-kit generate",
4
+ "db:migrate": "drizzle-kit migrate",
5
+ "db:push": "drizzle-kit push",
6
+ "db:studio": "drizzle-kit studio"
7
+ },
8
+ "dependencies": {
9
+ "drizzle-orm": "^0.36.0",
10
+ "pg": "^8.13.0"
11
+ },
12
+ "devDependencies": {
13
+ "@types/pg": "^8.11.0",
14
+ "drizzle-kit": "^0.28.0"
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import { drizzle } from 'drizzle-orm/node-postgres';
2
+ import pg from 'pg';
3
+ import * as schema from './schema/users.js';
4
+
5
+ const pool = new pg.Pool({
6
+ connectionString: process.env.DATABASE_URL,
7
+ });
8
+
9
+ export const db = drizzle(pool, { schema });
@@ -0,0 +1,12 @@
1
+ import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
2
+
3
+ export const users = pgTable('users', {
4
+ id: uuid('id').primaryKey().defaultRandom(),
5
+ email: varchar('email', { length: 255 }).notNull().unique(),
6
+ name: varchar('name', { length: 255 }),
7
+ createdAt: timestamp('created_at').defaultNow().notNull(),
8
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
9
+ });
10
+
11
+ export type User = typeof users.$inferSelect;
12
+ export type NewUser = typeof users.$inferInsert;
@@ -0,0 +1,10 @@
1
+ # Server configuration
2
+ PORT={{PORT}}
3
+ HOST=0.0.0.0
4
+ NODE_ENV=development
5
+
6
+ # CORS (optional, defaults to allow all in development)
7
+ # CORS_ORIGIN=https://your-frontend.com
8
+
9
+ # Database (SQLite file path)
10
+ DATABASE_URL=sqlite.db
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ export default defineConfig({
4
+ schema: './src/db/schema',
5
+ out: './src/db/migrations',
6
+ dialect: 'sqlite',
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL || 'sqlite.db',
9
+ },
10
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "scripts": {
3
+ "db:generate": "drizzle-kit generate",
4
+ "db:migrate": "drizzle-kit migrate",
5
+ "db:push": "drizzle-kit push",
6
+ "db:studio": "drizzle-kit studio"
7
+ },
8
+ "dependencies": {
9
+ "better-sqlite3": "^11.6.0",
10
+ "drizzle-orm": "^0.36.0"
11
+ },
12
+ "devDependencies": {
13
+ "@types/better-sqlite3": "^7.6.0",
14
+ "drizzle-kit": "^0.28.0"
15
+ }
16
+ }
@@ -0,0 +1,6 @@
1
+ import { drizzle } from 'drizzle-orm/better-sqlite3';
2
+ import Database from 'better-sqlite3';
3
+ import * as schema from './schema/users.js';
4
+
5
+ const sqlite = new Database(process.env.DATABASE_URL || 'sqlite.db');
6
+ export const db = drizzle(sqlite, { schema });
@@ -0,0 +1,12 @@
1
+ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
2
+
3
+ export const users = sqliteTable('users', {
4
+ id: integer('id').primaryKey({ autoIncrement: true }),
5
+ email: text('email').notNull().unique(),
6
+ name: text('name'),
7
+ createdAt: text('created_at').notNull().default('CURRENT_TIMESTAMP'),
8
+ updatedAt: text('updated_at').notNull().default('CURRENT_TIMESTAMP'),
9
+ });
10
+
11
+ export type User = typeof users.$inferSelect;
12
+ export type NewUser = typeof users.$inferInsert;
@@ -0,0 +1,10 @@
1
+ # Server configuration
2
+ PORT={{PORT}}
3
+ HOST=0.0.0.0
4
+ NODE_ENV=development
5
+
6
+ # CORS (optional, defaults to allow all in development)
7
+ # CORS_ORIGIN=https://your-frontend.com
8
+
9
+ # Supabase Database
10
+ SUPABASE_DATABASE_URL=postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ export default defineConfig({
4
+ schema: './src/db/schema',
5
+ out: './src/db/migrations',
6
+ dialect: 'postgresql',
7
+ dbCredentials: {
8
+ url: process.env.SUPABASE_DATABASE_URL!,
9
+ },
10
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "scripts": {
3
+ "db:generate": "drizzle-kit generate",
4
+ "db:migrate": "drizzle-kit migrate",
5
+ "db:push": "drizzle-kit push",
6
+ "db:studio": "drizzle-kit studio"
7
+ },
8
+ "dependencies": {
9
+ "drizzle-orm": "^0.36.0",
10
+ "postgres": "^3.4.0"
11
+ },
12
+ "devDependencies": {
13
+ "drizzle-kit": "^0.28.0"
14
+ }
15
+ }
@@ -0,0 +1,9 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js';
2
+ import postgres from 'postgres';
3
+ import * as schema from './schema/users.js';
4
+
5
+ const client = postgres(process.env.SUPABASE_DATABASE_URL!, {
6
+ prepare: false, // Required for Supabase connection pooler (Supavisor)
7
+ });
8
+
9
+ export const db = drizzle(client, { schema });