Skip to content

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)
);

Released under the ISC License.