Skip to main content

Overview

Meridian enables multiple users to collaborate on data analysis in real-time. See who’s viewing the same table, get notified of changes, and coordinate analysis efforts seamlessly.

Presence System

The presence system tracks active users and their current context:

User Presence

interface UserPresence {
  userId: string
  userName: string
  tableName?: string        // Current table being viewed
  lastActivity: number      // Timestamp
  isActive: boolean
}

Presence Indicators

Active users are displayed with avatars and status:
<Group>
  {activeUsers.map(user => (
    <Tooltip key={user.userId} label={user.userName}>
      <Avatar
        size="sm"
        radius="xl"
        color={user.isActive ? 'green' : 'gray'}
      >
        {user.userName[0]}
      </Avatar>
    </Tooltip>
  ))}
</Group>
Active presence is updated every 30 seconds and expires after 2 minutes of inactivity

Notifications

Users receive real-time notifications for important events:

Notification Types

Notified when other users run queries on the current table:
{
  type: 'query_executed',
  userId: 'user_123',
  userName: 'Alice',
  tableName: 'sales',
  query: 'SELECT * FROM sales WHERE amount > 1000',
  timestamp: Date.now()
}
Alerted when data is modified:
{
  type: 'data_modified',
  userId: 'user_456',
  userName: 'Bob',
  tableName: 'customers',
  operation: 'UPDATE',
  rowsAffected: 15,
  timestamp: Date.now()
}
See when visualizations are created:
{
  type: 'chart_created',
  userId: 'user_789',
  userName: 'Carol',
  chartType: 'bar',
  chartTitle: 'Revenue by Region',
  timestamp: Date.now()
}
Know when new insights are discovered:
{
  type: 'insights_generated',
  userId: 'user_101',
  userName: 'Dave',
  tableName: 'sales',
  insightCount: 5,
  timestamp: Date.now()
}

Notification Display

Notifications appear in the table header:
import { TableNotifications } from '@/components/TableNotifications'

<TableNotifications
  notifications={notifications}
  onDismiss={handleDismiss}
  onClear={handleClearAll}
/>
Implementation:
<Box>
  {notifications.map(notification => (
    <Alert key={notification.id} variant="light">
      <Group justify="space-between">
        <Stack gap={4}>
          <Text size="sm" fw={600}>
            {notification.userName}
          </Text>
          <Text size="xs" c="dimmed">
            {notification.message}
          </Text>
        </Stack>
        <ActionIcon
          size="sm"
          onClick={() => onDismiss(notification.id)}
        >
          <IconX size={14} />
        </ActionIcon>
      </Group>
    </Alert>
  ))}
</Box>

Query History

The query timeline shows all queries executed on a table:

Timeline Component

import { QueryTimeline } from '@/components/QueryTimeline'

<QueryTimeline
  tableName={tableName}
  onRollbackComplete={handleRollback}
/>

Query Log Entry

interface QueryLogEntry {
  _id: string
  _creationTime: number
  userId: string
  userName: string
  tableName: string
  query: string
  success: boolean
  rowsAffected?: number
  executionTime?: number
  error?: string
}

Timeline Features

Query History

View all queries executed on the table with timestamps and user attribution

Rollback

Revert to previous table states using query history

Query Replay

Re-execute previous queries to reproduce results

Error Tracking

See which queries failed and why

Live Data Updates

Meridian uses Convex’s reactive queries for real-time data synchronization:

Reactive Queries

import { useQuery } from 'convex/react'
import { api } from '@/convex/_generated/api'

// Automatically updates when data changes
const tableData = useQuery(
  api.csv.getTableData,
  { tableName }
)

const queryLogs = useQuery(
  api.queryLog.getLogsForTable,
  { tableName }
)

const presence = useQuery(
  api.presence.getActiveUsers,
  { tableName }
)

Optimistic Updates

For better UX, apply optimistic updates before server confirmation:
const [localData, setLocalData] = useState(serverData)

const handleUpdate = async (newData) => {
  // Immediate local update
  setLocalData(newData)
  
  try {
    // Server update
    await updateMutation({ data: newData })
  } catch (error) {
    // Rollback on error
    setLocalData(serverData)
    notifications.show({
      title: 'Update Failed',
      message: error.message,
      color: 'red',
    })
  }
}

Conflict Resolution

When multiple users modify the same data:

Last-Write-Wins

Meridian uses last-write-wins for simple conflict resolution:
interface DataUpdate {
  rowId: string
  columnName: string
  oldValue: any
  newValue: any
  userId: string
  timestamp: number
}

// The update with the latest timestamp wins
const resolveConflict = (update1: DataUpdate, update2: DataUpdate) => {
  return update1.timestamp > update2.timestamp ? update1 : update2
}

Conflict Notifications

Users are notified when their changes are overwritten:
notifications.show({
  title: 'Update Conflict',
  message: `Your changes to ${columnName} were overwritten by ${userName}`,
  color: 'orange',
  autoClose: 5000,
})

Collaboration Patterns

Read-Heavy Workflows

Ideal for:
  • Dashboard viewing
  • Report generation
  • Data exploration
  • Analysis review
// Multiple users can view without interference
const tableData = useQuery(api.csv.getTableData, { tableName })
const insights = useQuery(api.insights.get, { tableName })

Write-Heavy Workflows

Best practices:
  • Communicate before making changes
  • Use query history to track modifications
  • Enable notifications for the table
  • Consider locking critical operations

Analysis Handoff

Pass analysis work between team members:
  1. Share Agent Threads: Send the thread ID to collaborators
  2. Export Query History: Download and share SQL queries
  3. Snapshot Charts: Save chart configurations for reuse
  4. Document Insights: Add notes to insight panels

API Integration

Collaboration features use these Convex APIs:

Presence API

// Update presence
await updatePresence({
  userId: currentUser.id,
  tableName: currentTable,
  timestamp: Date.now(),
})

// Get active users
const activeUsers = await getActiveUsers({ tableName })

Notifications API

// Send notification
await sendNotification({
  type: 'query_executed',
  tableName,
  userId: currentUser.id,
  metadata: { query: sqlQuery },
})

// Get notifications
const notifications = await getNotifications({
  userId: currentUser.id,
  tableName,
})

Query Log API

// Log query
await logQuery({
  userId: currentUser.id,
  tableName,
  query: sqlQuery,
  success: true,
  rowsAffected: 42,
  executionTime: 125, // ms
})

// Get query history
const history = await getQueryHistory({ tableName })
See API Reference for full details.

Privacy & Security

All users in a workspace can see each other’s queries and presence. Ensure team members have appropriate access levels.

Access Control

Collaboration respects workspace permissions:
  • Viewers: Can see data and presence, cannot modify
  • Editors: Can query and modify data
  • Admins: Full access including user management

Audit Trail

All actions are logged for accountability:
interface AuditLogEntry {
  userId: string
  action: 'query' | 'update' | 'delete' | 'create'
  tableName: string
  details: string
  timestamp: number
  ipAddress?: string
}

Performance Considerations

Subscription Limits

Convex subscriptions are efficient, but monitor:
  • Number of active presence subscriptions
  • Notification subscription count
  • Query log subscription size

Pagination

For large query histories, use pagination:
const { results, loadMore, isLoading } = usePaginatedQuery(
  api.queryLog.getLogsForTable,
  { tableName },
  { initialNumItems: 50 }
)

Next Steps

Query History

Learn advanced collaboration workflows

Insights Discovery

Generate and share insights with your team

Build docs developers (and LLMs) love