vibefast-cli 1.3.4 → 1.3.5

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 (44) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/add.d.ts.map +1 -1
  3. package/dist/commands/add.js +35 -14
  4. package/dist/commands/add.js.map +1 -1
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +0 -5
  7. package/dist/commands/init.js.map +1 -1
  8. package/docs/commands.md +2 -2
  9. package/docs/quickstart.md +1 -1
  10. package/package.json +1 -1
  11. package/recipes/lemonsqueezy-supabase/apps/web/app/billing/page.tsx +48 -0
  12. package/recipes/lemonsqueezy-supabase/apps/web/app/pricing/page.tsx +19 -0
  13. package/recipes/lemonsqueezy-supabase/apps/web/components/payments/BillingSection.tsx +224 -0
  14. package/recipes/lemonsqueezy-supabase/apps/web/components/payments/PricingCard.tsx +87 -0
  15. package/recipes/lemonsqueezy-supabase/apps/web/components/payments/PricingSection.tsx +100 -0
  16. package/recipes/lemonsqueezy-supabase/apps/web/components/payments/index.ts +2 -0
  17. package/recipes/lemonsqueezy-supabase/apps/web/lib/plans.ts +84 -0
  18. package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/config.ts +92 -0
  19. package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/index.ts +138 -0
  20. package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/providers/lemonsqueezy.ts +165 -0
  21. package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/providers/stripe.ts +179 -0
  22. package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/types.ts +57 -0
  23. package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments-webhook/index.ts +339 -0
  24. package/recipes/lemonsqueezy-supabase/packages/backend/supabase/migrations/web_payments.sql +85 -0
  25. package/recipes/lemonsqueezy-supabase/recipe.json +129 -0
  26. package/recipes/lemonsqueezy-supabase-web@latest.zip +0 -0
  27. package/recipes/lemonsqueezy-web@latest.zip +0 -0
  28. package/recipes/stripe-supabase/apps/web/app/billing/page.tsx +48 -0
  29. package/recipes/stripe-supabase/apps/web/app/pricing/page.tsx +19 -0
  30. package/recipes/stripe-supabase/apps/web/components/payments/BillingSection.tsx +224 -0
  31. package/recipes/stripe-supabase/apps/web/components/payments/PricingCard.tsx +87 -0
  32. package/recipes/stripe-supabase/apps/web/components/payments/PricingSection.tsx +100 -0
  33. package/recipes/stripe-supabase/apps/web/components/payments/index.ts +2 -0
  34. package/recipes/stripe-supabase/apps/web/lib/plans.ts +84 -0
  35. package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/config.ts +92 -0
  36. package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/index.ts +138 -0
  37. package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/providers/lemonsqueezy.ts +165 -0
  38. package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/providers/stripe.ts +179 -0
  39. package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/types.ts +57 -0
  40. package/recipes/stripe-supabase/packages/backend/supabase/functions/payments-webhook/index.ts +339 -0
  41. package/recipes/stripe-supabase/packages/backend/supabase/migrations/web_payments.sql +85 -0
  42. package/recipes/stripe-supabase/recipe.json +123 -0
  43. package/recipes/stripe-supabase-web@latest.zip +0 -0
  44. package/recipes/stripe-web@latest.zip +0 -0
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Supabase Payments Webhook (Stripe or LemonSqueezy)
3
+ *
4
+ * Handles provider webhooks and upserts subscription/order records.
5
+ */
6
+
7
+ import 'jsr:@supabase/functions-js/edge-runtime.d.ts';
8
+ import { createClient } from 'npm:@supabase/supabase-js@2';
9
+ import Stripe from 'npm:stripe@14.25.0';
10
+ import { getPaymentProvider, getPlanByPriceId } from '../payments/config.ts';
11
+
12
+ const jsonResponse = (body: unknown, status = 200) =>
13
+ new Response(JSON.stringify(body), {
14
+ status,
15
+ headers: { 'Content-Type': 'application/json' },
16
+ });
17
+
18
+ const getEnv = (key: string) => {
19
+ const value = Deno.env.get(key);
20
+ if (!value) {
21
+ throw new Error(`Missing environment variable: ${key}`);
22
+ }
23
+ return value;
24
+ };
25
+
26
+ const getSupabaseAdmin = () => {
27
+ const supabaseUrl = getEnv('SUPABASE_URL');
28
+ const supabaseSecretKey =
29
+ Deno.env.get('SUPABASE_SECRET_KEY') ??
30
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
31
+ if (!supabaseSecretKey) {
32
+ throw new Error('Supabase service role key not configured');
33
+ }
34
+ return createClient(supabaseUrl, supabaseSecretKey);
35
+ };
36
+
37
+ const toIso = (timestamp?: number | null) =>
38
+ typeof timestamp === 'number'
39
+ ? new Date(timestamp * 1000).toISOString()
40
+ : null;
41
+
42
+ const parseUserId = (customData: unknown): string | null => {
43
+ if (typeof customData === 'object' && customData !== null) {
44
+ const data = customData as Record<string, unknown>;
45
+ if (typeof data.user_id === 'string') {
46
+ return data.user_id;
47
+ }
48
+ }
49
+ return null;
50
+ };
51
+
52
+ const verifyLemonSignature = async (
53
+ rawBody: string,
54
+ signature: string,
55
+ secret: string,
56
+ ): Promise<boolean> => {
57
+ const encoder = new TextEncoder();
58
+ const key = await crypto.subtle.importKey(
59
+ 'raw',
60
+ encoder.encode(secret),
61
+ { name: 'HMAC', hash: 'SHA-256' },
62
+ false,
63
+ ['sign'],
64
+ );
65
+
66
+ const signatureBuffer = await crypto.subtle.sign(
67
+ 'HMAC',
68
+ key,
69
+ encoder.encode(rawBody),
70
+ );
71
+
72
+ const hashArray = Array.from(new Uint8Array(signatureBuffer));
73
+ const digest = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
74
+ return digest === signature;
75
+ };
76
+
77
+ const upsertPaymentCustomer = async (
78
+ supabaseAdmin: ReturnType<typeof createClient>,
79
+ userId: string,
80
+ provider: string,
81
+ providerCustomerId: string,
82
+ ) => {
83
+ await supabaseAdmin.from('payment_customers').upsert(
84
+ {
85
+ user_id: userId,
86
+ provider,
87
+ provider_customer_id: providerCustomerId,
88
+ },
89
+ { onConflict: 'user_id,provider' },
90
+ );
91
+ };
92
+
93
+ const upsertSubscription = async (
94
+ supabaseAdmin: ReturnType<typeof createClient>,
95
+ data: {
96
+ userId: string;
97
+ provider: string;
98
+ providerSubscriptionId: string;
99
+ providerCustomerId?: string | null;
100
+ priceId?: string | null;
101
+ status?: string | null;
102
+ currentPeriodEnd?: string | null;
103
+ cancelAt?: string | null;
104
+ cancelAtPeriodEnd?: boolean | null;
105
+ metadata?: Record<string, unknown> | null;
106
+ },
107
+ ) => {
108
+ const plan = data.priceId ? getPlanByPriceId(data.priceId) : undefined;
109
+ const now = new Date().toISOString();
110
+
111
+ await supabaseAdmin.from('subscriptions').upsert(
112
+ {
113
+ user_id: data.userId,
114
+ provider: data.provider,
115
+ provider_subscription_id: data.providerSubscriptionId,
116
+ provider_customer_id: data.providerCustomerId ?? null,
117
+ price_id: data.priceId ?? null,
118
+ plan_id: plan?.id ?? null,
119
+ status: data.status ?? null,
120
+ current_period_end: data.currentPeriodEnd ?? null,
121
+ cancel_at: data.cancelAt ?? null,
122
+ cancel_at_period_end: data.cancelAtPeriodEnd ?? null,
123
+ metadata: data.metadata ?? null,
124
+ updated_at: now,
125
+ created_at: now,
126
+ },
127
+ { onConflict: 'provider,provider_subscription_id' },
128
+ );
129
+ };
130
+
131
+ Deno.serve(async (req: Request) => {
132
+ try {
133
+ if (req.method !== 'POST') {
134
+ return jsonResponse({ ok: false, message: 'Method not allowed' }, 405);
135
+ }
136
+
137
+ const provider = getPaymentProvider();
138
+ const supabaseAdmin = getSupabaseAdmin();
139
+ const rawBody = await req.text();
140
+
141
+ if (provider === 'stripe') {
142
+ const signature = req.headers.get('stripe-signature');
143
+ const secret = Deno.env.get('STRIPE_WEBHOOK_SECRET');
144
+ if (!signature || !secret) {
145
+ return jsonResponse({ ok: false, message: 'Stripe webhook not configured' }, 400);
146
+ }
147
+
148
+ const stripe = new Stripe(getEnv('STRIPE_SECRET_KEY'), {
149
+ apiVersion: '2023-10-16',
150
+ });
151
+
152
+ let event: Stripe.Event;
153
+ try {
154
+ event = stripe.webhooks.constructEvent(rawBody, signature, secret);
155
+ } catch (err) {
156
+ return jsonResponse({ ok: false, message: 'Invalid Stripe signature' }, 401);
157
+ }
158
+
159
+ if (
160
+ event.type === 'checkout.session.completed' &&
161
+ event.data.object &&
162
+ (event.data.object as Stripe.Checkout.Session).subscription
163
+ ) {
164
+ const session = event.data.object as Stripe.Checkout.Session;
165
+ const subscriptionId = session.subscription as string;
166
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId);
167
+
168
+ const userId =
169
+ session.metadata?.user_id ??
170
+ subscription.metadata?.user_id ??
171
+ null;
172
+
173
+ const customerId =
174
+ typeof subscription.customer === 'string'
175
+ ? subscription.customer
176
+ : subscription.customer?.id;
177
+
178
+ let resolvedUserId = userId;
179
+ if (!resolvedUserId && customerId) {
180
+ const { data } = await supabaseAdmin
181
+ .from('payment_customers')
182
+ .select('user_id')
183
+ .eq('provider', 'stripe')
184
+ .eq('provider_customer_id', customerId)
185
+ .maybeSingle();
186
+ resolvedUserId = data?.user_id ?? null;
187
+ }
188
+
189
+ if (resolvedUserId && customerId) {
190
+ await upsertPaymentCustomer(
191
+ supabaseAdmin,
192
+ resolvedUserId,
193
+ 'stripe',
194
+ customerId,
195
+ );
196
+ }
197
+
198
+ if (resolvedUserId) {
199
+ const priceId =
200
+ subscription.items?.data?.[0]?.price?.id ??
201
+ session.metadata?.price_id ??
202
+ null;
203
+
204
+ await upsertSubscription(supabaseAdmin, {
205
+ userId: resolvedUserId,
206
+ provider: 'stripe',
207
+ providerSubscriptionId: subscription.id,
208
+ providerCustomerId: customerId ?? null,
209
+ priceId,
210
+ status: subscription.status,
211
+ currentPeriodEnd: toIso(subscription.current_period_end),
212
+ cancelAt: toIso(subscription.cancel_at),
213
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
214
+ metadata: subscription.metadata ?? null,
215
+ });
216
+ }
217
+ }
218
+
219
+ if (event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.deleted') {
220
+ const subscription = event.data.object as Stripe.Subscription;
221
+ const customerId =
222
+ typeof subscription.customer === 'string'
223
+ ? subscription.customer
224
+ : subscription.customer?.id;
225
+
226
+ let userId = subscription.metadata?.user_id ?? null;
227
+ if (!userId && customerId) {
228
+ const { data } = await supabaseAdmin
229
+ .from('payment_customers')
230
+ .select('user_id')
231
+ .eq('provider', 'stripe')
232
+ .eq('provider_customer_id', customerId)
233
+ .maybeSingle();
234
+ userId = data?.user_id ?? null;
235
+ }
236
+
237
+ if (userId) {
238
+ const priceId = subscription.items?.data?.[0]?.price?.id ?? null;
239
+ await upsertSubscription(supabaseAdmin, {
240
+ userId,
241
+ provider: 'stripe',
242
+ providerSubscriptionId: subscription.id,
243
+ providerCustomerId: customerId ?? null,
244
+ priceId,
245
+ status: subscription.status,
246
+ currentPeriodEnd: toIso(subscription.current_period_end),
247
+ cancelAt: toIso(subscription.cancel_at),
248
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
249
+ metadata: subscription.metadata ?? null,
250
+ });
251
+ }
252
+ }
253
+
254
+ return jsonResponse({ ok: true });
255
+ }
256
+
257
+ // LemonSqueezy webhook
258
+ const secret = Deno.env.get('LEMONSQUEEZY_WEBHOOK_SECRET');
259
+ const signature = req.headers.get('X-Signature');
260
+ if (!secret || !signature) {
261
+ return jsonResponse({ ok: false, message: 'LemonSqueezy webhook not configured' }, 400);
262
+ }
263
+
264
+ const isValid = await verifyLemonSignature(rawBody, signature, secret);
265
+ if (!isValid) {
266
+ return jsonResponse({ ok: false, message: 'Invalid signature' }, 401);
267
+ }
268
+
269
+ const payload = JSON.parse(rawBody);
270
+ const eventName = payload?.meta?.event_name as string | undefined;
271
+ const data = payload?.data;
272
+
273
+ if (!eventName || !data) {
274
+ return jsonResponse({ ok: false, message: 'Missing event data' }, 400);
275
+ }
276
+
277
+ if (eventName.startsWith('subscription_')) {
278
+ const userId = parseUserId(payload?.meta?.custom_data);
279
+ if (userId) {
280
+ const attrs = data.attributes ?? {};
281
+ const currentPeriodEnd =
282
+ attrs.renews_at || attrs.ends_at || attrs.trial_ends_at || null;
283
+
284
+ if (attrs.customer_id) {
285
+ await upsertPaymentCustomer(
286
+ supabaseAdmin,
287
+ userId,
288
+ 'lemonsqueezy',
289
+ String(attrs.customer_id),
290
+ );
291
+ }
292
+
293
+ await upsertSubscription(supabaseAdmin, {
294
+ userId,
295
+ provider: 'lemonsqueezy',
296
+ providerSubscriptionId: String(data.id),
297
+ providerCustomerId: attrs.customer_id
298
+ ? String(attrs.customer_id)
299
+ : null,
300
+ priceId: attrs.variant_id ? String(attrs.variant_id) : null,
301
+ status: attrs.status ?? 'unknown',
302
+ currentPeriodEnd: currentPeriodEnd
303
+ ? new Date(currentPeriodEnd).toISOString()
304
+ : null,
305
+ cancelAt: attrs.ends_at ? new Date(attrs.ends_at).toISOString() : null,
306
+ cancelAtPeriodEnd: attrs.status === 'cancelled',
307
+ metadata: {
308
+ customerPortalUrl: attrs.urls?.customer_portal,
309
+ },
310
+ });
311
+ }
312
+ }
313
+
314
+ if (eventName === 'order_created') {
315
+ const userId = parseUserId(payload?.meta?.custom_data);
316
+ if (userId) {
317
+ const attrs = data.attributes ?? {};
318
+ await supabaseAdmin.from('orders').upsert(
319
+ {
320
+ provider: 'lemonsqueezy',
321
+ order_id: String(data.id),
322
+ user_id: userId,
323
+ product_id: attrs.product_id ? String(attrs.product_id) : null,
324
+ variant_id: attrs.variant_id ? String(attrs.variant_id) : null,
325
+ amount: attrs.total ?? null,
326
+ currency: attrs.currency ?? 'USD',
327
+ status: 'completed',
328
+ },
329
+ { onConflict: 'provider,order_id' },
330
+ );
331
+ }
332
+ }
333
+
334
+ return jsonResponse({ ok: true });
335
+ } catch (err) {
336
+ const message = err instanceof Error ? err.message : 'Unexpected error';
337
+ return jsonResponse({ ok: false, message }, 500);
338
+ }
339
+ });
@@ -0,0 +1,85 @@
1
+ -- Web Payments Migration (Stripe/LemonSqueezy)
2
+ -- Creates provider-agnostic tables for subscriptions and orders
3
+
4
+ -- ============================================================================
5
+ -- PAYMENT_CUSTOMERS TABLE
6
+ -- Maps users to provider customer IDs
7
+ -- ============================================================================
8
+ CREATE TABLE IF NOT EXISTS payment_customers (
9
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
10
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
11
+ provider TEXT NOT NULL,
12
+ provider_customer_id TEXT NOT NULL,
13
+ created_at TIMESTAMPTZ DEFAULT now(),
14
+ UNIQUE (user_id, provider),
15
+ UNIQUE (provider, provider_customer_id)
16
+ );
17
+
18
+ -- ============================================================================
19
+ -- SUBSCRIPTIONS TABLE
20
+ -- Tracks subscription state across providers
21
+ -- ============================================================================
22
+ CREATE TABLE IF NOT EXISTS subscriptions (
23
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
24
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
25
+ provider TEXT NOT NULL,
26
+ provider_subscription_id TEXT NOT NULL,
27
+ provider_customer_id TEXT,
28
+ price_id TEXT,
29
+ plan_id TEXT,
30
+ status TEXT,
31
+ current_period_end TIMESTAMPTZ,
32
+ cancel_at TIMESTAMPTZ,
33
+ cancel_at_period_end BOOLEAN,
34
+ metadata JSONB,
35
+ created_at TIMESTAMPTZ DEFAULT now(),
36
+ updated_at TIMESTAMPTZ DEFAULT now(),
37
+ UNIQUE (provider, provider_subscription_id)
38
+ );
39
+
40
+ -- ============================================================================
41
+ -- ORDERS TABLE
42
+ -- Stores one-time purchases (used by LemonSqueezy and future providers)
43
+ -- ============================================================================
44
+ CREATE TABLE IF NOT EXISTS orders (
45
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
46
+ provider TEXT NOT NULL,
47
+ order_id TEXT NOT NULL,
48
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
49
+ product_id TEXT,
50
+ variant_id TEXT,
51
+ amount NUMERIC,
52
+ currency TEXT,
53
+ status TEXT,
54
+ created_at TIMESTAMPTZ DEFAULT now(),
55
+ UNIQUE (provider, order_id)
56
+ );
57
+
58
+ -- ============================================================================
59
+ -- INDEXES
60
+ -- ============================================================================
61
+ CREATE INDEX IF NOT EXISTS idx_payment_customers_user_id ON payment_customers(user_id);
62
+ CREATE INDEX IF NOT EXISTS idx_payment_customers_provider ON payment_customers(provider);
63
+
64
+ CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id);
65
+ CREATE INDEX IF NOT EXISTS idx_subscriptions_provider ON subscriptions(provider);
66
+ CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
67
+
68
+ CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);
69
+ CREATE INDEX IF NOT EXISTS idx_orders_provider ON orders(provider);
70
+
71
+ -- ============================================================================
72
+ -- RLS POLICIES
73
+ -- ============================================================================
74
+ ALTER TABLE payment_customers ENABLE ROW LEVEL SECURITY;
75
+ ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
76
+ ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
77
+
78
+ CREATE POLICY "payment_customers_select_own" ON payment_customers
79
+ FOR SELECT USING (auth.uid() = user_id);
80
+
81
+ CREATE POLICY "subscriptions_select_own" ON subscriptions
82
+ FOR SELECT USING (auth.uid() = user_id);
83
+
84
+ CREATE POLICY "orders_select_own" ON orders
85
+ FOR SELECT USING (auth.uid() = user_id);
@@ -0,0 +1,129 @@
1
+ {
2
+ "name": "lemonsqueezy-supabase",
3
+ "version": "1.0.0",
4
+ "description": "Lemon Squeezy web payments for Supabase (pricing + billing)",
5
+ "copy": [
6
+ {
7
+ "from": "apps/web/app/pricing",
8
+ "to": "apps/web/app/pricing"
9
+ },
10
+ {
11
+ "from": "apps/web/app/billing",
12
+ "to": "apps/web/app/billing"
13
+ },
14
+ {
15
+ "from": "apps/web/components/payments",
16
+ "to": "apps/web/components/payments"
17
+ },
18
+ {
19
+ "from": "apps/web/lib/plans.ts",
20
+ "to": "apps/web/lib/plans.ts"
21
+ },
22
+ {
23
+ "from": "packages/backend/supabase/functions/payments",
24
+ "to": "packages/backend/supabase/functions/payments"
25
+ },
26
+ {
27
+ "from": "packages/backend/supabase/functions/payments-webhook",
28
+ "to": "packages/backend/supabase/functions/payments-webhook"
29
+ },
30
+ {
31
+ "from": "packages/backend/supabase/migrations/web_payments.sql",
32
+ "to": "packages/backend/supabase/migrations/web_payments.sql"
33
+ }
34
+ ],
35
+ "target": "web",
36
+ "env": [
37
+ {
38
+ "key": "PAYMENT_PROVIDER",
39
+ "description": "Active payments provider (stripe or lemonsqueezy)",
40
+ "example": "lemonsqueezy",
41
+ "file": "packages/backend/.env.local"
42
+ },
43
+ {
44
+ "key": "SITE_URL",
45
+ "description": "Public site URL for checkout redirects (no trailing slash)",
46
+ "example": "https://yourdomain.com",
47
+ "file": "packages/backend/.env.local"
48
+ },
49
+ {
50
+ "key": "PLAN_STARTER_PRICE_ID",
51
+ "description": "Lemon Squeezy variant ID for Starter plan",
52
+ "example": "123456",
53
+ "file": "packages/backend/.env.local"
54
+ },
55
+ {
56
+ "key": "PLAN_PRO_PRICE_ID",
57
+ "description": "Lemon Squeezy variant ID for Pro plan",
58
+ "example": "234567",
59
+ "file": "packages/backend/.env.local"
60
+ },
61
+ {
62
+ "key": "PLAN_ENTERPRISE_PRICE_ID",
63
+ "description": "Lemon Squeezy variant ID for Enterprise plan",
64
+ "example": "345678",
65
+ "file": "packages/backend/.env.local"
66
+ },
67
+ {
68
+ "key": "LEMONSQUEEZY_API_KEY",
69
+ "description": "Lemon Squeezy API key",
70
+ "example": "api_key_...",
71
+ "file": "packages/backend/.env.local"
72
+ },
73
+ {
74
+ "key": "LEMONSQUEEZY_STORE_ID",
75
+ "description": "Lemon Squeezy store ID",
76
+ "example": "12345",
77
+ "file": "packages/backend/.env.local"
78
+ },
79
+ {
80
+ "key": "LEMONSQUEEZY_WEBHOOK_SECRET",
81
+ "description": "Lemon Squeezy webhook signing secret",
82
+ "example": "whsec_...",
83
+ "file": "packages/backend/.env.local"
84
+ },
85
+ {
86
+ "key": "NEXT_PUBLIC_PLAN_STARTER_PRICE_ID",
87
+ "description": "Starter plan variant ID for the web app",
88
+ "example": "123456",
89
+ "file": "apps/web/.env.local"
90
+ },
91
+ {
92
+ "key": "NEXT_PUBLIC_PLAN_PRO_PRICE_ID",
93
+ "description": "Pro plan variant ID for the web app",
94
+ "example": "234567",
95
+ "file": "apps/web/.env.local"
96
+ },
97
+ {
98
+ "key": "NEXT_PUBLIC_PLAN_ENTERPRISE_PRICE_ID",
99
+ "description": "Enterprise plan variant ID for the web app",
100
+ "example": "345678",
101
+ "file": "apps/web/.env.local"
102
+ }
103
+ ],
104
+ "manualSteps": [
105
+ {
106
+ "title": "Push Supabase secrets",
107
+ "description": "Add PAYMENT_PROVIDER, SITE_URL, LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID, LEMONSQUEEZY_WEBHOOK_SECRET, and plan IDs to packages/backend/.env.local, then run: pnpm --filter @vibefast/backend secrets:push",
108
+ "file": "packages/backend/.env.local"
109
+ },
110
+ {
111
+ "title": "Deploy Edge Functions",
112
+ "description": "Deploy payments functions: pnpm --filter @vibefast/backend functions:deploy",
113
+ "file": "packages/backend/supabase/functions"
114
+ },
115
+ {
116
+ "title": "Create Stripe webhook",
117
+ "description": "Add a webhook endpoint: https://<project-ref>.functions.supabase.co/payments-webhook",
118
+ "file": "Lemon Squeezy Dashboard"
119
+ },
120
+ {
121
+ "title": "Apply database migrations",
122
+ "description": "Run: pnpm --filter @vibefast/backend db:push",
123
+ "file": "packages/backend/supabase/migrations"
124
+ }
125
+ ],
126
+ "postInstall": {
127
+ "message": "✅ Lemon Squeezy web payments (Supabase) added. Set env vars, deploy functions, and apply migrations."
128
+ }
129
+ }
Binary file
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import { Layout } from '@/components/Layout';
4
+ import { BillingSection } from '@/components/payments';
5
+ import { Authenticated, Unauthenticated } from '@/lib/supabase-provider';
6
+ import Link from 'next/link';
7
+ import { Button } from '@/components/ui/button';
8
+
9
+ // ============================================================================
10
+ // Billing Page
11
+ // User's subscription and billing management
12
+ // ============================================================================
13
+
14
+ export default function BillingPage() {
15
+ return (
16
+ <Layout>
17
+ <div className="container max-w-2xl py-8">
18
+ <div className="mb-8">
19
+ <h1 className="text-2xl font-bold">Billing & Subscription</h1>
20
+ <p className="mt-1 text-muted-foreground">
21
+ Manage your subscription and billing details
22
+ </p>
23
+ </div>
24
+
25
+ <Authenticated>
26
+ <BillingSection />
27
+ </Authenticated>
28
+
29
+ <Unauthenticated>
30
+ <div className="rounded-lg border bg-card p-8 text-center">
31
+ <h2 className="text-lg font-semibold">Sign in to view billing</h2>
32
+ <p className="mt-2 text-sm text-muted-foreground">
33
+ You need to be signed in to manage your subscription.
34
+ </p>
35
+ <div className="mt-6 flex justify-center gap-4">
36
+ <Button asChild>
37
+ <Link href="/">Sign In</Link>
38
+ </Button>
39
+ <Button variant="outline" asChild>
40
+ <Link href="/pricing">View Plans</Link>
41
+ </Button>
42
+ </div>
43
+ </div>
44
+ </Unauthenticated>
45
+ </div>
46
+ </Layout>
47
+ );
48
+ }
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+
3
+ import { Layout } from '@/components/Layout';
4
+ import { PricingSection } from '@/components/payments';
5
+
6
+ // ============================================================================
7
+ // Pricing Page
8
+ // Displays available subscription plans
9
+ // ============================================================================
10
+
11
+ export default function PricingPage() {
12
+ return (
13
+ <Layout>
14
+ <div className="container max-w-6xl py-8">
15
+ <PricingSection />
16
+ </div>
17
+ </Layout>
18
+ );
19
+ }