📊 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
| Component | Technology | Purpose |
|---|---|---|
| 🔐 Flowless | Managed Service (pubflow.com) | Authentication - Team auth, OAuth, roles |
| ⚡ Flowfull | Node.js (flowfull-node) | Your custom backend - Analytics, webhooks, API keys, teams |
| 🎨 Flowfull Clients | React (@pubflow/react) | Your frontend - Dashboard with charts and analytics |
| 💳 Bridge Payments | Managed Service | Payment 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:
- Frontend (React) → Your Backend (Flowfull/flowfull-node)
- Your Backend → Flowless for authentication
- Your Backend → Bridge Payments for subscription billing
- 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
- Visit pubflow.com
- Sign up or log in
- Click "Create Flowless Instance"
- Name it:
saas-auth - 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.
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 install2.2 Configure Environment Variables
Copy the example environment file:
bash
cp .env.example .envEdit .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 1BRIDGE_VALIDATION_SECRET: Bridge secret from Pubflow dashboardDATABASE_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
📚 Related Resources
- 🔐 Flowless Documentation - Authentication
- ⚡ Flowfull Documentation - Backend
- 💳 Bridge Payments - Billing
- 💬 Discord - Community