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