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
- Advanced: Multi-Instance - Multi-instance configuration
- Advanced: Performance - Performance optimization
- Tutorial - Complete tutorial
- API Reference - Full API documentation
- Build Your Own Client - Create custom clients