Browse Templates/Subscription Box Service

Subscription Box Service

A complete subscription box service database schema for businesses like Birchbox, HelloFresh, or Loot Crate. Includes subscriber management with flexible recurring plans, curated box assembly with individual item tracking, shipment lifecycle management, and subscription lifecycle handling with pause/cancel support. Designed for subscription commerce platforms that ship physical goods on a recurring schedule.

PostgreSQL10 tablesE-Commerce
subscriptionrecurringboxshipmentse-commerceplans
Created with ERD Studio

About this schema

boxes is what makes this different from an ordinary subscription. Each billing period produces a box row with its own period and contents, so the subscription is the agreement and the box is the thing that was actually curated, packed and shipped — and a subscriber's history is a list of boxes rather than a list of charges.

subscription_pauses is a table rather than a status, because a pause has a shape: a reason, when it started, when it is due to resume and when it actually did. Recording it as a status value would answer whether someone is paused and nothing about how often or for how long.

box_items copies retail_value from the product. The appeal of a box is that its contents are worth more than the price paid, and that claim has to stay true for a box shipped last March even after the products have been repriced.

Key relationships

subscribers → subscriptions ← plans
The plan supplies price, billing_interval and items_count; the subscription is one person on that plan with its own next_billing_at.
subscriptions → boxes → box_items → products
The agreement, the period's box, and what went in it. The product link survives while the value is frozen on the item.
subscriptions → subscription_pauses
A dated row per pause with resume_at and resumed_at, so skipped periods are explainable rather than just missing.
boxes → shipments
Fulfilment attaches to the box, not the subscription, because each period ships separately with its own tracking.
subscriptions → payments
Charges key on the subscription with their own billing period, so a failed payment is visible without a box existing.
boxes → reviews (favorite_item_id)
The review points at one item inside the box, which is what tells a curator which product actually landed.

Design decisions

The box is a row, not an implied shipment

A subscription with a payment and a shipment per period would work and would lose the curation. The box has a name, a period, a total value and contents chosen for that month, and the subscriber's relationship is with those boxes. Creating it explicitly is also what lets a box be assembled and reviewed before it ships, which a payment-driven model cannot express.

Pauses are rows with their own dates

A paused status on the subscription is one column and answers one question. A pause table records why, how long and whether the resume actually happened, which is what turns churn analysis into something answerable — a pause that never resumes is a cancellation nobody labelled. The cost is that the current state has to consider both the subscription status and any open pause.

Retail value is copied onto the box item

Reading retail_price from the product would restate the value of every past box when a product's price changes, and would break entirely when a product is retired. Freezing it per item keeps the box's stated value permanently true, which matters because that number is the marketing claim the subscriber bought on.

Payments key on the subscription, shipments on the box

They are separate events that fail separately. A card can decline while the box has already been packed, and a box can ship while a payment is still settling. Attaching both to the subscription would conflate them; attaching both to the box would leave a failed payment with nowhere to live when no box was created.

Tables in this template

subscribersSubscribers
9 cols
plansPlans
9 cols
subscriptionsSubscriptions
9 cols
subscription_pausesSubscription Pauses
6 cols
boxesBoxes
8 cols
box_itemsBox Items
5 cols
productsProducts
11 cols
shipmentsShipments
10 cols
paymentsPayments
10 cols
reviewsReviews
8 cols

Frequently asked

Why not derive boxes from payments?
Because a box is curated before it is charged for, and sometimes a period produces a payment with no box or a box with no payment — a replacement, a gift, a skipped month. Each needs its own row to be representable.
How does a pause affect billing?
The application has to push next_billing_at forward and skip box creation while an open pause covers the period. The schema records the pause; it does not enforce what billing does with it.
Where is inventory for the products?
products.stock_quantity, with no movement log behind it. For a business that commits a product to five thousand boxes at once, that single number is the first thing to outgrow — allocation per box is what prevents promising stock twice.
Why does a review point at a favourite item?
Because the useful signal for a curator is which product worked, not just how the box scored. favorite_item_id turns a rating into something that changes what goes in next month's box.
Can a subscriber get a box out of cycle?
Yes — boxes keys on the subscription and carries its own period, so a replacement or a one-off box is just another row. Nothing requires a box to correspond to a payment.