SaaS Platform Use Case
Build a multi-tenant SaaS backend with Backflow.
Overview
This guide demonstrates building a SaaS platform with:
- Multi-tenant organization management
- Subscription billing with Stripe
- User roles and permissions
- Usage tracking and limits
- Team invitations
Route Configuration
json
{
"routeAliases": {
"orgs": "/api/v1/organizations",
"members": "/api/v1/members",
"subscriptions": "/api/v1/subscriptions",
"usage": "/api/v1/usage"
}
}Organizations
Create Organization
json
{
"path": "/organizations",
"method": "post",
"requireAuth": true,
"validation": {
"body": {
"name": { "type": "string", "required": true },
"slug": { "type": "string", "pattern": "^[a-z0-9-]+$" }
}
},
"supabaseQueries": [
{
"table": "organizations",
"operation": "insert",
"data": {
"name": "{{body.name}}",
"slug": "{{body.slug}}",
"owner_id": "{{auth.sub}}"
},
"select": "*",
"alias": "org"
},
{
"table": "organization_members",
"operation": "insert",
"data": {
"organization_id": "{{results.org.id}}",
"user_id": "{{auth.sub}}",
"role": "owner"
}
}
]
}Get Organization
json
{
"path": "/organizations/:slug",
"method": "get",
"requireAuth": true,
"supabaseQueries": [{
"table": "organizations",
"operation": "select",
"select": "*, organization_members(user_id, role, users(name, email))",
"filters": [
{ "column": "slug", "operator": "eq", "value": "{{params.slug}}" }
],
"single": true
}]
}Team Members
Invite Member
json
{
"path": "/organizations/:orgId/invites",
"method": "post",
"requireAuth": true,
"rbac": {
"roles": ["owner", "admin"]
},
"validation": {
"body": {
"email": { "type": "string", "format": "email", "required": true },
"role": { "type": "string", "enum": ["admin", "member", "viewer"] }
}
},
"workflow": {
"steps": [
{
"id": "create-invite",
"type": "database",
"params": {
"table": "invitations",
"operation": "insert",
"data": {
"organization_id": "{{params.orgId}}",
"email": "{{body.email}}",
"role": "{{body.role || 'member'}}",
"token": "{{uuid()}}",
"expires_at": "{{dateAdd(now(), '7d')}}"
}
}
},
{
"id": "send-invite",
"type": "integration",
"integration": "sendgrid",
"action": "sendTemplate",
"params": {
"to": "{{body.email}}",
"templateId": "team-invite",
"dynamicTemplateData": {
"inviteUrl": "{{env.APP_URL}}/invite/{{steps.create-invite.result.token}}",
"orgName": "{{context.organization.name}}"
}
}
}
]
}
}Accept Invitation
json
{
"path": "/invites/:token/accept",
"method": "post",
"requireAuth": true,
"supabaseQueries": [
{
"table": "invitations",
"operation": "select",
"filters": [
{ "column": "token", "operator": "eq", "value": "{{params.token}}" },
{ "column": "expires_at", "operator": "gt", "value": "now()" },
{ "column": "accepted_at", "operator": "is", "value": null }
],
"single": true,
"alias": "invite"
},
{
"table": "organization_members",
"operation": "insert",
"condition": "{{results.invite}}",
"data": {
"organization_id": "{{results.invite.organization_id}}",
"user_id": "{{auth.sub}}",
"role": "{{results.invite.role}}"
}
},
{
"table": "invitations",
"operation": "update",
"condition": "{{results.invite}}",
"data": { "accepted_at": "now()" },
"filters": [
{ "column": "id", "operator": "eq", "value": "{{results.invite.id}}" }
]
}
]
}Subscriptions
Create Subscription
json
{
"path": "/organizations/:orgId/subscription",
"method": "post",
"requireAuth": true,
"rbac": { "roles": ["owner"] },
"workflow": {
"steps": [
{
"id": "create-customer",
"type": "integration",
"integration": "stripe",
"action": "createCustomer",
"params": {
"email": "{{auth.email}}",
"metadata": { "org_id": "{{params.orgId}}" }
}
},
{
"id": "create-subscription",
"type": "integration",
"integration": "stripe",
"action": "createSubscription",
"params": {
"customer": "{{steps.create-customer.result.id}}",
"price": "{{body.price_id}}",
"payment_behavior": "default_incomplete",
"expand": ["latest_invoice.payment_intent"]
}
},
{
"id": "save-subscription",
"type": "database",
"params": {
"table": "subscriptions",
"operation": "insert",
"data": {
"organization_id": "{{params.orgId}}",
"stripe_customer_id": "{{steps.create-customer.result.id}}",
"stripe_subscription_id": "{{steps.create-subscription.result.id}}",
"status": "{{steps.create-subscription.result.status}}",
"plan": "{{body.plan}}"
}
}
}
]
}
}Get Subscription Status
json
{
"path": "/organizations/:orgId/subscription",
"method": "get",
"requireAuth": true,
"supabaseQueries": [{
"table": "subscriptions",
"operation": "select",
"select": "*, usage_records(feature, used, limit)",
"filters": [
{ "column": "organization_id", "operator": "eq", "value": "{{params.orgId}}" }
],
"single": true
}],
"cache": { "enabled": true, "ttl": 60 }
}Usage Tracking
Track Usage
json
{
"path": "/usage/track",
"method": "post",
"requireAuth": true,
"validation": {
"body": {
"feature": { "type": "string", "required": true },
"quantity": { "type": "number", "default": 1 }
}
},
"supabaseQueries": [{
"table": "usage_records",
"operation": "upsert",
"data": {
"organization_id": "{{auth.tenant_id}}",
"feature": "{{body.feature}}",
"used": "used + {{body.quantity}}",
"period": "{{formatDate(now(), 'YYYY-MM')}}"
},
"onConflict": ["organization_id", "feature", "period"]
}]
}Check Limit
json
{
"path": "/usage/check/:feature",
"method": "get",
"requireAuth": true,
"supabaseQueries": [{
"table": "usage_records",
"operation": "select",
"select": "used, limit, (limit - used) as remaining",
"filters": [
{ "column": "organization_id", "operator": "eq", "value": "{{auth.tenant_id}}" },
{ "column": "feature", "operator": "eq", "value": "{{params.feature}}" },
{ "column": "period", "operator": "eq", "value": "{{formatDate(now(), 'YYYY-MM')}}" }
],
"single": true
}]
}Billing Webhooks
json
{
"webhooks": [{
"name": "stripe-billing",
"path": "/webhooks/stripe",
"provider": "stripe",
"secret": "{{env.STRIPE_WEBHOOK_SECRET}}",
"actions": [
{
"condition": "{{payload.type}} === 'invoice.paid'",
"workflow": {
"steps": [
{
"id": "reset-usage",
"type": "database",
"params": {
"table": "usage_records",
"operation": "update",
"data": { "used": 0 },
"filters": [
{ "column": "organization_id", "operator": "eq", "value": "{{payload.data.object.metadata.org_id}}" }
]
}
}
]
}
},
{
"condition": "{{payload.type}} === 'customer.subscription.deleted'",
"workflow": {
"steps": [{
"id": "cancel-subscription",
"type": "database",
"params": {
"table": "subscriptions",
"operation": "update",
"data": { "status": "canceled", "canceled_at": "now()" },
"filters": [
{ "column": "stripe_subscription_id", "operator": "eq", "value": "{{payload.data.object.id}}" }
]
}
}]
}
}
]
}]
}Database Schema
sql
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
owner_id UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id),
user_id UUID NOT NULL,
role TEXT DEFAULT 'member',
joined_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(organization_id, user_id)
);
CREATE TABLE invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id),
email TEXT NOT NULL,
role TEXT DEFAULT 'member',
token TEXT UNIQUE NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
accepted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id),
stripe_customer_id TEXT,
stripe_subscription_id TEXT,
plan TEXT,
status TEXT DEFAULT 'active',
canceled_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE usage_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id),
feature TEXT NOT NULL,
used INTEGER DEFAULT 0,
limit INTEGER,
period TEXT NOT NULL,
UNIQUE(organization_id, feature, period)
);