Skip to main content
RapidDev - Software Development Agency

How to Build a Subscription Box Service with Lovable

Build a subscription box service in Lovable with Stripe recurring billing synced to a Supabase fulfillment pipeline. Subscribers pick a plan from comparison Cards, a webhook handler creates shipment records on each billing cycle, and a Calendar shows the next box dispatch date — all with real-time shipment tracking.

What you'll build

  • Plan comparison Cards with box preview images, item count, and a price per box breakdown
  • Stripe Subscription with monthly billing synchronized to fulfillment cycle start dates
  • Shipments table in Supabase that auto-populates when Stripe invoices a subscriber
  • Webhook handler linking invoice.paid events to shipment creation and scheduling
  • Subscriber portal showing current box contents, next shipment Calendar view, and tracking status
  • Admin fulfillment dashboard with shipments DataTable, status filters, and bulk dispatch workflow
  • Subscription management page with plan upgrades and the next box cutoff date countdown
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced15 min read4–5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a subscription box service in Lovable with Stripe recurring billing synced to a Supabase fulfillment pipeline. Subscribers pick a plan from comparison Cards, a webhook handler creates shipment records on each billing cycle, and a Calendar shows the next box dispatch date — all with real-time shipment tracking.

What you're building

A subscription box service has two synchronized lifecycles: billing and fulfillment. Stripe handles billing automatically — it creates invoices at the start of each billing period and charges the subscriber's card. Your webhook handler links billing events to fulfillment by creating a shipment record in Supabase each time an invoice is paid. This approach ensures you only create shipments for subscribers who have actually been charged.

The shipments table tracks the fulfillment journey: created (invoice paid, awaiting packing), packed (warehouse packed the box), shipped (tracking number assigned), delivered (carrier confirms delivery), and returned. Admin staff use the fulfillment dashboard to bulk-update shipment statuses and add tracking numbers. Subscribers see their current shipment status and next dispatch date in their portal.

The Calendar component from shadcn/ui shows the subscriber's next billing and dispatch date. Most subscription boxes dispatch a few days after billing (to allow time for packing). The dispatch_date column on shipments is set to billing_date + configured dispatch_offset_days. This gives subscribers a clear expectation of when their box arrives.

Final result

A complete subscription box platform with Stripe billing, automated shipment creation, admin fulfillment dashboard, and subscriber-facing tracking portal.

Tech stack

LovableFrontend app builder
Stripe BillingRecurring subscription billing
SupabaseDatabase and Edge Functions
shadcn/uiUI components including Calendar
RechartsRevenue and subscriber growth charts
React Hook Form + ZodAddress and preference forms

Prerequisites

  • Lovable Pro account for multi-function Edge Function generation
  • Stripe account with STRIPE_SECRET_KEY, VITE_STRIPE_PUBLISHABLE_KEY, and STRIPE_WEBHOOK_SECRET in Cloud tab → Secrets
  • Supabase project with service role key in Secrets
  • Deployed Lovable app URL for Stripe webhook configuration and payment flows
  • Understanding that Stripe billing events trigger shipment creation — no manual dispatch needed

Build steps

1

Set up the subscription box schema

Create the tables for box plans, subscriptions, shipments, and box contents. The schema models the full lifecycle from plan selection to delivered box.

prompt.txt
1Create a subscription box service schema in Supabase:
2
3Tables:
4- box_plans: id (uuid pk), name (text), description (text), items_per_box (int), price_cents (int), currency (text default 'usd'), stripe_price_id (text unique), interval (text default 'month'), box_value_cents (int, retail value of items), is_active (bool default true), is_featured (bool default false), image_url (text), dispatch_offset_days (int default 3), sort_order (int), created_at
5
6- box_subscriptions: id (uuid pk), user_id (uuid references auth.users unique), stripe_subscription_id (text unique), stripe_customer_id (text), plan_id (uuid references box_plans), status (text: active|canceled|paused|past_due), shipping_address (jsonb), preferences (jsonb default '{}'), current_period_start (timestamptz), current_period_end (timestamptz), cancel_at_period_end (bool default false), created_at, updated_at
7
8- shipments: id (uuid pk), subscription_id (uuid references box_subscriptions), user_id (uuid references auth.users), stripe_invoice_id (text unique), plan_id (uuid references box_plans), status (text default 'created': created|packed|shipped|delivered|returned), tracking_number (text nullable), tracking_url (text nullable), carrier (text nullable), dispatch_date (date), billing_period_start (timestamptz), billing_period_end (timestamptz), delivered_at (timestamptz nullable), notes (text nullable), created_at, updated_at
9
10- box_contents: id (uuid pk), plan_id (uuid references box_plans), billing_month (text, e.g. '2026-04'), item_name (text), item_description (text), item_image_url (text nullable), retail_value_cents (int), created_at
11
12RLS:
13- box_plans: public SELECT
14- box_subscriptions: users can SELECT/UPDATE their own
15- shipments: users can SELECT their own
16- box_contents: public SELECT
17
18Service role full access to all tables.

Pro tip: Add dispatch_offset_days to box_plans so different tiers can have different packing times. A premium plan might dispatch next-day while a standard plan dispatches 3 days after billing. This field drives the dispatch_date calculation in the webhook handler.

Expected result: All four tables are created with RLS. TypeScript types are generated. The box_plans table has the dispatch_offset_days column for each plan.

2

Build the plan comparison and subscription signup flow

Ask Lovable to create the plan selection page with comparison Cards and the subscription creation Edge Function.

prompt.txt
1Build a subscription plans page at src/pages/Plans.tsx.
2
3Requirements:
4- Fetch all is_active box_plans ordered by sort_order
5- Display in a responsive grid as shadcn/ui Cards:
6 - Plan image (box preview photo)
7 - Plan name (large, font-bold)
8 - 'Most Popular' Badge if is_featured=true, with accent border
9 - Price per month as large text, below it 'per box'
10 - Box value: 'Over ${box_value} in products'
11 - Items per box: '{items_per_box} curated items'
12 - Feature list with 3-4 bullets (free shipping, cancel anytime, etc.)
13 - Subscribe Button (primary for featured, outline for others)
14- If user already has an active subscription:
15 - Their current plan's button shows 'Current Plan' (disabled)
16 - Higher-value plans show 'Upgrade'
17 - Lower plans show 'Downgrade'
18- Subscribing opens a Dialog with two steps:
19 Step 1: Shipping address form (react-hook-form + Zod): firstName, lastName, address1, address2, city, state, postalCode, country Select
20 Step 2: Payment method form using Stripe Elements
21- After payment method is collected, call create-box-subscription Edge Function
22- Show next dispatch date after successful subscription: 'Your first box ships on {date}'

Pro tip: Show the next box dispatch date in the subscription success message. Calculate it as today + plan.dispatch_offset_days, then format it as a friendly date. This sets subscriber expectations immediately and reduces 'where is my box?' support requests.

Expected result: The plans page renders with comparison cards. The subscription dialog collects address and payment. A successful subscription shows the next dispatch date.

3

Create the subscription billing Edge Functions

Build the Edge Functions for creating subscriptions and handling the Stripe billing webhook that creates shipments.

prompt.txt
1Create two Supabase Edge Functions:
2
31. supabase/functions/create-box-subscription/index.ts
4- Accept: { planId, paymentMethodId, shippingAddress }
5- Get authenticated user from JWT
6- Look up box_plans by planId to get stripe_price_id and dispatch_offset_days
7- Create Stripe customer with user email
8- Attach payment method and set as default
9- Create Stripe Subscription with stripe_price_id
10- Insert into box_subscriptions: user_id, plan_id, stripe_subscription_id, stripe_customer_id, status='active', shipping_address, current_period_start, current_period_end
11- Return: { subscriptionId, nextDispatchDate }
12 (nextDispatchDate = current_period_start + dispatch_offset_days)
13
142. supabase/functions/box-billing-webhook/index.ts
15- Verify Stripe webhook signature with constructEventAsync
16- Handle invoice.paid:
17 a. Get stripe_subscription_id from invoice.subscription
18 b. Look up box_subscriptions in Supabase by stripe_subscription_id
19 c. Look up the plan to get dispatch_offset_days
20 d. Calculate dispatch_date = invoice.period_start date + dispatch_offset_days
21 e. Insert into shipments: subscription_id, user_id, plan_id, stripe_invoice_id, status='created', dispatch_date, billing_period_start, billing_period_end
22- Handle customer.subscription.updated: update box_subscriptions status
23- Handle customer.subscription.deleted: update status='canceled'
24- Return 200 for all events

Pro tip: In the webhook handler, check if a shipment with the same stripe_invoice_id already exists before inserting. Use upsert with onConflict: 'stripe_invoice_id' to handle webhook retries safely without creating duplicate shipments.

Expected result: Creating a subscription in test mode works. A test invoice.paid webhook creates a shipment row with the calculated dispatch_date. The box_subscriptions table updates status on subscription lifecycle events.

4

Build the subscriber portal

Ask Lovable to create the subscriber-facing portal showing current subscription status, next shipment Calendar view, and shipment history.

prompt.txt
1Build a subscriber portal at src/pages/MySubscription.tsx.
2
3Requirements:
4- Fetch the current user's box_subscription joined with box_plans
5- If no subscription: show a Card with 'You don't have an active subscription' and a Browse Plans Button
6
7Top section - Subscription status:
8- Plan name, status Badge (active=green, paused=yellow, canceled=gray)
9- Current period Card: 'Current box period: {period_start} – {period_end}'
10- Next billing date formatted as 'Next billing date: April 1, 2026'
11- Monthly amount formatted as currency
12
13Calendar section:
14- shadcn/ui Calendar component (read-only) showing the current month
15- Mark today with a dot indicator
16- Mark the next dispatch_date with a highlighted cell and tooltip: 'Your box dispatches on this date'
17- Mark the next billing date with a different color indicator
18
19Current shipment section:
20- Fetch the most recent shipment for this subscription
21- Show status as a horizontal Step indicator: Created Packed Shipped Delivered
22- If tracking_number is set, show it with a copy button and a tracking_url link
23- If status='shipped', show estimated delivery (shipped_at + 5 days)
24
25Shipment history section:
26- List past 6 shipments as compact rows with period, status Badge, and tracking link
27
28Manage subscription buttons:
29- 'Change Plan', 'Update Address', 'Pause Subscription', 'Cancel Subscription'

Pro tip: The shadcn/ui Calendar component accepts a modifiers prop to highlight specific dates with custom styles. Pass dispatch dates and billing dates as separate modifier keys so they render with different colors on the calendar.

Expected result: The subscriber portal shows the subscription status, a calendar with highlighted dispatch and billing dates, and the current shipment status with tracking.

5

Build the admin fulfillment dashboard

Ask Lovable to build the admin-only fulfillment dashboard with a DataTable for managing shipments, bulk status updates, and a revenue chart.

prompt.txt
1Build an admin fulfillment dashboard at src/pages/admin/Fulfillment.tsx (admin-only route).
2
3Requirements:
4- Fetch all shipments ordered by dispatch_date ASC joined with box_plans and auth.users
5- Status filter Tabs at top: All | Created | Packed | Shipped | Delivered
6- Dispatch date filter: 'This week' | 'Next 30 days' | 'All upcoming'
7- DataTable with columns:
8 - Subscriber email
9 - Plan name Badge
10 - Dispatch date (highlighted red if past today and not shipped)
11 - Status Badge with color coding
12 - Tracking number (editable inline Input)
13 - Carrier (editable Select: USPS/FedEx/UPS/DHL)
14 - Actions: update status dropdown button
15- Select multiple rows with checkboxes
16- Bulk actions toolbar when rows selected: 'Mark as Packed', 'Mark as Shipped', 'Export CSV'
17- When marking as Shipped, open a Dialog asking for tracking number and carrier (required)
18- Summary Cards at top: Total This Week, Pending Dispatch, Shipped, Delivered
19- Below the table: Recharts BarChart showing new subscribers per month for last 6 months

Expected result: The fulfillment dashboard shows all upcoming and recent shipments. Bulk marking rows as shipped opens the tracking number Dialog. The status filter tabs update the table. The subscriber chart shows monthly growth.

Complete code

supabase/functions/box-billing-webhook/index.ts
1import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
2import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
3import Stripe from 'https://esm.sh/stripe@14?target=deno'
4
5const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') ?? '', {
6 apiVersion: '2023-10-16',
7 httpClient: Stripe.createFetchHttpClient(),
8})
9const cryptoProvider = Stripe.createSubtleCryptoProvider()
10
11serve(async (req: Request) => {
12 const body = await req.text()
13 const sig = req.headers.get('stripe-signature') ?? ''
14
15 let event: Stripe.Event
16 try {
17 event = await stripe.webhooks.constructEventAsync(
18 body, sig, Deno.env.get('STRIPE_WEBHOOK_SECRET') ?? '',
19 undefined, cryptoProvider
20 )
21 } catch {
22 return new Response('Signature failed', { status: 400 })
23 }
24
25 const supabase = createClient(
26 Deno.env.get('SUPABASE_URL') ?? '',
27 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
28 )
29
30 if (event.type === 'invoice.paid') {
31 const inv = event.data.object as Stripe.Invoice
32 const stripeSubId = typeof inv.subscription === 'string' ? inv.subscription : inv.subscription?.id
33 if (!stripeSubId) return new Response(JSON.stringify({ received: true }))
34
35 const { data: sub } = await supabase
36 .from('box_subscriptions')
37 .select('id, user_id, plan_id, box_plans(dispatch_offset_days)')
38 .eq('stripe_subscription_id', stripeSubId)
39 .single()
40
41 if (sub) {
42 const plan = sub.box_plans as { dispatch_offset_days: number }
43 const periodStart = new Date(inv.period_start * 1000)
44 const dispatchDate = new Date(periodStart)
45 dispatchDate.setDate(dispatchDate.getDate() + plan.dispatch_offset_days)
46
47 await supabase.from('shipments').upsert({
48 subscription_id: sub.id,
49 user_id: sub.user_id,
50 plan_id: sub.plan_id,
51 stripe_invoice_id: inv.id,
52 status: 'created',
53 dispatch_date: dispatchDate.toISOString().split('T')[0],
54 billing_period_start: periodStart.toISOString(),
55 billing_period_end: new Date(inv.period_end * 1000).toISOString(),
56 }, { onConflict: 'stripe_invoice_id' })
57 }
58 }
59
60 if (event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.deleted') {
61 const stripeSub = event.data.object as Stripe.Subscription
62 await supabase.from('box_subscriptions').update({
63 status: stripeSub.status === 'canceled' ? 'canceled' : stripeSub.status,
64 cancel_at_period_end: stripeSub.cancel_at_period_end,
65 current_period_end: new Date(stripeSub.current_period_end * 1000).toISOString(),
66 updated_at: new Date().toISOString(),
67 }).eq('stripe_subscription_id', stripeSub.id)
68 }
69
70 return new Response(JSON.stringify({ received: true }), { status: 200 })
71})

Customization ideas

Box customization preferences

Add a preferences flow where subscribers fill out a profile form after subscribing: dietary restrictions, size, style preferences, pet types, etc. Store as JSONB in box_subscriptions.preferences. Display preferences for each subscriber in the admin fulfillment view so packers can see individual requirements when assembling boxes.

Unboxing experience with contents reveal

Add a box reveal page that uses the box_contents table to show what was in each past box. Display contents with product images, retail values, and descriptions. Add a spoiler toggle so subscribers can choose to preview upcoming box contents or wait for the surprise. Feature boxes with total retail value prominently to reinforce value.

Pause subscription workflow

Add a Pause button to the subscriber portal. Calling the Stripe API to pause a subscription sets pause_collection: { behavior: 'keep_as_draft' } which stops billing without canceling. In the webhook handler, when a subscription is paused, mark the corresponding upcoming shipment as skipped. Resume by removing the pause_collection setting.

Referral program with gift subscriptions

Add a Gift button that lets subscribers purchase a gift subscription for someone else. Create a gift_subscriptions table with recipient email, duration in months, and a unique redemption code. When the recipient redeems the code, create a Stripe subscription with a coupon for free months. Track referral credits in a separate credits table.

Inventory management integration

Add an inventory table listing all items available for box assembly. Connect to the box_contents system so each monthly box has a defined contents list. Add available quantity tracking. In the admin dashboard, show inventory status for upcoming dispatch dates and alert when stock is low for a planned item. Integrate with a supplier order form when stock needs replenishing.

Common pitfalls

Pitfall: Creating shipment records when a subscription is created instead of when an invoice is paid

How to avoid: Create shipment records only when invoice.paid fires in the webhook handler. Stripe sends invoice.paid for both the first invoice and all subsequent renewals. Upsert with stripe_invoice_id as the conflict key to handle webhook retries safely.

Pitfall: Not syncing subscription status changes to Supabase from webhooks

How to avoid: Handle customer.subscription.updated and customer.subscription.deleted webhooks and update the box_subscriptions.status in Supabase accordingly. Always read subscription status from your Supabase table, never directly from Stripe in the frontend.

Pitfall: Calculating dispatch dates client-side on the fly

How to avoid: Calculate and store dispatch_date in the shipments table at creation time (when the invoice.paid webhook fires). The stored date is permanent and reflects the offset that was in effect at that time, regardless of future changes to dispatch_offset_days.

Pitfall: Allowing subscribers to change their shipping address after the cutoff date

How to avoid: Add a shipping_address_locked_at column to shipments. When a shipment reaches packed status, lock address changes for that shipment. Show the cutoff date in the portal: 'Address changes for your next box are accepted until {cutoff_date}'.

Best practices

  • Create shipments only when invoice.paid fires — never at subscription creation. This ensures shipments only exist for paid billing cycles.
  • Use upsert with stripe_invoice_id as the conflict key in the webhook handler to prevent duplicate shipments from webhook retries.
  • Store dispatch_date at shipment creation time rather than calculating it dynamically. Stored dates are immutable and correct regardless of future plan changes.
  • Display the next billing date and dispatch date prominently in the subscriber portal. Most subscription box support requests are 'when is my next box?'
  • Implement a monthly box cutoff date visible to subscribers. After this date, changes to preferences and shipping address apply to the following month's box, not the current one.
  • Track shipment status changes with timestamps (packed_at, shipped_at, delivered_at columns) rather than just the current status. This gives you fulfillment speed analytics.
  • Show subscribers the retail value of their box and the savings versus buying individually. This reinforces the value perception and reduces churn.
  • Use Stripe's Smart Retries for failed renewals. Configure the retry schedule in Stripe Dashboard → Settings → Billing to balance revenue recovery with subscriber experience.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a subscription box service in a Lovable app where Stripe billing cycles trigger fulfillment records in Supabase. Explain how Stripe invoicing works for subscriptions: when is an invoice created, when is it paid, and what events fire in what order during the first billing cycle vs subsequent renewals? Show me the Stripe webhook events I need to subscribe to for a complete subscription lifecycle and what data each event contains.

Lovable Prompt

Add a shipment tracking status component to the subscriber portal. Create a horizontal step indicator with four steps: 'Box Created', 'Being Packed', 'Shipped', and 'Delivered'. The active step has a filled circle icon, completed steps have checkmarks, and future steps have empty circles. Below the step indicator, show the date of the most recent status change. If the status is Shipped, show the tracking number in a monospace font with a copy-to-clipboard button and a link to the carrier tracking page. If the status is Delivered, show the delivery date and a Card asking 'How was your box?' with 5 star rating buttons.

Build Prompt

In Supabase, create a SQL view upcoming_shipments that joins shipments with box_subscriptions (for shipping address) and box_plans (for plan name) and auth.users (for subscriber email). Filter to shipments where status in ('created', 'packed') and dispatch_date <= current_date + 30. Order by dispatch_date ASC. Grant SELECT to service role only. Also create a function count_active_subscribers() that returns the count of box_subscriptions where status='active', used in admin dashboard metric cards.

Frequently asked questions

How do I synchronize billing cycles with my monthly dispatch schedule?

The most common approach is to anchor all subscriptions to a fixed billing date, or to accept that different subscribers have different billing dates and dispatch boxes rolling throughout the month. The webhook-driven approach in this guide handles rolling billing naturally — each invoice.paid event triggers a shipment with a dispatch_date calculated from that invoice's period_start plus the configured offset. For fixed monthly dispatch (e.g., all boxes ship on the 1st), set billing_cycle_anchor on Stripe subscriptions to the cutoff date.

What happens if Stripe fails to collect payment for a renewal?

Stripe Smart Retries attempts the payment over several days. During retries, no invoice.paid event fires, so no new shipment is created — you do not ship unpaid boxes. If retries are exhausted, Stripe fires invoice.payment_failed and may update the subscription to past_due or unpaid. Your webhook updates box_subscriptions.status accordingly. Subscribers in past_due status see a payment update prompt in their portal.

Can I offer different billing frequencies like quarterly or annual?

Yes. Create additional Stripe Prices with interval: quarter or interval: year and add them to your box_plans table. For annual subscriptions, your webhook creates 12 pending shipments at once (one per month) when the annual invoice is paid, or creates shipments monthly using a scheduled Edge Function that checks which annual subscribers are in an active billing period.

How do I handle subscribers who want to skip a month?

Add a Skip button to the subscriber portal. Calling the Stripe API to pause the subscription for one cycle (pause_collection: { behavior: 'keep_as_draft', resumes_at: next_period_start }) skips billing for that period. No invoice.paid fires, so no shipment is created. Display the skipped period in the shipment history as a row with status 'Skipped'.

Can I change box contents month-to-month?

Yes. The box_contents table links items to a billing_month column (e.g., 2026-04). Each month you add new content rows for the next month's box. The subscriber portal can show 'Next month's box' contents after a reveal date. The admin can manage monthly contents from a simple CRUD interface built on the box_contents table.

How do I handle address validation for international subscribers?

Add a country Select to the shipping address form with a curated list of countries you ship to. For address validation, integrate with a carrier address validation API (FedEx, USPS, or a third-party service like EasyPost) in an Edge Function before saving the address. Display inline validation errors for invalid postal codes or addresses the carrier cannot deliver to.

Is there help available for building a larger subscription box platform?

RapidDev builds production subscription box platforms in Lovable including inventory management, multi-warehouse fulfillment, and complex tier configurations. Contact us if you need a full-featured subscription commerce solution.

How do I track customer lifetime value and churn for the subscription box?

Calculate LTV from your Supabase data: SUM of amount_paid from all paid invoices per subscriber. Calculate churn monthly as (subscribers who canceled this month) / (active subscribers at start of month). Build these as SQL aggregation functions and display on the admin dashboard. Recharts AreaCharts showing LTV distribution and monthly churn rate are the two most useful metrics for a subscription box business.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.