Skip to main content

Complete Application Examples

Learn by building! These complete application examples show you how to build real-world apps with Cocobase.

Todo Application

A simple yet complete todo app with authentication, CRUD operations, and real-time sync.

Features

  • User authentication
  • Create, read, update, delete todos
  • Mark todos as complete
  • Real-time sync across devices
  • Filter by status
// lib/cocobase.ts
import { Cocobase } from "cocobase";

export const db = new Cocobase({
  apiKey: process.env.NEXT_PUBLIC_COCOBASE_API_KEY!,
});

// types/todo.ts
export interface Todo {
  id: string;
  title: string;
  completed: boolean;
  userId: string;
  createdAt: string;
}

// hooks/useTodos.ts
import { useState, useEffect } from 'react';
import { db } from '@/lib/cocobase';
import type { Todo } from '@/types/todo';

export function useTodos() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadTodos();

    // Real-time subscription
    const unsubscribe = db.watch('todos', (event) => {
      if (event.type === 'create') {
        setTodos(prev => [...prev, event.data as Todo]);
      } else if (event.type === 'update') {
        setTodos(prev => prev.map(t =>
          t.id === event.data.id ? event.data as Todo : t
        ));
      } else if (event.type === 'delete') {
        setTodos(prev => prev.filter(t => t.id !== event.documentId));
      }
    });

    return () => unsubscribe();
  }, []);

  const loadTodos = async () => {
    try {
      const data = await db.listDocuments<Todo>('todos', {
        sort: { createdAt: -1 }
      });
      setTodos(data);
    } finally {
      setLoading(false);
    }
  };

  const addTodo = async (title: string) => {
    const todo = await db.createDocument<Todo>('todos', {
      title,
      completed: false,
      userId: db.auth.getCurrentUser()?.id,
    });
    return todo;
  };

  const toggleTodo = async (id: string, completed: boolean) => {
    await db.updateDocument('todos', id, { completed });
  };

  const deleteTodo = async (id: string) => {
    await db.deleteDocument('todos', id);
  };

  return { todos, loading, addTodo, toggleTodo, deleteTodo };
}

// components/TodoList.tsx
import { useTodos } from '@/hooks/useTodos';

export function TodoList() {
  const { todos, loading, addTodo, toggleTodo, deleteTodo } = useTodos();
  const [newTodo, setNewTodo] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!newTodo.trim()) return;
    await addTodo(newTodo);
    setNewTodo('');
  };

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="What needs to be done?"
        />
        <button type="submit">Add</button>
      </form>

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={(e) => toggleTodo(todo.id, e.target.checked)}
            />
            <span style={{
              textDecoration: todo.completed ? 'line-through' : 'none'
            }}>
              {todo.title}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

E-Commerce Application

A complete e-commerce app with products, cart, and checkout.

Features

  • Product catalog with search
  • Shopping cart
  • User orders
  • Inventory management
  • Real-time stock updates
// types/product.ts
export interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  stock: number;
  category: string;
  images: string[];
}

export interface CartItem {
  productId: string;
  quantity: number;
  product?: Product;
}

export interface Order {
  id: string;
  userId: string;
  items: CartItem[];
  total: number;
  status: 'pending' | 'processing' | 'shipped' | 'delivered';
  createdAt: string;
}

// hooks/useProducts.ts
export function useProducts(category?: string) {
  const [products, setProducts] = useState<Product[]>([]);

  useEffect(() => {
    loadProducts();
  }, [category]);

  const loadProducts = async () => {
    const filters = category ? { category: { $eq: category } } : {};
    const data = await db.listDocuments<Product>('products', {
      filters,
      sort: { createdAt: -1 }
    });
    setProducts(data);
  };

  const searchProducts = async (query: string) => {
    const data = await db.listDocuments<Product>('products', {
      filters: {
        name: { $contains: query }
      }
    });
    setProducts(data);
  };

  return { products, searchProducts };
}

// hooks/useCart.ts
export function useCart() {
  const [cart, setCart] = useState<CartItem[]>([]);

  const addToCart = async (productId: string, quantity: number = 1) => {
    const existing = cart.find(item => item.productId === productId);

    if (existing) {
      setCart(cart.map(item =>
        item.productId === productId
          ? { ...item, quantity: item.quantity + quantity }
          : item
      ));
    } else {
      setCart([...cart, { productId, quantity }]);
    }

    // Save to database
    await db.createDocument('cart_items', {
      userId: db.auth.getCurrentUser()?.id,
      productId,
      quantity
    });
  };

  const removeFromCart = async (productId: string) => {
    setCart(cart.filter(item => item.productId !== productId));

    // Delete from database
    const cartItem = await db.listDocuments('cart_items', {
      filters: {
        userId: { $eq: db.auth.getCurrentUser()?.id },
        productId: { $eq: productId }
      }
    });

    if (cartItem[0]) {
      await db.deleteDocument('cart_items', cartItem[0].id);
    }
  };

  const checkout = async () => {
    const user = db.auth.getCurrentUser();
    if (!user) throw new Error('Please login to checkout');

    // Calculate total
    const total = cart.reduce((sum, item) => {
      return sum + (item.product?.price || 0) * item.quantity;
    }, 0);

    // Create order
    const order = await db.createDocument<Order>('orders', {
      userId: user.id,
      items: cart,
      total,
      status: 'pending'
    });

    // Clear cart
    setCart([]);

    return order;
  };

  return { cart, addToCart, removeFromCart, checkout };
}

// components/ProductCard.tsx
export function ProductCard({ product }: { product: Product }) {
  const { addToCart } = useCart();

  return (
    <div className="product-card">
      <img src={product.images[0]} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.description}</p>
      <p className="price">${product.price.toFixed(2)}</p>
      <p className="stock">
        {product.stock > 0 ? `${product.stock} in stock` : 'Out of stock'}
      </p>
      <button
        onClick={() => addToCart(product.id)}
        disabled={product.stock === 0}
      >
        Add to Cart
      </button>
    </div>
  );
}

// components/Cart.tsx
export function Cart() {
  const { cart, removeFromCart, checkout } = useCart();
  const [loading, setLoading] = useState(false);

  const handleCheckout = async () => {
    setLoading(true);
    try {
      const order = await checkout();
      alert(`Order placed! Order ID: ${order.id}`);
    } finally {
      setLoading(false);
    }
  };

  const total = cart.reduce((sum, item) =>
    sum + (item.product?.price || 0) * item.quantity, 0
  );

  return (
    <div className="cart">
      <h2>Shopping Cart</h2>
      {cart.map(item => (
        <div key={item.productId} className="cart-item">
          <span>{item.product?.name}</span>
          <span>x{item.quantity}</span>
          <span>${((item.product?.price || 0) * item.quantity).toFixed(2)}</span>
          <button onClick={() => removeFromCart(item.productId)}>
            Remove
          </button>
        </div>
      ))}
      <div className="cart-total">
        <strong>Total: ${total.toFixed(2)}</strong>
      </div>
      <button onClick={handleCheckout} disabled={loading || cart.length === 0}>
        {loading ? 'Processing...' : 'Checkout'}
      </button>
    </div>
  );
}

Social Media Feed

A social media application with posts, likes, comments, and follows.

Features

  • Create and view posts
  • Like/unlike posts
  • Comment on posts
  • Follow/unfollow users
  • Real-time feed updates
  • User profiles
// types/social.ts
export interface Post {
  id: string;
  userId: string;
  user?: User;
  content: string;
  images?: string[];
  likes: string[];
  commentCount: number;
  createdAt: string;
}

export interface Comment {
  id: string;
  postId: string;
  userId: string;
  user?: User;
  content: string;
  createdAt: string;
}

export interface User {
  id: string;
  email: string;
  data: {
    full_name: string;
    avatar?: string;
    bio?: string;
  };
}

// hooks/useFeed.ts
export function useFeed() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadFeed();

    // Real-time updates
    const unsubscribe = db.watch('posts', (event) => {
      if (event.type === 'create') {
        setPosts(prev => [event.data as Post, ...prev]);
      } else if (event.type === 'update') {
        setPosts(prev => prev.map(p =>
          p.id === event.data.id ? event.data as Post : p
        ));
      } else if (event.type === 'delete') {
        setPosts(prev => prev.filter(p => p.id !== event.documentId));
      }
    });

    return () => unsubscribe();
  }, []);

  const loadFeed = async () => {
    try {
      const data = await db.listDocuments<Post>('posts', {
        sort: { createdAt: -1 },
        populate: ['userId'],
        limit: 20
      });
      setPosts(data);
    } finally {
      setLoading(false);
    }
  };

  const createPost = async (content: string, images?: string[]) => {
    const post = await db.createDocument<Post>('posts', {
      userId: db.auth.getCurrentUser()?.id,
      content,
      images: images || [],
      likes: [],
      commentCount: 0
    });
    return post;
  };

  const likePost = async (postId: string) => {
    const userId = db.auth.getCurrentUser()?.id;
    const post = posts.find(p => p.id === postId);

    if (!post || !userId) return;

    const likes = post.likes.includes(userId)
      ? post.likes.filter(id => id !== userId)
      : [...post.likes, userId];

    await db.updateDocument('posts', postId, { likes });
  };

  const deletePost = async (postId: string) => {
    await db.deleteDocument('posts', postId);
  };

  return { posts, loading, createPost, likePost, deletePost };
}

// hooks/useComments.ts
export function useComments(postId: string) {
  const [comments, setComments] = useState<Comment[]>([]);

  useEffect(() => {
    loadComments();
  }, [postId]);

  const loadComments = async () => {
    const data = await db.listDocuments<Comment>('comments', {
      filters: { postId: { $eq: postId } },
      sort: { createdAt: -1 },
      populate: ['userId']
    });
    setComments(data);
  };

  const addComment = async (content: string) => {
    const comment = await db.createDocument<Comment>('comments', {
      postId,
      userId: db.auth.getCurrentUser()?.id,
      content
    });

    // Increment comment count
    const post = await db.getDocument('posts', postId);
    await db.updateDocument('posts', postId, {
      commentCount: (post.commentCount || 0) + 1
    });

    return comment;
  };

  return { comments, addComment };
}

// components/PostCard.tsx
export function PostCard({ post }: { post: Post }) {
  const { likePost, deletePost } = useFeed();
  const currentUser = db.auth.getCurrentUser();
  const isLiked = post.likes.includes(currentUser?.id || '');

  return (
    <div className="post-card">
      <div className="post-header">
        <img src={post.user?.data.avatar} alt={post.user?.data.full_name} />
        <div>
          <h4>{post.user?.data.full_name}</h4>
          <span>{new Date(post.createdAt).toLocaleDateString()}</span>
        </div>
      </div>

      <p className="post-content">{post.content}</p>

      {post.images && post.images.length > 0 && (
        <div className="post-images">
          {post.images.map((img, i) => (
            <img key={i} src={img} alt="" />
          ))}
        </div>
      )}

      <div className="post-actions">
        <button onClick={() => likePost(post.id)}>
          {isLiked ? '❤️' : '🤍'} {post.likes.length}
        </button>
        <button>💬 {post.commentCount}</button>
        {currentUser?.id === post.userId && (
          <button onClick={() => deletePost(post.id)}>🗑️ Delete</button>
        )}
      </div>
    </div>
  );
}

// components/CreatePost.tsx
export function CreatePost() {
  const { createPost } = useFeed();
  const [content, setContent] = useState('');
  const [posting, setPosting] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!content.trim()) return;

    setPosting(true);
    try {
      await createPost(content);
      setContent('');
    } finally {
      setPosting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="create-post">
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="What's on your mind?"
        rows={3}
      />
      <button type="submit" disabled={posting || !content.trim()}>
        {posting ? 'Posting...' : 'Post'}
      </button>
    </form>
  );
}

Real-Time Chat Application

A complete chat app with rooms, direct messages, and online status.

Features

  • Real-time messaging
  • Chat rooms
  • Direct messages
  • Online status
  • Typing indicators
  • Message history
// hooks/useChat.ts
export function useChat(roomId: string) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [typing, setTyping] = useState<string[]>([]);

  useEffect(() => {
    // Load message history
    loadMessages();

    // Subscribe to new messages
    const unsubscribe = db.realtimeApi.joinRoom(roomId, (event) => {
      if (event.type === 'message') {
        setMessages(prev => [...prev, event.data]);
      } else if (event.type === 'typing') {
        setTyping(prev => [...prev, event.userId]);
        setTimeout(() => {
          setTyping(prev => prev.filter(id => id !== event.userId));
        }, 3000);
      }
    });

    return () => unsubscribe();
  }, [roomId]);

  const loadMessages = async () => {
    const data = await db.listDocuments('messages', {
      filters: { roomId: { $eq: roomId } },
      sort: { createdAt: 1 },
      populate: ['userId'],
      limit: 50
    });
    setMessages(data);
  };

  const sendMessage = async (content: string) => {
    const message = await db.createDocument('messages', {
      roomId,
      userId: db.auth.getCurrentUser()?.id,
      content
    });

    // Broadcast to room
    await db.realtimeApi.broadcast(roomId, {
      type: 'message',
      data: message
    });

    return message;
  };

  const sendTyping = async () => {
    await db.realtimeApi.broadcast(roomId, {
      type: 'typing',
      userId: db.auth.getCurrentUser()?.id
    });
  };

  return { messages, typing, sendMessage, sendTyping };
}

// components/ChatRoom.tsx
export function ChatRoom({ roomId }: { roomId: string }) {
  const { messages, typing, sendMessage, sendTyping } = useChat(roomId);
  const [input, setInput] = useState('');
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  const handleSend = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim()) return;

    await sendMessage(input);
    setInput('');
  };

  const handleTyping = () => {
    sendTyping();
  };

  return (
    <div className="chat-room">
      <div className="messages">
        {messages.map(msg => (
          <div key={msg.id} className="message">
            <strong>{msg.user?.data.full_name}:</strong>
            <span>{msg.content}</span>
            <small>{new Date(msg.createdAt).toLocaleTimeString()}</small>
          </div>
        ))}
        {typing.length > 0 && (
          <div className="typing-indicator">
            Someone is typing...
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>

      <form onSubmit={handleSend} className="message-input">
        <input
          value={input}
          onChange={(e) => {
            setInput(e.target.value);
            handleTyping();
          }}
          placeholder="Type a message..."
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Notes App with Real-Time Sync

A collaborative notes app with folders and real-time synchronization.

Features

  • Create and organize notes in folders
  • Rich text editing
  • Real-time sync across devices
  • Collaboration with other users
  • Search notes
// Full implementation available in the examples repository

Complete App Architecture

All these apps follow this general architecture:
/src
  /lib
    cocobase.ts         # Cocobase client setup
  /types
    index.ts            # TypeScript interfaces
  /hooks
    useAuth.ts          # Authentication hook
    useData.ts          # Data fetching hooks
  /components
    ...                 # React components
  /pages or /screens
    ...                 # App pages/screens

Best Practices

  • Keep documents denormalized for read performance
  • Use relationships for one-to-many associations
  • Store frequently accessed data together
  • Only subscribe to data you’re actively displaying - Unsubscribe when components unmount - Filter subscriptions to reduce bandwidth
  • Always handle errors in async operations - Show user-friendly error messages
  • Implement retry logic for network errors
  • Paginate large lists
  • Cache frequently accessed data
  • Use optimistic UI updates

Source Code

Find complete source code for all these examples in our GitHub repository:

Cocobase Examples Repository

Full source code, setup instructions, and live demos

Next Steps