Skip to content

📝 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:

ComponentTechnologyPurpose
🔐 FlowlessManaged Service (pubflow.com)Authentication backend - handles user registration, login, sessions, OAuth
FlowfullNode.js (flowfull-node)Your custom backend with business logic - Posts CRUD, comments, user profiles
🎨 Flowfull ClientsReact (@pubflow/react)Your frontend - React components, hooks, routing
💳 Bridge PaymentsManaged 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:

  1. Frontend (React) → Your Backend (Flowfull/flowfull-node)
  2. Your Backend → Flowless (pubflow.com) for session validation
  3. Flowless → Your Backend (Trust Token with user data)
  4. 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

  1. Visit pubflow.com
  2. Sign up or log in
  3. Click "Create Flowless Instance"
  4. Name it: blog-auth
  5. Copy your Bridge Secret (you'll need this later)

Your Flowless URL will be:

https://blog-auth.pubflow.com

1.2 Configure Email Templates (Optional)

Enable email verification and password reset:

  1. Go to Instance Settings → Email
  2. Enable "Email Verification"
  3. Enable "Password Reset"
  4. Customize email templates (optional)

📖 Learn More

For detailed Flowless setup, see Flowless Getting Started

1.3 Test Authentication

Use the API Playground to test:

bash
# 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.

View Documentation →

bash
# 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 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://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 1
  • BRIDGE_VALIDATION_SECRET: Bridge secret from Pubflow dashboard
  • DATABASE_URL: Your PostgreSQL/MySQL/LibSQL connection string

2.3 Create Database Schema

Create schema.sql:

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:

bash
psql $DATABASE_URL < schema.sql

2.4 Implement Posts API

Create src/routes/posts.ts:

typescript
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 posts

2.5 Implement Comments API

Create src/routes/comments.ts:

typescript
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.

View Documentation →

bash
# 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 install

3.2 Configure Environment

Copy the example environment file:

bash
cp .env.example .env.local

Edit .env.local with your backend URL:

env
# 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:

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:

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

  1. Go to Pubflow Dashboard
  2. Create a Bridge Payments instance
  3. 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:

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

bash
# Install Railway CLI
npm install -g @railway/cli

# Login and deploy
railway login
railway init
railway up

Option 2: Render

bash
# Create render.yaml
services:
  - type: web
    name: blog-backend
    env: node
    buildCommand: npm install
    startCommand: npm start

5.2 Deploy React Frontend

Option 1: Vercel

bash
# Install Vercel CLI
npm install -g vercel

# Deploy
vercel

Option 2: Netlify

bash
# Install Netlify CLI
npm install -g netlify-cli

# Deploy
netlify deploy --prod

5.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:

typescript
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