📝 Full-Stack Blog App
Build a complete blog application with authentication, posts, comments, and premium subscriptions.
📖 Cross-Ecosystem Example
This is a complete tutorial showing how to build a full-stack application using the Pubflow ecosystem. It demonstrates the standard Flowfull architecture pattern using flowfull-node for the backend.
🎯 What You'll Build
A production-ready blog platform with:
- ✅ User authentication (register, login, logout)
- ✅ Create, edit, and delete blog posts
- ✅ Comments system with nested replies
- ✅ Premium subscriptions ($9.99/month)
- ✅ Admin dashboard for content moderation
- ✅ Rich text editor with image uploads
- ✅ SEO-optimized with meta tags
- ✅ Responsive design (mobile + desktop)
Time to complete: ~90 minutes
🛠️ Tech Stack
This example demonstrates the complete Pubflow ecosystem:
| Component | Technology | Purpose |
|---|---|---|
| 🔐 Flowless | Managed Service (pubflow.com) | Authentication backend - handles user registration, login, sessions, OAuth |
| ⚡ Flowfull | Node.js (flowfull-node) | Your custom backend with business logic - Posts CRUD, comments, user profiles |
| 🎨 Flowfull Clients | React (@pubflow/react) | Your frontend - React components, hooks, routing |
| 💳 Bridge Payments | Managed Service (optional) | Payment processing - Monthly billing, payment methods |
🏗️ 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)
🏗️ Architecture
Flow:
- Frontend (React) → Your Backend (Flowfull/flowfull-node)
- Your Backend → Flowless (pubflow.com) for session validation
- Flowless → Your Backend (Trust Token with user data)
- Your Backend → Frontend (Protected data)
📋 Prerequisites
Before starting, make sure you have:
- ✅ 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:
blog-auth - Copy your Bridge Secret (you'll need this later)
Your Flowless URL will be:
https://blog-auth.pubflow.com1.2 Configure Email Templates (Optional)
Enable email verification and password reset:
- Go to Instance Settings → Email
- Enable "Email Verification"
- Enable "Password Reset"
- Customize email templates (optional)
📖 Learn More
For detailed Flowless setup, see Flowless Getting Started
1.3 Test Authentication
Use the API Playground to test:
# Register a test user
curl -X POST https://your-instance.pubflow.com/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "SecurePass123!",
"name": "Test User"
}'✅ Checkpoint: You should receive a session_id in the response.
⚡ Step 2: Build Flowfull Backend (30 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. It includes all core features, best practices, and is regularly updated.
# Clone the official flowfull-node template
git clone https://github.com/pubflow/flowfull-node.git blog-backend
cd blog-backend
# Install dependencies
bun install
# or: npm install2.2 Configure Environment Variables
Copy the example environment file:
cp .env.example .envEdit .env with your configuration:
# Flowless Configuration
FLOWLESS_API_URL=https://blog-auth.pubflow.com
BRIDGE_VALIDATION_SECRET=your_bridge_secret_here
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/blog
# Server
PORT=3001
NODE_ENV=development
# Optional: Cache (for better performance)
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
2.3 Create Database Schema
Create schema.sql:
-- Posts table
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(255) NOT NULL,
title VARCHAR(500) NOT NULL,
slug VARCHAR(500) UNIQUE NOT NULL,
content TEXT NOT NULL,
excerpt TEXT,
cover_image VARCHAR(1000),
published BOOLEAN DEFAULT false,
premium_only BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Comments table
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
parent_id UUID REFERENCES comments(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_slug ON posts(slug);
CREATE INDEX idx_comments_post_id ON comments(post_id);
CREATE INDEX idx_comments_parent_id ON comments(parent_id);Run migrations:
psql $DATABASE_URL < schema.sql2.4 Implement Posts API
Create src/routes/posts.ts:
import { Hono } from 'hono'
import { validateSession } from '../middleware/auth'
import { db } from '../db'
const posts = new Hono()
// Get all posts (public)
posts.get('/', async (c) => {
const posts = await db
.selectFrom('posts')
.selectAll()
.where('published', '=', true)
.orderBy('created_at', 'desc')
.execute()
return c.json({ posts })
})
// Get single post by slug
posts.get('/:slug', async (c) => {
const slug = c.req.param('slug')
const post = await db
.selectFrom('posts')
.selectAll()
.where('slug', '=', slug)
.where('published', '=', true)
.executeTakeFirst()
if (!post) {
return c.json({ error: 'Post not found' }, 404)
}
return c.json({ post })
})
// Create post (authenticated)
posts.post('/', validateSession, async (c) => {
const user = c.get('user')
const { title, content, excerpt, cover_image, premium_only } = await c.req.json()
// Generate slug from title
const slug = title.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
const post = await db
.insertInto('posts')
.values({
user_id: user.id,
title,
slug,
content,
excerpt,
cover_image,
premium_only: premium_only || false,
published: false
})
.returningAll()
.executeTakeFirst()
return c.json({ post }, 201)
})
// Update post (authenticated, owner only)
posts.put('/:id', validateSession, async (c) => {
const user = c.get('user')
const id = c.req.param('id')
const { title, content, excerpt, cover_image, published, premium_only } = await c.req.json()
// Check ownership
const existing = await db
.selectFrom('posts')
.select('user_id')
.where('id', '=', id)
.executeTakeFirst()
if (!existing || existing.user_id !== user.id) {
return c.json({ error: 'Unauthorized' }, 403)
}
const post = await db
.updateTable('posts')
.set({
title,
content,
excerpt,
cover_image,
published,
premium_only,
updated_at: new Date()
})
.where('id', '=', id)
.returningAll()
.executeTakeFirst()
return c.json({ post })
})
// Delete post (authenticated, owner only)
posts.delete('/:id', validateSession, async (c) => {
const user = c.get('user')
const id = c.req.param('id')
const existing = await db
.selectFrom('posts')
.select('user_id')
.where('id', '=', id)
.executeTakeFirst()
if (!existing || existing.user_id !== user.id) {
return c.json({ error: 'Unauthorized' }, 403)
}
await db
.deleteFrom('posts')
.where('id', '=', id)
.execute()
return c.json({ success: true })
})
export default posts2.5 Implement Comments API
Create src/routes/comments.ts:
import { Hono } from 'hono'
import { validateSession } from '../middleware/auth'
import { db } from '../db'
const comments = new Hono()
// Get comments for a post
comments.get('/post/:postId', async (c) => {
const postId = c.req.param('postId')
const comments = await db
.selectFrom('comments')
.selectAll()
.where('post_id', '=', postId)
.orderBy('created_at', 'asc')
.execute()
return c.json({ comments })
})
// Create comment (authenticated)
comments.post('/', validateSession, async (c) => {
const user = c.get('user')
const { post_id, parent_id, content } = await c.req.json()
const comment = await db
.insertInto('comments')
.values({
post_id,
parent_id: parent_id || null,
user_id: user.id,
content
})
.returningAll()
.executeTakeFirst()
return c.json({ comment }, 201)
})
// Delete comment (authenticated, owner only)
comments.delete('/:id', validateSession, async (c) => {
const user = c.get('user')
const id = c.req.param('id')
const existing = await db
.selectFrom('comments')
.select('user_id')
.where('id', '=', id)
.executeTakeFirst()
if (!existing || existing.user_id !== user.id) {
return c.json({ error: 'Unauthorized' }, 403)
}
await db
.deleteFrom('comments')
.where('id', '=', id)
.execute()
return c.json({ success: true })
})
export default comments✅ Checkpoint: Test your APIs with curl or Postman.
🎨 Step 3: Create React Frontend (40 min)
3.1 Clone flowfull-client (TanStack Start Starter Kit)
🎨 Official Frontend Starter
flowfull-client is the official React starter kit with TanStack Start, featuring complete authentication, routing, and professional UI with Tailwind CSS + shadcn/ui.
# Clone the official TanStack Start starter kit
git clone https://github.com/pubflow/flowfull-client.git blog-frontend
cd blog-frontend
# Install dependencies
npm install
# or: bun install3.2 Configure Environment
Copy the example environment file:
cp .env.example .env.localEdit .env.local with your backend URL:
# API Configuration
VITE_API_BASE_URL=http://localhost:3001
VITE_BRIDGE_BASE_PATH=/bridge
VITE_AUTH_BASE_PATH=/auth
# Branding Configuration (Optional)
VITE_APP_NAME=My Blog
VITE_APP_LOGO=https://your-domain.com/logo.png
VITE_PRIMARY_COLOR=#006aff
VITE_SECONDARY_COLOR=#4a90e2🔧 Environment Variables
VITE_API_BASE_URL: Your Flowfull backend URL (from Step 2)VITE_BRIDGE_BASE_PATH: Bridge API path (default:/bridge)VITE_AUTH_BASE_PATH: Auth API path (default:/auth)
3.3 Create Blog Post List
Create src/pages/blog.tsx:
import { useBridge } from '@pubflow/react'
import { Link } from '@tanstack/react-router'
export default function BlogPage() {
const { data: posts, loading } = useBridge('/posts')
if (loading) return <div>Loading...</div>
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
<div className="space-y-6">
{posts?.map((post: any) => (
<article key={post.id} className="border rounded-lg p-6">
{post.cover_image && (
<img
src={post.cover_image}
alt={post.title}
className="w-full h-48 object-cover rounded mb-4"
/>
)}
<h2 className="text-2xl font-bold mb-2">
<Link to={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">
{new Date(post.created_at).toLocaleDateString()}
</span>
{post.premium_only && (
<span className="bg-yellow-100 text-yellow-800 px-3 py-1 rounded text-sm">
Premium
</span>
)}
</div>
</article>
))}
</div>
</div>
)
}3.4 Create Post Editor
Create src/pages/editor.tsx:
import { useState } from 'react'
import { useBridge, useAuth } from '@pubflow/react'
import { useNavigate } from '@tanstack/react-router'
export default function EditorPage() {
const { user } = useAuth()
const navigate = useNavigate()
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [excerpt, setExcerpt] = useState('')
const [premiumOnly, setPremiumOnly] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const response = await fetch(`${import.meta.env.VITE_BRIDGE_URL}/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Session-Id': localStorage.getItem('pubflow_session_id') || ''
},
body: JSON.stringify({
title,
content,
excerpt,
premium_only: premiumOnly
})
})
if (response.ok) {
const { post } = await response.json()
navigate({ to: `/blog/${post.slug}` })
}
}
if (!user) {
return <div>Please login to create posts</div>
}
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">Create New Post</h1>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium mb-2">Title</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full border rounded px-4 py-2"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Excerpt</label>
<textarea
value={excerpt}
onChange={(e) => setExcerpt(e.target.value)}
className="w-full border rounded px-4 py-2"
rows={3}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Content</label>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full border rounded px-4 py-2"
rows={15}
required
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={premiumOnly}
onChange={(e) => setPremiumOnly(e.target.checked)}
className="mr-2"
/>
<label className="text-sm">Premium content only</label>
</div>
<button
type="submit"
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
>
Create Post
</button>
</form>
</div>
)
}✅ Checkpoint: You can now create and view blog posts!
💳 Step 4: Add Premium Subscriptions (15 min)
4.1 Setup Bridge Payments
- Go to Pubflow Dashboard
- Create a Bridge Payments instance
- Configure Stripe:
- Add Stripe API keys
- Create a product: "Premium Subscription"
- Set price: $9.99/month
4.2 Create Subscription Page
Create src/pages/premium.tsx:
import { useAuth } from '@pubflow/react'
import { useState } from 'react'
export default function PremiumPage() {
const { user } = useAuth()
const [loading, setLoading] = useState(false)
const handleSubscribe = async () => {
setLoading(true)
try {
// Create payment intent
const response = await fetch(`${import.meta.env.VITE_PAYMENTS_URL}/intents`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Session-Id': localStorage.getItem('pubflow_session_id') || ''
},
body: JSON.stringify({
total_cents: 999, // $9.99
currency: 'USD',
concept: 'Premium Subscription',
recurring: true,
interval: 'month'
})
})
const { client_secret } = await response.json()
// Redirect to Stripe checkout
// (In production, use Stripe.js for better UX)
window.location.href = `https://checkout.stripe.com/${client_secret}`
} catch (error) {
console.error('Subscription failed:', error)
} finally {
setLoading(false)
}
}
return (
<div className="max-w-2xl mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">Go Premium</h1>
<div className="border rounded-lg p-8">
<h2 className="text-2xl font-bold mb-4">Premium Membership</h2>
<p className="text-3xl font-bold mb-6">$9.99<span className="text-lg text-gray-600">/month</span></p>
<ul className="space-y-3 mb-8">
<li className="flex items-center">
<span className="text-green-600 mr-2">✓</span>
Access to all premium articles
</li>
<li className="flex items-center">
<span className="text-green-600 mr-2">✓</span>
Ad-free reading experience
</li>
<li className="flex items-center">
<span className="text-green-600 mr-2">✓</span>
Early access to new content
</li>
<li className="flex items-center">
<span className="text-green-600 mr-2">✓</span>
Support independent writers
</li>
</ul>
<button
onClick={handleSubscribe}
disabled={loading || !user}
className="w-full bg-blue-600 text-white px-6 py-3 rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Processing...' : 'Subscribe Now'}
</button>
{!user && (
<p className="text-sm text-gray-600 mt-4 text-center">
Please login to subscribe
</p>
)}
</div>
</div>
)
}🚀 Step 5: Deploy (10 min)
5.1 Deploy Flowfull Backend
Option 1: Railway
# Install Railway CLI
npm install -g @railway/cli
# Login and deploy
railway login
railway init
railway upOption 2: Render
# Create render.yaml
services:
- type: web
name: blog-backend
env: node
buildCommand: npm install
startCommand: npm start5.2 Deploy React Frontend
Option 1: Vercel
# Install Vercel CLI
npm install -g vercel
# Deploy
vercelOption 2: Netlify
# Install Netlify CLI
npm install -g netlify-cli
# Deploy
netlify deploy --prod5.3 Update Environment Variables
Update production environment variables:
- Frontend:
VITE_BRIDGE_URL→ your deployed backend URL - Backend:
BRIDGE_URL→ your Flowless instance URL
🎉 Result
Congratulations! You've built a complete full-stack blog application using the Pubflow ecosystem.
What you've accomplished:
- ✅ User authentication with Flowless
- ✅ Custom backend API with Flowfull
- ✅ Modern React frontend with Flowfull Clients
- ✅ Payment processing with Bridge Payments
- ✅ Production deployment
🔧 Troubleshooting
Issue: "Session validation failed"
Solution: Make sure your BRIDGE_SECRET matches in both frontend and backend.
Issue: "CORS error"
Solution: Add CORS middleware to your Flowfull backend:
import { cors } from 'hono/cors'
app.use('/*', cors({
origin: ['http://localhost:5173', 'https://yourdomain.com'],
credentials: true
}))Issue: "Payment failed"
Solution: Check your Stripe API keys and webhook configuration.
📖 Next Steps
Enhance your blog with:
- 📸 Image uploads - Add S3 integration for post images
- 🔍 Search functionality - Implement full-text search
- 📊 Analytics - Track post views and engagement
- 💬 Real-time comments - Use WebSockets for live updates
- 🌐 Multi-language - Add i18n support
- 📱 Mobile app - Build React Native version
📚 Related Resources
- 🔐 Flowless Documentation - Learn more about authentication
- ⚡ Flowfull Documentation - Backend framework guide
- 🎨 Flowfull Clients - Frontend libraries
- 💳 Bridge Payments - Payment integration
- 💬 Discord Community - Get help and share your project