Building Collaborative Apps

Learn how to build multi-user collaborative applications with mixed data isolation patterns. Create shared content with user-level accountability and edit tracking.

25 minutes Intermediate Collaborative App

What You'll Build

A wiki-style collaborative application that demonstrates mixed isolation patterns - public content that all users can see and edit, combined with user-specific edit history tracking for accountability.

  • Public Wiki Pages: Shared content visible to all users without isolation
  • Edit History Tracking: User-attributed change logs for transparency
  • User Attribution: Track who made each change without restricting access
  • Collaborative Editing: Multiple users can contribute to shared knowledge

Key Concept: Mixed Isolation Patterns

Not all data needs the same isolation level. Collaborative apps often need public content (accessible to everyone) combined with user tracking (who did what). This tutorial shows you how to implement this pattern effectively.

Step 1: Understanding Mixed Isolation

Before diving into code, let's understand the key architectural decision that makes collaborative apps possible: mixed data isolation.

The Isolation Spectrum

Full User Isolation (Default)

Each user only sees their own data. Perfect for personal apps like note-taking or task management.

user_isolation: enforced  # Default behavior

Disabled Isolation (Shared)

All users see the same data. Ideal for collaborative content like wikis, shared documents, or public forums.

user_isolation: disabled  # Shared content

The Wiki Pattern

The wiki-test-app demonstrates a powerful hybrid approach: pages are public (everyone can read and edit them), but edit history tracks users (accountability for changes). This gives you the best of both worlds - open collaboration with full transparency.

Step 2: Configuring the Manifest for Mixed Isolation

The manifest is where you define your isolation strategy. Here's the actual configuration from the wiki-test-app:

App-Level Isolation Setting

app:
  name: Wiki Test App
  app_slug: wiki-test-app
  version: 0.0.3
  description: Wiki-style app demonstrating mixed isolation - public pages with user-tracked edit history
  entryPoint: index.js
  icon: fas fa-book
  category: simple
  publisher: FiberWise
  user_isolation: disabled  # App-wide setting for shared content

Important: App-Level vs Model-Level

Setting user_isolation: disabled at the app level makes all models shared by default. You can still override this per-model if needed for specific data types that require isolation.

Data Model Definitions

The wiki app uses two models with different purposes:

Wiki Pages Model (Public Content)

models:
- name: Wiki Page
  model_slug: wiki_pages
  description: Public wiki pages visible to all users
  fields:
  - name: Page ID
    field_column: page_id
    type: uuid
    required: true
    is_primary_key: true
    description: Unique identifier for each wiki page
  - name: Page Title
    field_column: page_title
    type: string
    required: true
    description: Title of the wiki page
  - name: Page Slug
    field_column: page_slug
    type: string
    required: true
    unique: true
    description: URL-friendly version of the page title
  - name: Content
    field_column: content
    type: text
    required: true
    description: Current content of the wiki page
  - name: Summary
    field_column: summary
    type: string
    description: Brief description of the page
  - name: Last Editor
    field_column: last_editor_id
    type: string
    description: User ID of last person to edit this page
    is_system_field: true
  - name: View Count
    field_column: view_count
    type: integer
    default: 0
    description: Number of times this page has been viewed

Edit History Model (User Tracking)

- name: Edit History
  model_slug: edit_history
  description: User-tracked history of all wiki page edits
  fields:
  - name: Edit ID
    field_column: edit_id
    type: uuid
    required: true
    is_primary_key: true
    description: Unique identifier for each edit
  - name: Page ID
    field_column: page_id
    type: uuid
    required: true
    description: Reference to the wiki page that was edited
  - name: User ID
    field_column: user_id
    type: string
    description: ID of user who made this edit
    is_system_field: true
  - name: Edit Type
    field_column: edit_type
    type: string
    required: true
    default: update
    description: Type of edit (create, update, delete)
  - name: Previous Content
    field_column: previous_content
    type: text
    description: Content before this edit
  - name: New Content
    field_column: new_content
    type: text
    required: true
    description: Content after this edit
  - name: Edit Summary
    field_column: edit_summary
    type: string
    description: User-provided summary of changes made
  - name: Characters Changed
    field_column: characters_changed
    type: integer
    description: Number of characters added or removed

Design Pattern: Attribution Fields

Notice the user_id and last_editor_id fields marked as is_system_field: true. These are automatically populated by the platform and provide user attribution even when isolation is disabled.

Step 3: Implementing the WikiAgent

The WikiAgent handles all wiki operations - creating pages, updating content, and tracking edit history. Let's examine the actual implementation.

Agent Registration in Manifest

agents:
- name: WikiAgent
  agent_slug: wiki-agent
  agent_type_id: custom
  version: 1.0.0
  description: Agent for managing wiki pages and tracking edit history
  implementation_path: agents/wiki_agent.py
  language: python

Agent Class Structure

"""
Wiki Agent for managing wiki pages with mixed isolation.
Demonstrates public content with user-tracked edit history.
"""

from typing import Dict, Any, List, Optional
import uuid
import json
from datetime import datetime
from fiberwise_sdk import FiberAgent
from fiberwise_common.services.database_service import DatabaseService


class WikiAgent(FiberAgent):
    """Agent for managing wiki pages and edit history tracking."""

    def __init__(self):
        super().__init__()
        self.db_service = DatabaseService()

    async def execute(self, input_data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
        """
        Execute wiki operations based on input commands.

        Args:
            input_data: Command and parameters for wiki operations
            **kwargs: Injected services (fiber, etc.)

        Returns:
            Dictionary with operation result

        Available commands:
        - create_page: Create a new wiki page with edit tracking
        - update_page: Update existing page and record edit history
        - get_page: Get page content and basic info
        - get_page_history: Get edit history for a page
        - search_pages: Search through wiki pages
        - get_user_contributions: Get edit history for a specific user
        """
        # Access injected services from kwargs
        fiber = kwargs.get('fiber')
        command = input_data.get('command', 'get_page')

        command_map = {
            'create_page': self._create_page,
            'update_page': self._update_page,
            'get_page': self._get_page,
            'get_page_history': self._get_page_history,
            'search_pages': self._search_pages,
            'get_user_contributions': self._get_user_contributions,
            'analyze_collaboration': self._analyze_collaboration
        }

        if command in command_map:
            return await command_map[command](input_data)
        else:
            return {
                'success': False,
                'error': f'Unknown command: {command}',
                'available_commands': list(command_map.keys())
            }

The execute() Pattern

All FiberWise agents use the execute() method as their entry point. The fiber service is injected via kwargs, giving you access to platform features like authentication and data services.

Step 4: Creating Pages with Edit Tracking

When creating a wiki page, we need to both create the page (public) and record the creation in edit history (user-attributed).

The Create Page Implementation

async def _create_page(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
    """Create a new wiki page with user tracking."""
    try:
        title = input_data.get('title', '').strip()
        content = input_data.get('content', '').strip()
        summary = input_data.get('summary', '').strip()
        user_id = input_data.get('user_id', 'system')
        user_name = input_data.get('user_name', 'System')

        if not title or not content:
            return {
                'success': False,
                'error': 'Title and content are required'
            }

        # Generate slug from title
        slug = self._generate_slug(title)

        # Check if page with this slug already exists
        existing = await self.db_service.get_records('wiki_pages', {'page_slug': slug})
        if existing:
            return {
                'success': False,
                'error': f'Page with slug "{slug}" already exists'
            }

        page_id = str(uuid.uuid4())

        # Create the page (public, no user isolation)
        page_data = {
            'page_id': page_id,
            'page_title': title,
            'page_slug': slug,
            'content': content,
            'summary': summary,
            'last_editor_id': user_id,
            'last_editor_name': user_name,
            'view_count': 0
        }

        await self.db_service.create_record('wiki_pages', page_data)

        # Create edit history entry (tracks user)
        edit_data = {
            'edit_id': str(uuid.uuid4()),
            'page_id': page_id,
            'user_id': user_id,
            'user_name': user_name,
            'edit_type': 'create',
            'new_content': content,
            'edit_summary': f'Created page: {title}',
            'characters_changed': len(content)
        }

        await self.db_service.create_record('edit_history', edit_data)

        return {
            'success': True,
            'message': f'Wiki page "{title}" created successfully',
            'data': {
                'page_id': page_id,
                'page_slug': slug,
                'title': title,
                'created_by': user_name,
                'isolation_info': {
                    'page_visibility': 'public (user_isolation: disabled)',
                    'edit_tracking': 'user-specific history recorded'
                }
            }
        }
    except Exception as e:
        return {
            'success': False,
            'error': f'Failed to create page: {str(e)}'
        }

Create Page Data Flow

graph TD A[User Submits Page] --> B[WikiAgent.execute] B --> C[_create_page method] C --> D[Validate Input] D --> E[Generate URL Slug] E --> F[Check for Duplicates] F --> G[Create Wiki Page Record] G --> H[Create Edit History Record] H --> I[Return Success Response] G -->|Public Data| J[(wiki_pages table)] H -->|User-Attributed| K[(edit_history table)] classDef user fill:#e3f2fd classDef agent fill:#f3e5f5 classDef data fill:#e8f5e8 classDef storage fill:#fff3e0 class A user class B,C,D,E,F,I agent class G,H data class J,K storage

Step 5: Updating Pages with History

When a user edits a page, we capture the previous content, calculate the changes, and create a detailed history record.

The Update Page Implementation

async def _update_page(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
    """Update existing page and record edit history."""
    try:
        page_slug = input_data.get('page_slug', '').strip()
        new_content = input_data.get('content', '').strip()
        edit_summary = input_data.get('edit_summary', 'Updated page content').strip()
        user_id = input_data.get('user_id', 'system')
        user_name = input_data.get('user_name', 'System')

        if not page_slug or not new_content:
            return {
                'success': False,
                'error': 'Page slug and content are required'
            }

        # Get existing page
        pages = await self.db_service.get_records('wiki_pages', {'page_slug': page_slug})
        if not pages:
            return {
                'success': False,
                'error': f'Page with slug "{page_slug}" not found'
            }

        page = pages[0]
        previous_content = page.get('content', '')

        # Calculate change metrics
        char_diff = len(new_content) - len(previous_content)

        # Update the page
        update_data = {
            'content': new_content,
            'last_editor_id': user_id,
            'last_editor_name': user_name,
            'updated_at': datetime.utcnow().isoformat()
        }

        await self.db_service.update_record('wiki_pages', page['page_id'], update_data)

        # Record edit history
        edit_data = {
            'edit_id': str(uuid.uuid4()),
            'page_id': page['page_id'],
            'user_id': user_id,
            'user_name': user_name,
            'edit_type': 'update',
            'previous_content': previous_content,
            'new_content': new_content,
            'edit_summary': edit_summary,
            'characters_changed': char_diff
        }

        await self.db_service.create_record('edit_history', edit_data)

        return {
            'success': True,
            'message': f'Page "{page["page_title"]}" updated successfully',
            'data': {
                'page_slug': page_slug,
                'updated_by': user_name,
                'characters_changed': char_diff,
                'edit_summary': edit_summary
            }
        }
    except Exception as e:
        return {
            'success': False,
            'error': f'Failed to update page: {str(e)}'
        }

Edit History Best Practices

  • Store Previous Content: Keep the old version for diff comparisons and rollback capabilities
  • Track Change Metrics: Calculate characters_changed for contribution statistics
  • Require Edit Summaries: Encourage users to explain their changes
  • Preserve Attribution: Always record who made the change and when

Step 6: Retrieving Page History

One of the key benefits of edit tracking is the ability to see the complete history of changes to any page.

The Get Page History Implementation

async def _get_page_history(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
    """Get edit history for a page."""
    try:
        page_slug = input_data.get('page_slug', '').strip()
        limit = input_data.get('limit', 50)

        if not page_slug:
            return {
                'success': False,
                'error': 'Page slug is required'
            }

        # Get page ID
        pages = await self.db_service.get_records('wiki_pages', {'page_slug': page_slug})
        if not pages:
            return {
                'success': False,
                'error': f'Page "{page_slug}" not found'
            }

        page_id = pages[0]['page_id']

        # Get edit history
        history = await self.db_service.get_records(
            'edit_history',
            {'page_id': page_id},
            order_by='created_at DESC',
            limit=limit
        )

        return {
            'success': True,
            'message': f'Retrieved {len(history)} edit records for "{page_slug}"',
            'data': {
                'page_title': pages[0]['page_title'],
                'total_edits': len(history),
                'edit_history': history,
                'contributors': list(set([edit['user_name'] for edit in history]))
            }
        }
    except Exception as e:
        return {
            'success': False,
            'error': f'Failed to get page history: {str(e)}'
        }

Get User Contributions

async def _get_user_contributions(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
    """Get all edit contributions from a specific user."""
    try:
        user_id = input_data.get('user_id', '').strip()
        limit = input_data.get('limit', 100)

        if not user_id:
            return {
                'success': False,
                'error': 'User ID is required'
            }

        # Get user's edit history
        edits = await self.db_service.get_records(
            'edit_history',
            {'user_id': user_id},
            order_by='created_at DESC',
            limit=limit
        )

        # Get page titles for each edit
        page_ids = list(set([edit['page_id'] for edit in edits]))
        pages = {}
        for page_id in page_ids:
            page_records = await self.db_service.get_records('wiki_pages', {'page_id': page_id})
            if page_records:
                pages[page_id] = page_records[0]['page_title']

        # Enhance edit records with page titles
        for edit in edits:
            edit['page_title'] = pages.get(edit['page_id'], 'Unknown Page')

        return {
            'success': True,
            'message': f'Retrieved {len(edits)} contributions',
            'data': {
                'user_name': edits[0]['user_name'] if edits else 'Unknown',
                'total_contributions': len(edits),
                'pages_edited': len(set([edit['page_id'] for edit in edits])),
                'contributions': edits
            }
        }
    except Exception as e:
        return {
            'success': False,
            'error': f'Failed to get user contributions: {str(e)}'
        }

Step 7: Building the Frontend Components

The wiki app uses Web Components with the FIBER SDK for data operations. Here's how the frontend interacts with shared data.

Wiki Home Component

/**
 * Wiki Home Component
 * Main component for the wiki app
 */
import { FIBER } from '../../index.js';

export class WikiHome extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // App state
    this.isLoading = true;
    this.currentUser = null;
    this.recentPages = [];
    this.recentEdits = [];
  }

  async loadData() {
    try {
      // Load current user using FIBER auth
      this.currentUser = await FIBER.auth.getCurrentUser();

      // Load recent pages using FIBER SDK with context filter
      const pagesResponse = await FIBER.data.listItems('wiki_pages', {
        limit: 5,
        orderBy: 'updated_at DESC',
        context: 'user'
      });
      this.recentPages = pagesResponse.items || [];

      // Load recent edits with context filter
      const editsResponse = await FIBER.data.listItems('edit_history', {
        limit: 10,
        orderBy: 'created_at DESC',
        context: 'user'
      });
      this.recentEdits = editsResponse.items || [];

      this.isLoading = false;
      this.updateDisplay();
    } catch (error) {
      console.error('Failed to load data:', error);
      this.isLoading = false;
      this.updateDisplay();
    }
  }

  renderRecentEdits() {
    const container = this.shadowRoot.querySelector('#recent-edits-list');
    if (!container) return;

    container.innerHTML = this.recentEdits.map(edit => {
      const editData = edit.data || edit;

      return `
        <div class="mb-2">
          <strong>${editData.user_id}</strong>
          <span class="badge bg-primary">${editData.edit_type}</span>
          <br>
          <small>${editData.edit_summary || 'No summary'}</small>
          <br>
          <small class="text-muted">${new Date(edit.created_at).toLocaleString()}</small>
        </div>
        <hr class="my-2">
      `;
    }).join('');
  }
}

Create Page Component

/**
 * Create Page Component
 */
import { FIBER } from '../../index.js';

export class CreatePage extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.currentUser = null;
  }

  generateSlug(title) {
    return title.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '-');
  }

  async createPage() {
    const title = this.shadowRoot.querySelector('#pageTitle').value.trim();
    const content = this.shadowRoot.querySelector('#pageContent').value.trim();
    const summary = this.shadowRoot.querySelector('#pageSummary').value.trim();

    if (!title || !content) {
      this.showError('Title and content are required');
      return;
    }

    const slug = this.generateSlug(title);

    try {
      // Create page using FIBER SDK - API will handle user tracking
      const page = await FIBER.data.createItem('wiki_pages', {
        page_title: title,
        page_slug: slug,
        content: content,
        summary: summary,
        view_count: 0
      });

      // Create edit history - API will handle user tracking
      await FIBER.data.createItem('edit_history', {
        page_id: page.item_id,
        edit_type: 'create',
        new_content: content,
        edit_summary: `Created page: ${title}`,
        characters_changed: content.length
      });

      this.showSuccess('Page created successfully!');

      // Navigate back home after 1 second
      setTimeout(() => {
        FIBER.router.navigateTo('/');
      }, 1000);

    } catch (error) {
      console.error('Failed to create page:', error);
      this.showError('Failed to create page. Please try again.');
    }
  }
}

Frontend Data Access Pattern

With user_isolation: disabled, the FIBER.data.listItems() and FIBER.data.createItem() calls return and create data visible to all users. The platform still tracks which user made each request via system fields.

Step 8: Analyzing Collaboration Patterns

With edit history tracking, you can build powerful analytics to understand how users collaborate.

Collaboration Analysis Implementation

async def _analyze_collaboration(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
    """Analyze collaboration patterns in the wiki."""
    try:
        # Get all pages and edit history
        pages = await self.db_service.get_records('wiki_pages')
        edits = await self.db_service.get_records('edit_history', order_by='created_at DESC')

        # Analyze data
        total_pages = len(pages)
        total_edits = len(edits)
        unique_contributors = len(set([edit['user_id'] for edit in edits]))

        # Find most active contributors
        contributor_counts = {}
        for edit in edits:
            user = edit['user_name']
            contributor_counts[user] = contributor_counts.get(user, 0) + 1

        top_contributors = sorted(
            contributor_counts.items(),
            key=lambda x: x[1],
            reverse=True
        )[:5]

        # Find most edited pages
        page_edit_counts = {}
        for edit in edits:
            page_id = edit['page_id']
            page_edit_counts[page_id] = page_edit_counts.get(page_id, 0) + 1

        most_edited = []
        for page_id, edit_count in sorted(
            page_edit_counts.items(),
            key=lambda x: x[1],
            reverse=True
        )[:3]:
            page_info = next((p for p in pages if p['page_id'] == page_id), None)
            if page_info:
                most_edited.append({
                    'title': page_info['page_title'],
                    'edit_count': edit_count
                })

        return {
            'success': True,
            'message': 'Collaboration analysis complete',
            'data': {
                'overview': {
                    'total_pages': total_pages,
                    'total_edits': total_edits,
                    'unique_contributors': unique_contributors,
                    'avg_edits_per_page': round(total_edits / max(total_pages, 1), 2)
                },
                'top_contributors': top_contributors,
                'most_edited_pages': most_edited,
                'isolation_benefits': {
                    'transparency': 'All edits are attributed to users',
                    'accountability': 'Change history preserves authorship',
                    'collaboration': 'Public access enables shared knowledge',
                    'tracking': 'Mixed approach balances openness with attribution'
                }
            }
        }
    except Exception as e:
        return {
            'success': False,
            'error': f'Failed to analyze collaboration: {str(e)}'
        }

Collaboration Analytics Flow

graph TD A[Request Analytics] --> B[Fetch All Pages] A --> C[Fetch All Edit History] B --> D[Calculate Totals] C --> D D --> E[Find Top Contributors] D --> F[Find Most Edited Pages] E --> G[Build Analytics Response] F --> G G --> H[Return Insights] classDef request fill:#e3f2fd classDef process fill:#f3e5f5 classDef result fill:#e8f5e8 class A request class B,C,D,E,F process class G,H result

Key Takeaways

Mixed Isolation Pattern

# App-level: disable isolation for shared content
user_isolation: disabled

# Use system fields for user tracking
- name: User ID
  field_column: user_id
  is_system_field: true

Combine public data access with user attribution fields to enable collaboration while maintaining accountability.

Edit History Tracking

# Always record changes with context
edit_data = {
    'user_id': user_id,
    'edit_type': 'update',
    'previous_content': old_content,
    'new_content': new_content,
    'characters_changed': len(new_content) - len(old_content)
}

Capture detailed change history for transparency, rollback capabilities, and contribution analytics.

Agent Execute Pattern

async def execute(self, input_data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
    fiber = kwargs.get('fiber')
    command = input_data.get('command')

    # Route to appropriate handler
    return await command_map[command](input_data)

Use the standard execute() pattern with command routing for clean, maintainable agent code.

Frontend Integration

// All users see shared data
const pages = await FIBER.data.listItems('wiki_pages');

// Create items visible to all users
await FIBER.data.createItem('wiki_pages', pageData);

The FIBER SDK automatically handles shared data access when isolation is disabled at the app level.

Next Steps

Add Real-time Collaboration

Implement WebSocket-based real-time updates so users see changes as they happen.

Real-time Guide

Implement Content Versioning

Add version comparison and rollback capabilities using the edit history.

More Tutorials

Add Access Controls

Implement role-based permissions for edit vs read-only access.

Permissions Guide