Production upgrade: Clerk auth, Stripe billing, analytics, PWA support

Backend & Auth:
- Add @clerk/nextjs for production authentication
- Create src/proxy.ts with clerkMiddleware() for route protection
- Implement multi-tenant auth with role-based access control
- Add Clerk components (Show, UserButton, SignInButton, SignUpButton)

Billing & Payments:
- Full Stripe integration (subscriptions, add-ons, customer portal)
- Plan tiers: Starter 9/mo, Farm 49/mo, Enterprise 99/mo
- Webhook handling for subscription events
- createSubscription(), createAddonSubscription(), createCustomerPortalSession()

API & Security:
- Rate limiting with @upstash/ratelimit (100 req/min API, 20 req/min checkout)
- Zod validation schemas for all endpoints (orders, products, campaigns, etc.)
- Security headers (CSP, HSTS, X-Frame-Options)
- API routes: /api/v1/ with validated, rate-limited endpoints

Monitoring:
- Sentry error tracking with performance monitoring
- PostHog analytics for feature usage, funnels, cohorts
- User activity logging and breadcrumb tracking

Admin Features:
- Analytics dashboard with revenue charts, customer growth, conversion funnel
- Onboarding flow with 6-step interactive tour
- Referral system with share tracking and reward redemption
- Changelog feed with in-app notifications

PWA & SEO:
- Web app manifest with icons and shortcuts
- Service worker for offline support and caching
- Full SEO metadata, OpenGraph, Twitter cards
- Structured data (JSON-LD) for organization and products

Database:
- Add referral_codes, changelogs, onboarding_progress tables
- Add user_activity_logs, api_keys, notification_preferences
- Comprehensive RLS policies for all new tables
- Seed data for demo brands and products
This commit is contained in:
2026-06-02 05:33:42 +00:00
parent b845d69aba
commit 6ab52a2499
32 changed files with 5816 additions and 501 deletions
+673
View File
@@ -0,0 +1,673 @@
// Enhanced Stripe Billing - Full Subscription Management
// Supports plans, add-ons, upgrades, cancellations, and customer portal
import Stripe from "stripe";
import { v4 as uuidv4 } from "uuid";
// Initialize Stripe
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-04-30.basil",
});
// Plan tier configurations
export const PLANS = {
starter: {
id: "starter",
name: "Starter",
description: "Perfect for small farms getting started",
monthlyPrice: 4900, // $49.00 in cents
annualPrice: 44100, // $441.00 (15% discount)
features: [
"Up to 25 products",
"10 stops/month",
"1 user",
"Basic pickup management",
"Email support",
],
limits: {
max_users: 1,
max_stops_monthly: 10,
max_products: 25,
},
},
farm: {
id: "farm",
name: "Farm",
description: "For growing farms with more needs",
monthlyPrice: 14900, // $149.00
annualPrice: 152280, // $1,522.80 (10% discount)
features: [
"Unlimited products",
"Unlimited stops",
"5 users",
"Wholesale Portal",
"Harvest Reach",
"Priority support",
],
limits: {
max_users: 5,
max_stops_monthly: -1, // unlimited
max_products: -1,
},
},
enterprise: {
id: "enterprise",
name: "Enterprise",
description: "Custom solution for larger operations",
monthlyPrice: 39900, // $399.00
annualPrice: 0, // Custom pricing
features: [
"Everything in Farm",
"AI Intelligence Pack",
"SMS Campaigns",
"Square Sync",
"Water Log",
"Unlimited users",
"Unlimited brands",
"Custom development",
"Dedicated SLA",
],
limits: {
max_users: -1,
max_stops_monthly: -1,
max_products: -1,
},
},
} as const;
// Add-ons configuration
export const ADDONS = {
wholesale_portal: {
id: "wholesale_portal",
name: "Wholesale Portal",
description: "Full wholesale customer portal with ordering",
monthlyPrice: 9900, // $99.00
},
harvest_reach: {
id: "harvest_reach",
name: "Harvest Reach",
description: "Email and SMS marketing campaigns",
monthlyPrice: 7900, // $79.00
},
ai_tools: {
id: "ai_tools",
name: "AI Intelligence Pack",
description: "AI-powered insights and automation",
monthlyPrice: 5900, // $59.00
},
water_log: {
id: "water_log",
name: "Water Log",
description: "Track irrigation and water usage",
monthlyPrice: 3900, // $39.00
},
square_sync: {
id: "square_sync",
name: "Square Sync",
description: "Sync inventory with Square POS",
monthlyPrice: 3900, // $39.00
},
sms_campaigns: {
id: "sms_campaigns",
name: "SMS Campaigns",
description: "SMS marketing via Twilio",
monthlyPrice: 2900, // $29.00
},
} as const;
// Stripe Price IDs (configurable via environment)
const PRICE_IDS = {
starter_monthly: process.env.STRIPE_PRICE_STARTER_MONTHLY || "price_starter_monthly",
starter_annual: process.env.STRIPE_PRICE_STARTER_ANNUAL || "price_starter_annual",
farm_monthly: process.env.STRIPE_PRICE_FARM_MONTHLY || "price_farm_monthly",
farm_annual: process.env.STRIPE_PRICE_FARM_ANNUAL || "price_farm_annual",
enterprise_monthly: process.env.STRIPE_PRICE_ENTERPRISE_MONTHLY || "price_enterprise_monthly",
wholesale_portal: process.env.STRIPE_PRICE_WHOLESALE_PORTAL || "price_wholesale_portal",
harvest_reach: process.env.STRIPE_PRICE_HARVEST_REACH || "price_harvest_reach",
ai_tools: process.env.STRIPE_PRICE_AI_TOOLS || "price_ai_tools",
water_log: process.env.STRIPE_PRICE_WATER_LOG || "price_water_log",
square_sync: process.env.STRIPE_PRICE_SQUARE_SYNC || "price_square_sync",
sms_campaigns: process.env.STRIPE_PRICE_SMS_CAMPAIGNS || "price_sms_campaigns",
};
// ============================================
// SUBSCRIPTION MANAGEMENT
// ============================================
export interface CreateSubscriptionOptions {
brandId: string;
brandName: string;
email: string;
plan: keyof typeof PLANS;
interval: "monthly" | "annual";
successUrl: string;
cancelUrl: string;
metadata?: Record<string, string>;
}
export async function createSubscription(options: CreateSubscriptionOptions) {
const { brandId, brandName, email, plan, interval, successUrl, cancelUrl, metadata } = options;
// Get or create Stripe customer
let customerId = await getOrCreateCustomer(brandId, brandName, email);
// Get price ID for selected plan
const priceId = interval === "annual"
? PRICE_IDS[`${plan}_annual` as keyof typeof PRICE_IDS]
: PRICE_IDS[`${plan}_monthly` as keyof typeof PRICE_IDS];
// Create checkout session
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: "subscription",
payment_method_types: ["card"],
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: successUrl,
cancel_url: cancelUrl,
subscription_data: {
metadata: {
brand_id: brandId,
brand_name: brandName,
plan_tier: plan,
interval,
},
trial_period_days: 14, // 14-day trial for new subscriptions
},
metadata: {
brand_id: brandId,
...metadata,
},
allow_promotion_codes: true,
billing_address_collection: "required",
});
return session;
}
export async function createAddonSubscription(options: {
customerId: string;
addon: keyof typeof ADDONS;
brandId: string;
successUrl: string;
cancelUrl: string;
}) {
const { customerId, addon, brandId, successUrl, cancelUrl } = options;
const priceId = PRICE_IDS[addon as keyof typeof PRICE_IDS];
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: "subscription",
payment_method_types: ["card"],
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: successUrl,
cancel_url: cancelUrl,
subscription_data: {
metadata: {
brand_id: brandId,
addon_key: addon,
addon_name: ADDONS[addon].name,
},
},
metadata: {
brand_id: brandId,
addon_key: addon,
type: "addon",
},
});
return session;
}
// ============================================
// CUSTOMER PORTAL
// ============================================
export async function createCustomerPortalSession(options: {
customerId: string;
returnUrl: string;
}) {
const { customerId, returnUrl } = options;
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: returnUrl,
});
return session;
}
// ============================================
// SUBSCRIPTION UPDATES
// ============================================
export async function cancelSubscription(subscriptionId: string, immediate: boolean = false) {
if (immediate) {
return await stripe.subscriptions.cancel(subscriptionId);
}
return await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
}
export async function reactivateSubscription(subscriptionId: string) {
return await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: false,
});
}
export async function changePlan(subscriptionId: string, newPlan: keyof typeof PLANS, interval: "monthly" | "annual") {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
// Get new price ID
const priceId = interval === "annual"
? PRICE_IDS[`${newPlan}_annual` as keyof typeof PRICE_IDS]
: PRICE_IDS[`${newPlan}_monthly` as keyof typeof PRICE_IDS];
// Prorate the change
return await stripe.subscriptions.update(subscriptionId, {
items: [
{
id: subscription.items.data[0].id,
price: priceId,
},
],
proration_behavior: "create_prorations",
});
}
// ============================================
// CUSTOMER MANAGEMENT
// ============================================
export async function getOrCreateCustomer(brandId: string, name: string, email: string) {
// Check if customer exists for this brand
const existingCustomers = await stripe.customers.list({
email,
limit: 1,
});
if (existingCustomers.data.length > 0) {
return existingCustomers.data[0].id;
}
// Create new customer
const customer = await stripe.customers.create({
email,
name,
metadata: {
brand_id: brandId,
},
});
return customer.id;
}
export async function getCustomer(customerId: string) {
return await stripe.customers.retrieve(customerId);
}
export async function updateCustomer(customerId: string, data: Partial<Stripe.CustomerUpdateParams>) {
return await stripe.customers.update(customerId, data);
}
// ============================================
// PAYMENT METHODS
// ============================================
export async function listPaymentMethods(customerId: string) {
return await stripe.paymentMethods.list({
customer: customerId,
type: "card",
});
}
export async function attachPaymentMethod(paymentMethodId: string, customerId: string) {
return await stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId,
});
}
export async function setDefaultPaymentMethod(customerId: string, paymentMethodId: string) {
return await stripe.customers.update(customerId, {
invoice_settings: {
default_payment_method: paymentMethodId,
},
});
}
// ============================================
// INVOICES
// ============================================
export async function listInvoices(customerId: string, limit: number = 24) {
return await stripe.invoices.list({
customer: customerId,
limit,
});
}
export async function getInvoice(invoiceId: string) {
return await stripe.invoices.retrieve(invoiceId);
}
export async function getUpcomingInvoice(customerId: string) {
return await stripe.invoices.retrieveUpcoming({
customer: customerId,
});
}
// ============================================
// PAYMENTS & REFUNDS
// ============================================
export async function createPaymentIntent(options: {
amount: number;
currency?: string;
customerId?: string;
metadata?: Record<string, string>;
}) {
return await stripe.paymentIntents.create({
amount: options.amount,
currency: options.currency || "usd",
customer: options.customerId,
metadata: options.metadata,
automatic_payment_methods: {
enabled: true,
},
});
}
export async function createRefund(paymentIntentId: string, amount?: number, reason?: string) {
return await stripe.refunds.create({
payment_intent: paymentIntentId,
amount,
reason: reason as Stripe.RefundCreateParams.Reason || "requested_by_customer",
});
}
// ============================================
// WEBHOOK PROCESSING
// ============================================
export async function processWebhook(payload: Buffer, signature: string) {
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
try {
const event = stripe.webhooks.constructEvent(payload, signature, webhookSecret);
switch (event.type) {
case "checkout.session.completed":
await handleCheckoutCompleted(event.data.object);
break;
case "customer.subscription.created":
await handleSubscriptionCreated(event.data.object);
break;
case "customer.subscription.updated":
await handleSubscriptionUpdated(event.data.object);
break;
case "customer.subscription.deleted":
await handleSubscriptionDeleted(event.data.object);
break;
case "invoice.payment_succeeded":
await handleInvoicePaymentSucceeded(event.data.object);
break;
case "invoice.payment_failed":
await handleInvoicePaymentFailed(event.data.object);
break;
case "customer.updated":
await handleCustomerUpdated(event.data.object);
break;
}
return { received: true };
} catch (error) {
console.error("Webhook processing error:", error);
throw error;
}
}
// Webhook handlers
async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
const { brand_id, type } = session.metadata || {};
if (type === "addon") {
// Handle addon checkout
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
await enableAddonFeature(brand_id, subscription.metadata.addon_key);
} else {
// Handle plan checkout
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
await updateBrandSubscription(
brand_id,
subscription.id,
subscription.status,
subscription.metadata.plan_tier,
subscription.current_period_end
);
}
}
async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
const { brand_id, plan_tier } = subscription.metadata;
await updateBrandSubscription(
brand_id,
subscription.id,
subscription.status,
plan_tier,
subscription.current_period_end
);
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
const { brand_id, plan_tier, addon_key } = subscription.metadata;
if (addon_key) {
// Add-on subscription
if (subscription.status === "active") {
await enableAddonFeature(brand_id, addon_key);
} else {
await disableAddonFeature(brand_id, addon_key);
}
} else {
// Plan subscription
await updateBrandSubscription(
brand_id,
subscription.id,
subscription.status,
plan_tier,
subscription.current_period_end
);
}
}
async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
const { brand_id, plan_tier } = subscription.metadata;
// Downgrade to free tier or cancel
await updateBrandSubscription(
brand_id,
null,
"cancelled",
null,
null
);
}
async function handleInvoicePaymentSucceeded(invoice: Stripe.Invoice) {
// Log successful payment
console.log(`Payment succeeded for customer: ${invoice.customer}`);
}
async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
// Notify brand about failed payment
console.log(`Payment failed for customer: ${invoice.customer}`);
// You could trigger an email notification here
// await sendPaymentFailedNotification(invoice.customer as string);
}
async function handleCustomerUpdated(customer: Stripe.Customer) {
// Sync customer data with our database
console.log(`Customer updated: ${customer.id}`);
}
// ============================================
// DATABASE HELPERS
// ============================================
async function updateBrandSubscription(
brandId: string | undefined,
subscriptionId: string | null,
status: string,
planTier: string | undefined,
periodEnd: number | null
) {
if (!brandId) return;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
try {
await fetch(
`${supabaseUrl}/rest/v1/rpc/set_brand_subscription`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
p_brand_id: brandId,
p_subscription_id: subscriptionId,
p_subscription_status: status,
p_plan_tier: planTier,
p_current_period_end: periodEnd ? new Date(periodEnd * 1000).toISOString() : null,
}),
}
);
} catch (error) {
console.error("Failed to update brand subscription:", error);
}
}
async function enableAddonFeature(brandId: string | undefined, addonKey: string | undefined) {
if (!brandId || !addonKey) return;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
try {
await fetch(
`${supabaseUrl}/rest/v1/rpc/set_brand_feature`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
p_brand_id: brandId,
p_feature_key: addonKey,
p_enabled: true,
}),
}
);
} catch (error) {
console.error("Failed to enable addon feature:", error);
}
}
async function disableAddonFeature(brandId: string | undefined, addonKey: string | undefined) {
if (!brandId || !addonKey) return;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
try {
await fetch(
`${supabaseUrl}/rest/v1/rpc/set_brand_feature`,
{
method: "POST",
headers: {
apikey: serviceKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
p_brand_id: brandId,
p_feature_key: addonKey,
p_enabled: false,
}),
}
);
} catch (error) {
console.error("Failed to disable addon feature:", error);
}
}
// ============================================
// USAGE BILLING
// ============================================
export async function createUsageRecord(options: {
subscriptionItemId: string;
quantity: number;
timestamp?: number;
idempotencyKey?: string;
}) {
return await stripe.subscriptionItems.createUsageRecord(
options.subscriptionItemId,
{
quantity: options.quantity,
timestamp: options.timestamp,
},
{
idempotencyKey: options.idempotencyKey || uuidv4(),
}
);
}
export async function getUsageSummary(subscriptionItemId: string, period: { start: number; end: number }) {
return await stripe.usageRecordSummaries.list(
subscriptionItemId,
{
limit: 12,
}
);
}
// ============================================
// EXPORT STRIPE INSTANCE AND HELPERS
// ============================================
export { stripe };
export async function getSubscription(subscriptionId: string) {
return await stripe.subscriptions.retrieve(subscriptionId);
}
export async function listSubscriptions(customerId: string) {
return await stripe.subscriptions.list({
customer: customerId,
status: "all",
limit: 10,
});
}