Skip to content

📊 SaaS Dashboard

Build a complete SaaS platform with team management, API keys, analytics, and billing.

📖 Cross-Ecosystem Example

This is a complete tutorial demonstrating the standard Flowfull architecture pattern using flowfull-node for your custom SaaS backend.

🎯 What You'll Build

A production-ready SaaS dashboard with:

  • ✅ Team management with role-based access
  • ✅ API key generation and management
  • ✅ Usage analytics and metrics
  • ✅ Webhook management
  • ✅ Team billing and subscriptions
  • ✅ Activity logs and audit trail
  • ✅ Admin panel

Time to complete: ~150 minutes


🛠️ Tech Stack

ComponentTechnologyPurpose
🔐 FlowlessManaged Service (pubflow.com)Authentication - Team auth, OAuth, roles
FlowfullNode.js (flowfull-node)Your custom backend - Analytics, webhooks, API keys, teams
🎨 Flowfull ClientsReact (@pubflow/react)Your frontend - Dashboard with charts and analytics
💳 Bridge PaymentsManaged ServicePayment processing - Team subscriptions, usage billing

🏗️ Standard Architecture Pattern

  • Flowless = Authentication backend (managed, no code needed)
  • Flowfull = Your custom backend using flowfull-node (you write this)
  • Flowfull Clients = Your frontend using @pubflow/react (you write this)
  • Bridge Payments = Payment backend (managed, API integration only)

🏗️ Architecture

Flow:

  1. Frontend (React) → Your Backend (Flowfull/flowfull-node)
  2. Your Backend → Flowless for authentication
  3. Your Backend → Bridge Payments for subscription billing
  4. Bridge Payments → Your Backend (webhooks for billing events)

📋 Prerequisites

  • Bun v1.0+ (Install) or Node.js 18+
  • Database - PostgreSQL, MySQL, or LibSQL/Turso
  • Pubflow account - Sign up at pubflow.com
  • Code Editor - VS Code, Cursor, or your favorite editor
  • ✅ Basic knowledge of React and TypeScript

🚀 Step 1: Setup Flowless Authentication (5 min)

1.1 Create Flowless Instance

  1. Visit pubflow.com
  2. Sign up or log in
  3. Click "Create Flowless Instance"
  4. Name it: saas-auth
  5. Copy your Bridge Secret

Your Flowless URL will be:

https://saas-auth.pubflow.com

📖 Learn More

For detailed Flowless setup, see Flowless Getting Started


⚡ Step 2: Setup Flowfull Backend (10 min)

2.1 Clone flowfull-node (Official Starter Kit)

📦 Official Starter Kit

flowfull-node is the official, production-ready starter kit maintained by the Pubflow team.

View Documentation →

bash
# Clone the official flowfull-node template
git clone https://github.com/pubflow/flowfull-node.git saas-backend
cd saas-backend

# Install dependencies
bun install
# or: npm install

2.2 Configure Environment Variables

Copy the example environment file:

bash
cp .env.example .env

Edit .env with your configuration:

env
# Flowless Configuration
FLOWLESS_API_URL=https://saas-auth.pubflow.com
BRIDGE_VALIDATION_SECRET=your_bridge_secret_here

# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/saas

# Server
PORT=3001
NODE_ENV=development

# Optional: Cache
REDIS_URL=redis://localhost:6379

🔧 Environment Variables

  • FLOWLESS_API_URL: Your Flowless instance URL from Step 1
  • BRIDGE_VALIDATION_SECRET: Bridge secret from Pubflow dashboard
  • DATABASE_URL: Your PostgreSQL/MySQL/LibSQL connection string

📊 Step 3: Database Schema

sql
-- Teams
CREATE TABLE teams (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(255) NOT NULL,
  owner_id VARCHAR(255) NOT NULL,
  plan VARCHAR(50) DEFAULT 'free',
  created_at TIMESTAMP DEFAULT NOW()
);

-- Team Members
CREATE TABLE team_members (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
  user_id VARCHAR(255) NOT NULL,
  role VARCHAR(50) DEFAULT 'member',
  created_at TIMESTAMP DEFAULT NOW(),
  UNIQUE(team_id, user_id)
);

-- API Keys
CREATE TABLE api_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
  name VARCHAR(255) NOT NULL,
  key_hash VARCHAR(255) NOT NULL,
  prefix VARCHAR(20) NOT NULL,
  last_used_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Webhooks
CREATE TABLE webhooks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
  url VARCHAR(1000) NOT NULL,
  events JSONB DEFAULT '[]',
  secret VARCHAR(255) NOT NULL,
  active BOOLEAN DEFAULT true,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Analytics Events
CREATE TABLE analytics_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
  event_type VARCHAR(100) NOT NULL,
  metadata JSONB,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Usage Metrics
CREATE TABLE usage_metrics (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
  metric_type VARCHAR(100) NOT NULL,
  value INTEGER NOT NULL,
  period DATE NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

⚡ Step 1: Team Management API

typescript
// src/routes/teams.ts
import { Hono } from 'hono'
import { validateSession } from '../middleware/auth'

const teams = new Hono()

// Create team
teams.post('/', validateSession, async (c) => {
  const user = c.get('user')
  const { name } = await c.req.json()
  
  const team = await db
    .insertInto('teams')
    .values({
      name,
      owner_id: user.id,
      plan: 'free'
    })
    .returningAll()
    .executeTakeFirst()
  
  // Add owner as admin
  await db
    .insertInto('team_members')
    .values({
      team_id: team.id,
      user_id: user.id,
      role: 'admin'
    })
    .execute()
  
  return c.json({ team }, 201)
})

// Get team members
teams.get('/:teamId/members', validateSession, async (c) => {
  const teamId = c.req.param('teamId')
  
  const members = await db
    .selectFrom('team_members')
    .selectAll()
    .where('team_id', '=', teamId)
    .execute()
  
  return c.json({ members })
})

// Invite member
teams.post('/:teamId/invite', validateSession, async (c) => {
  const teamId = c.req.param('teamId')
  const { email, role } = await c.req.json()
  
  // Send invitation email
  // Add to team_members when accepted
  
  return c.json({ success: true })
})

🔑 Step 2: API Keys Management

typescript
// src/routes/api-keys.ts
import { Hono } from 'hono'
import { createHash, randomBytes } from 'crypto'

const apiKeys = new Hono()

// Generate API key
apiKeys.post('/', validateSession, async (c) => {
  const user = c.get('user')
  const { team_id, name } = await c.req.json()
  
  // Generate key
  const key = `sk_${randomBytes(32).toString('hex')}`
  const keyHash = createHash('sha256').update(key).digest('hex')
  const prefix = key.substring(0, 12)
  
  const apiKey = await db
    .insertInto('api_keys')
    .values({
      team_id,
      name,
      key_hash: keyHash,
      prefix
    })
    .returningAll()
    .executeTakeFirst()
  
  // Return full key only once
  return c.json({ api_key: { ...apiKey, key } }, 201)
})

// List API keys
apiKeys.get('/team/:teamId', validateSession, async (c) => {
  const teamId = c.req.param('teamId')
  
  const keys = await db
    .selectFrom('api_keys')
    .select(['id', 'name', 'prefix', 'last_used_at', 'created_at'])
    .where('team_id', '=', teamId)
    .execute()
  
  return c.json({ keys })
})

📊 Step 3: Analytics Dashboard

tsx
// src/pages/analytics.tsx
import { useBridge } from '@pubflow/react'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts'

export default function AnalyticsPage() {
  const { data: metrics } = useBridge('/analytics/metrics?period=7d')
  
  return (
    <div className="p-8">
      <h1 className="text-3xl font-bold mb-8">Analytics</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
        <div className="border rounded-lg p-6">
          <h3 className="text-gray-600 text-sm mb-2">API Calls</h3>
          <p className="text-3xl font-bold">{metrics?.api_calls || 0}</p>
          <span className="text-green-600 text-sm">+12% from last week</span>
        </div>
        
        <div className="border rounded-lg p-6">
          <h3 className="text-gray-600 text-sm mb-2">Active Users</h3>
          <p className="text-3xl font-bold">{metrics?.active_users || 0}</p>
          <span className="text-green-600 text-sm">+8% from last week</span>
        </div>
        
        <div className="border rounded-lg p-6">
          <h3 className="text-gray-600 text-sm mb-2">Success Rate</h3>
          <p className="text-3xl font-bold">{metrics?.success_rate || 0}%</p>
          <span className="text-gray-600 text-sm">Last 7 days</span>
        </div>
      </div>
      
      <div className="border rounded-lg p-6">
        <h2 className="text-xl font-bold mb-4">API Usage</h2>
        <LineChart width={800} height={300} data={metrics?.daily_usage || []}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="date" />
          <YAxis />
          <Tooltip />
          <Line type="monotone" dataKey="calls" stroke="#2196F3" />
        </LineChart>
      </div>
    </div>
  )
}

💳 Step 4: Team Billing

tsx
// src/pages/billing.tsx
import { useState } from 'react'
import { useAuth } from '@pubflow/react'

export default function BillingPage() {
  const { user } = useAuth()
  const [loading, setLoading] = useState(false)
  
  const plans = [
    { name: 'Free', price: 0, calls: 1000 },
    { name: 'Pro', price: 29, calls: 100000 },
    { name: 'Enterprise', price: 299, calls: 'Unlimited' }
  ]
  
  const handleUpgrade = async (plan: string) => {
    setLoading(true)
    
    const response = await fetch('/api/billing/subscribe', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Session-Id': localStorage.getItem('pubflow_session_id') || ''
      },
      body: JSON.stringify({ plan })
    })
    
    const { client_secret } = await response.json()
    window.location.href = `https://checkout.stripe.com/${client_secret}`
  }
  
  return (
    <div className="p-8">
      <h1 className="text-3xl font-bold mb-8">Billing</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {plans.map((plan) => (
          <div key={plan.name} className="border rounded-lg p-6">
            <h3 className="text-xl font-bold mb-2">{plan.name}</h3>
            <p className="text-3xl font-bold mb-4">
              ${plan.price}<span className="text-lg text-gray-600">/mo</span>
            </p>
            <p className="text-gray-600 mb-6">{plan.calls} API calls/month</p>
            <button
              onClick={() => handleUpgrade(plan.name.toLowerCase())}
              disabled={loading}
              className="w-full bg-blue-600 text-white py-2 rounded"
            >
              {loading ? 'Processing...' : 'Upgrade'}
            </button>
          </div>
        ))}
      </div>
    </div>
  )
}

🎉 Result

You've built a complete SaaS dashboard with:

  • ✅ Team management
  • ✅ API key generation
  • ✅ Analytics dashboard
  • ✅ Billing integration

📖 Next Steps

  • 🔔 Notifications - Real-time alerts
  • 📧 Email reports - Weekly summaries
  • 🔒 2FA - Enhanced security
  • 📱 Mobile app - iOS/Android dashboard