Skip to content

Real-World Applications

Examples of complete applications and common patterns built with Flowfull Clients.

Architecture

These examples demonstrate the complete Flowfull ecosystem:

  • @pubflow/react, @pubflow/react-native, or @pubflow/nextjs - For authentication with Flowless
  • @pubflow/flowfull-client - For HTTP requests to your custom Flowfull backend

E-Commerce Platform

A full-featured e-commerce platform with product catalog, shopping cart, and checkout.

Features:

  • User authentication with Flowless
  • Product browsing with search and filters
  • Shopping cart management
  • Order processing and history
  • Admin dashboard for inventory

Tech Stack:

  • Next.js 14 (App Router)
  • @pubflow/nextjs (for authentication)
  • @pubflow/flowfull-client (for backend API)
  • Stripe for payments
  • Tailwind CSS

Key Implementation:

typescript
// app/providers.tsx
'use client';

import { PubflowProvider } from '@pubflow/nextjs';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <PubflowProvider
      config={{
        baseUrl: process.env.NEXT_PUBLIC_FLOWLESS_URL!,
        bridgeBasePath: '/bridge',
        authBasePath: '/auth'
      }}
      loginRedirectPath="/login"
      publicPaths={['/login', '/register', '/products']}
    >
      {children}
    </PubflowProvider>
  );
}

// lib/flowfull.ts
import { createFlowfull } from '@pubflow/flowfull-client';

export const flowfull = createFlowfull(process.env.NEXT_PUBLIC_BACKEND_URL!);

// app/products/page.tsx
'use client';

import { useState, useEffect } from 'react';
import { flowfull } from '@/lib/flowfull';

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  stock: number;
}

export default function ProductsPage() {
  const [products, setProducts] = useState<Product[]>([]);
  const [category, setCategory] = useState('');
  const [minPrice, setMinPrice] = useState('');
  const [maxPrice, setMaxPrice] = useState('');
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetchProducts();
  }, [category, minPrice, maxPrice]);

  async function fetchProducts() {
    setIsLoading(true);

    try {
      let query = flowfull.query('/products')
        .filter('status', 'active')
        .filter('stock', '>', 0);

      if (category) {
        query = query.filter('category', category);
      }
      if (minPrice) {
        query = query.filter('price', '>=', parseFloat(minPrice));
      }
      if (maxPrice) {
        query = query.filter('price', '<=', parseFloat(maxPrice));
      }

      const data = await query
        .orderBy('name', 'asc')
        .limit(20)
        .execute<Product[]>();

      setProducts(data);
    } catch (error) {
      console.error('Error fetching products:', error);
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <div className="grid grid-cols-4 gap-4">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { createServerApi } from '@/lib/api';

export async function POST(request: NextRequest) {
  const cookieStore = cookies();
  const sessionId = cookieStore.get('pubflow_session_id')?.value;

  if (!sessionId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const serverApi = createServerApi(sessionId);
  const { cartItems } = await request.json();

  // Get user profile
  const profileResponse = await serverApi.get('/profile');
  if (!profileResponse.success) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Create order
  const orderResponse = await serverApi.post('/orders', {
    user_id: profileResponse.data.id,
    items: cartItems,
    total: calculateTotal(cartItems)
  });

  if (!orderResponse.success) {
    return NextResponse.json(
      { error: orderResponse.error },
      { status: orderResponse.status }
    );
  }

  return NextResponse.json({ success: true, order: orderResponse.data });
}

function calculateTotal(items: any[]) {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

Task Management App

A collaborative task management application with teams and projects.

Features:

  • User authentication and team management
  • Project and task creation
  • Task status management
  • File attachments
  • Activity timeline

Tech Stack:

  • React (Vite)
  • @pubflow/flowfull-client
  • React Router
  • React DnD for drag-and-drop

Key Implementation:

typescript
// src/api/client.ts
import { createFlowfull } from '@pubflow/flowfull-client';

export const api = createFlowfull(import.meta.env.VITE_API_URL);

// src/pages/ProjectBoard.tsx
import { useState, useEffect } from 'react';
import { api } from '../api/client';

interface Task {
  id: string;
  title: string;
  description: string;
  status: 'todo' | 'in-progress' | 'done';
  project_id: string;
  assigned_to?: string;
}

function ProjectBoard({ projectId }: { projectId: string }) {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetchTasks();
  }, [projectId]);

  async function fetchTasks() {
    setIsLoading(true);

    const response = await api
      .query('/tasks')
      .where('project_id', projectId)
      .sort('created_at', 'desc')
      .get<Task[]>();

    if (response.success) {
      setTasks(response.data || []);
    }

    setIsLoading(false);
  }

  async function handleTaskMove(taskId: string, newStatus: string) {
    // Optimistic update
    setTasks(prev =>
      prev.map(task =>
        task.id === taskId ? { ...task, status: newStatus as any } : task
      )
    );

    const response = await api.patch(`/tasks/${taskId}`, { status: newStatus });

    if (!response.success) {
      // Revert on error
      fetchTasks();
    }
  }

  return (
    <div className="kanban-board">
      <Column status="todo" tasks={tasks.filter(t => t.status === 'todo')} onMove={handleTaskMove} />
      <Column status="in-progress" tasks={tasks.filter(t => t.status === 'in-progress')} onMove={handleTaskMove} />
      <Column status="done" tasks={tasks.filter(t => t.status === 'done')} onMove={handleTaskMove} />
    </div>
  );
}

Social Media App

A mobile social media application with posts, comments, and user profiles.

Features:

  • User authentication and profiles
  • Create and share posts with images
  • Like and comment system
  • Follow/unfollow users
  • Activity feed

Tech Stack:

  • React Native (Expo)
  • @pubflow/flowfull-client
  • React Navigation
  • Expo Image Picker

Key Implementation:

typescript
// src/api/client.ts
import { createFlowfull } from '@pubflow/flowfull-client';

export const api = createFlowfull(process.env.EXPO_PUBLIC_API_URL!);

// src/screens/FeedScreen.tsx
import { useState, useEffect } from 'react';
import { RefreshControl, FlatList, View, Text } from 'react-native';
import { api } from '../api/client';

interface Post {
  id: string;
  user_id: string;
  content: string;
  image_url?: string;
  likes_count: number;
  comments_count: number;
  created_at: string;
}

export default function FeedScreen() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [isRefreshing, setIsRefreshing] = useState(false);

  useEffect(() => {
    fetchPosts();
  }, []);

  async function fetchPosts(isRefresh = false) {
    if (isRefresh) {
      setIsRefreshing(true);
    } else {
      setIsLoading(true);
    }

    const response = await api
      .query('/posts')
      .sort('created_at', 'desc')
      .limit(20)
      .get<Post[]>();

    if (response.success) {
      setPosts(response.data || []);
    }

    setIsLoading(false);
    setIsRefreshing(false);
  }

  async function handleLike(postId: string) {
    // Optimistic update
    setPosts(prev =>
      prev.map(post =>
        post.id === postId
          ? { ...post, likes_count: post.likes_count + 1 }
          : post
      )
    );

    const response = await api.post('/likes', { post_id: postId });

    if (!response.success) {
      // Revert on error
      fetchPosts();
    }
  }

  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <PostCard post={item} onLike={() => handleLike(item.id)} />
      )}
      refreshControl={
        <RefreshControl
          refreshing={isRefreshing}
          onRefresh={() => fetchPosts(true)}
        />
      }
      ListEmptyComponent={
        isLoading ? null : (
          <View style={{ padding: 20, alignItems: 'center' }}>
            <Text>No posts yet</Text>
          </View>
        )
      }
    />
  );
}

// src/screens/CreatePostScreen.tsx
import { useState } from 'react';
import { api } from '../api/client';
import * as ImagePicker from 'expo-image-picker';

export default function CreatePostScreen({ navigation }: any) {
  const [content, setContent] = useState('');
  const [image, setImage] = useState<string | null>(null);

  const postService = useBridgeApi({ endpoint: 'posts' });
  const { mutate: createPost, isLoading } = useBridgeMutation(
    postService,
    'create'
  );

  async function handleSubmit() {
    await createPost({
      data: { content, image }
    });
    navigation.goBack();
  }

  return (
    <View>
      <TextInput
        value={content}
        onChangeText={setContent}
        placeholder="What's on your mind?"
        multiline
      />
      <Button title="Add Image" onPress={pickImage} />
      <Button title="Post" onPress={handleSubmit} disabled={isLoading} />
    </View>
  );
}

Blog Platform

A content management system for blogging with rich text editing.

Features:

  • User authentication (authors, editors, admins)
  • Rich text editor for posts
  • Categories and tags
  • Comments system
  • SEO optimization with SSR

Tech Stack:

  • Next.js 14 (App Router)
  • @pubflow/nextjs
  • MDX Editor
  • Tailwind CSS

Key Implementation:

typescript
// app/blog/[slug]/page.tsx
import { withPubflowSSR } from '@pubflow/nextjs';

export const getServerSideProps = withPubflowSSR(
  async (context, api, auth) => {
    const { slug } = context.params;
    const post = await api.get(`/bridge/posts/${slug}`);
    const comments = await api.get(`/bridge/comments?postId=${post.id}`);

    return {
      props: { post, comments }
    };
  }
);

export default function BlogPost({ post, comments }: any) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      <CommentSection comments={comments} postId={post.id} />
    </article>
  );
}

// app/admin/posts/new/page.tsx
'use client';

import { useBridgeMutation } from '@pubflow/nextjs';
import dynamic from 'next/dynamic';

const MDXEditor = dynamic(() => import('@mdxeditor/editor'), { ssr: false });

export default function NewPost() {
  const [content, setContent] = useState('');
  const postService = useBridgeApi({ endpoint: 'posts' });
  const { mutate: createPost } = useBridgeMutation(postService, 'create');

  async function handlePublish() {
    await createPost({
      data: {
        title,
        content,
        status: 'published'
      }
    });
  }

  return (
    <div>
      <input type="text" placeholder="Title" />
      <MDXEditor markdown={content} onChange={setContent} />
      <button onClick={handlePublish}>Publish</button>
    </div>
  );
}

Common Patterns

Multi-Instance Setup

Applications connecting to multiple backends:

typescript
// src/api/clients.ts
import { createFlowfull } from '@pubflow/flowfull-client';

// Main API
export const mainApi = createFlowfull('https://api.example.com');

// Analytics API
export const analyticsApi = createFlowfull('https://analytics.example.com');

// Usage
import { mainApi, analyticsApi } from './api/clients';

// Fetch from main API
const users = await mainApi.query('/users').get();

// Send analytics event
await analyticsApi.post('/events', { event: 'page_view', page: '/home' });

Role-Based Access Control

typescript
// src/hooks/useAuth.ts
import { useState, useEffect } from 'react';
import { api } from '../api/client';

interface User {
  id: string;
  name: string;
  user_type: string;
}

export function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    checkAuth();
  }, []);

  async function checkAuth() {
    const hasSession = await api.hasSession();

    if (hasSession) {
      const response = await api.get<User>('/profile');
      if (response.success) {
        setUser(response.data);
      }
    }

    setIsLoading(false);
  }

  return { user, isAuthenticated: !!user, isLoading };
}

// src/components/AdminPanel.tsx
import { useAuth } from '../hooks/useAuth';

function AdminPanel() {
  const { user } = useAuth();

  if (user?.user_type !== 'admin' && user?.user_type !== 'superadmin') {
    return <AccessDenied />;
  }

  return <AdminContent />;
}

// Or with routing
function Navigation() {
  const { user } = useAuth();

  if (user?.user_type === 'admin') {
    return <AdminNavigator />;
  }

  return <UserNavigator />;
}

Infinite Scroll

typescript
import { useState, useEffect } from 'react';
import { api } from '../api/client';

interface Item {
  id: string;
  name: string;
}

function InfiniteList() {
  const [allItems, setAllItems] = useState<Item[]>([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    loadMore();
  }, [page]);

  async function loadMore() {
    if (isLoading || !hasMore) return;

    setIsLoading(true);

    const response = await api
      .query('/items')
      .page(page)
      .limit(20)
      .get<Item[]>();

    if (response.success) {
      setAllItems(prev => [...prev, ...(response.data || [])]);
      setHasMore(response.meta?.has_more || false);
    }

    setIsLoading(false);
  }

  return (
    <div>
      {allItems.map(item => <ItemCard key={item.id} item={item} />)}
      {hasMore && (
        <button onClick={() => setPage(p => p + 1)} disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

Search with Debouncing

typescript
import { useState, useEffect } from 'react';
import { api } from '../api/client';

interface Product {
  id: string;
  name: string;
  price: number;
}

function SearchBar() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<Product[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    // Debounce search
    const timer = setTimeout(() => {
      if (query) {
        searchProducts(query);
      } else {
        setResults([]);
      }
    }, 300);

    return () => clearTimeout(timer);
  }, [query]);

  async function searchProducts(searchQuery: string) {
    setIsLoading(true);

    const response = await api
      .query('/products')
      .search(searchQuery)
      .limit(10)
      .get<Product[]>();

    if (response.success) {
      setResults(response.data || []);
    }

    setIsLoading(false);
  }

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search products..."
      />
      {isLoading && <div>Searching...</div>}
      {results.map(item => <SearchResult key={item.id} item={item} />)}
    </div>
  );
}

Optimistic Updates

typescript
import { useState } from 'react';
import { api } from '../api/client';

function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);
  const [isLiking, setIsLiking] = useState(false);

  async function handleLike() {
    if (isLiking) return;

    // Optimistic update
    const previousLikes = likes;
    setLikes(likes + 1);
    setIsLiking(true);

    try {
      const response = await api.post('/likes', { post_id: postId });

      if (!response.success) {
        // Revert on error
        setLikes(previousLikes);
      }
    } catch (error) {
      // Revert on error
      setLikes(previousLikes);
    } finally {
      setIsLiking(false);
    }
  }

  return (
    <button onClick={handleLike} disabled={isLiking}>
      ❤️ {likes} likes
    </button>
  );
}

Next Steps