Back-EndSchemas
SaaS Schema
Complete MongoDB schemas for a SaaS product — Plan, Subscription, and Usage tracking.
SaaS Schema
Schemas for a subscription-based SaaS product — pricing plans, a subscription linking a user to a plan, and usage tracking for metered features.
Plan Schema
// src/models/plan.model.js
import mongoose from "mongoose";
const planSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
unique: true, // e.g. "Free", "Pro", "Enterprise"
},
price: {
type: Number,
required: true,
min: 0,
},
billingCycle: {
type: String,
enum: ["monthly", "yearly"],
default: "monthly",
},
features: [{ type: String }],
limits: {
projects: { type: Number, default: 3 },
teamMembers: { type: Number, default: 1 },
storageMB: { type: Number, default: 500 },
},
stripePriceId: {
type: String, // links this plan to a Stripe Price object
},
isActive: {
type: Boolean,
default: true,
},
},
{ timestamps: true }
);
const Plan = mongoose.model("Plan", planSchema);
export default Plan;Subscription Schema
// src/models/subscription.model.js
import mongoose from "mongoose";
const subscriptionSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
unique: true, // one active subscription per user
},
plan: {
type: mongoose.Schema.Types.ObjectId,
ref: "Plan",
required: true,
},
status: {
type: String,
enum: ["active", "trialing", "past_due", "cancelled", "expired"],
default: "trialing",
},
stripeCustomerId: {
type: String,
},
stripeSubscriptionId: {
type: String,
},
currentPeriodStart: {
type: Date,
default: Date.now,
},
currentPeriodEnd: {
type: Date,
required: true,
},
cancelAtPeriodEnd: {
type: Boolean,
default: false,
},
},
{ timestamps: true }
);
const Subscription = mongoose.model("Subscription", subscriptionSchema);
export default Subscription;Usage Schema
For metered features — tracking how much of a limited resource a user has consumed within their current billing period.
// src/models/usage.model.js
import mongoose from "mongoose";
const usageSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
metric: {
type: String,
required: true, // e.g. "projects", "apiCalls", "storageMB"
},
count: {
type: Number,
default: 0,
},
// which billing period this usage record belongs to
periodStart: {
type: Date,
required: true,
},
periodEnd: {
type: Date,
required: true,
},
},
{ timestamps: true }
);
// one usage record per user, per metric, per billing period
usageSchema.index({ user: 1, metric: 1, periodStart: 1 }, { unique: true });
const Usage = mongoose.model("Usage", usageSchema);
export default Usage;Checking If a User Has Hit Their Limit
async function checkLimit(userId, metric) {
const subscription = await Subscription.findOne({ user: userId }).populate("plan");
if (!subscription || subscription.status !== "active") {
throw new Error("No active subscription");
}
const limit = subscription.plan.limits[metric];
const usage = await Usage.findOne({
user: userId,
metric,
periodStart: { $lte: new Date() },
periodEnd: { $gte: new Date() },
});
const currentCount = usage ? usage.count : 0;
return {
allowed: currentCount < limit,
currentCount,
limit,
};
}Incrementing Usage
async function incrementUsage(userId, metric, periodStart, periodEnd) {
// upsert — create the usage record if it doesn't exist yet, otherwise increment
await Usage.findOneAndUpdate(
{ user: userId, metric, periodStart },
{
$inc: { count: 1 },
$setOnInsert: { periodEnd },
},
{ upsert: true, new: true }
);
}$setOnInsert only applies those fields when a new document is created by the upsert — it has no effect when updating an existing document. This is the standard pattern for "create or increment" logic.
Summary
Plandefines pricing tiers with alimitsobject — easy to check against when enforcing feature restrictionsSubscriptionlinks a user to a plan and tracks Stripe IDs alongside billing period dates — designed to sync with Stripe webhook eventsUsagetracks metered consumption per user, per metric, per billing period — with a compound unique index to prevent duplicate recordscheckLimit()andincrementUsage()together form the core pattern for enforcing plan limits in a SaaS app- This schema set assumes Stripe for billing —
stripeCustomerId,stripeSubscriptionId, andstripePriceIdare the linking fields