> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shipnative.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Realtime Features

> Build chat apps, activity feeds, and presence tracking

Shipnative includes ready-to-use hooks for common realtime patterns like chat, presence tracking, and live updates.

## Backend Comparison

| Feature                | Supabase             | Convex                    |
| ---------------------- | -------------------- | ------------------------- |
| **Realtime Mechanism** | PostgreSQL Changes   | Built-in reactivity       |
| **Setup Required**     | Enable per table     | Automatic                 |
| **Subscriptions**      | Manual setup         | Automatic with `useQuery` |
| **Typing Indicators**  | Via Presence channel | Via mutations             |
| **Presence**           | Via Presence API     | Via queries               |

***

## Quick Start

<Tabs>
  <Tab title="Supabase">
    ```typescript theme={null}
    import { useRealtimeMessages } from '@/hooks/useRealtimeMessages'

    function ChatRoom({ channelId }: { channelId: string }) {
      const { messages, sendMessage, isConnected } = useRealtimeMessages({ channelId })

      return (
        <View>
          {messages.map(msg => <Text key={msg.id}>{msg.content}</Text>)}
          <Button onPress={() => sendMessage('Hello!')} title="Send" />
        </View>
      )
    }
    ```
  </Tab>

  <Tab title="Convex">
    ```typescript theme={null}
    import { useQuery, useMutation } from '@/hooks/convex'
    import { api } from '@convex/_generated/api'

    function ChatRoom({ channelId }: { channelId: string }) {
      // Messages auto-update in real-time
      const messages = useQuery(api.messages.list, { channelId })
      const sendMessage = useMutation(api.messages.send)

      return (
        <View>
          {messages?.map(msg => <Text key={msg._id}>{msg.content}</Text>)}
          <Button
            onPress={() => sendMessage({ channelId, content: 'Hello!' })}
            title="Send"
          />
        </View>
      )
    }
    ```
  </Tab>
</Tabs>

***

## Chat with Supabase

### useRealtimeMessages

Full-featured chat hook with typing indicators.

```typescript theme={null}
const {
  messages,           // Current messages
  sendMessage,        // Send a new message
  updateMessage,      // Edit a message
  deleteMessage,      // Delete a message
  typingUsers,        // Users currently typing
  setTyping,          // Broadcast typing state
  isConnected,        // Connection status
  loading,            // Loading state
  error,              // Error state
  refresh,            // Refresh messages
} = useRealtimeMessages({
  channelId: 'room-123',
  maxMessages: 100,
  onNewMessage: (msg) => playNotificationSound(),
  onTypingChange: (users) => console.log('Typing:', users),
})
```

<Accordion title="Full Chat Example">
  ```typescript theme={null}
  function ChatRoom({ channelId }: { channelId: string }) {
    const [inputText, setInputText] = useState('')
    const {
      messages,
      sendMessage,
      typingUsers,
      setTyping,
      isConnected,
      loading,
    } = useRealtimeMessages({
      channelId,
      onNewMessage: () => Haptics.impactAsync(),
    })

    const handleSend = async () => {
      if (!inputText.trim()) return
      const { error } = await sendMessage(inputText)
      if (!error) setInputText('')
    }

    const handleInputChange = (text: string) => {
      setInputText(text)
      setTyping(text.length > 0)
    }

    if (loading) return <LoadingSpinner />

    return (
      <View style={{ flex: 1 }}>
        {!isConnected && <Banner text="Reconnecting..." />}

        <FlatList
          data={messages}
          renderItem={({ item }) => (
            <MessageBubble
              content={item.content}
              isOwn={item.user_id === currentUserId}
              timestamp={item.created_at}
            />
          )}
          keyExtractor={(item) => item.id}
          inverted
        />

        {typingUsers.length > 0 && (
          <Text style={styles.typing}>
            {typingUsers.join(', ')} typing...
          </Text>
        )}

        <View style={styles.inputRow}>
          <TextInput
            value={inputText}
            onChangeText={handleInputChange}
            placeholder="Type a message..."
          />
          <Button onPress={handleSend} title="Send" />
        </View>
      </View>
    )
  }
  ```
</Accordion>

***

### useRealtimePresence

Track online users with status indicators.

```typescript theme={null}
const {
  presentUsers,       // List of online users
  userCount,          // Number of users online
  isConnected,        // Connection status
  updateStatus,       // Update your status
  updateCustomData,   // Update custom presence data
  isUserOnline,       // Check if specific user is online
  getUserPresence,    // Get user's presence state
} = useRealtimePresence({
  channelName: 'room:123',
  initialStatus: 'online',
  customData: { currentScreen: 'chat' },
  onUserJoin: (user) => console.log(`${user.user_id} joined`),
  onUserLeave: (userId) => console.log(`${userId} left`),
})
```

***

### useRealtimeSubscription

Generic hook for subscribing to any table changes.

```typescript theme={null}
const { isConnected, error, reconnect, disconnect } = useRealtimeSubscription<Order>({
  table: 'orders',
  event: 'INSERT',  // 'INSERT' | 'UPDATE' | 'DELETE' | '*'
  filter: { column: 'user_id', value: userId },
  onInsert: (order) => toast.success(`New order #${order.id}!`),
  onUpdate: (order, oldOrder) => console.log('Updated:', order),
  onDelete: (oldOrder) => console.log('Deleted:', oldOrder.id),
})
```

***

## Chat with Convex

With Convex, queries are automatically reactive - no manual subscription setup needed.

### Messages Query

```typescript theme={null}
// convex/messages.ts
import { query, mutation } from './_generated/server'
import { v } from 'convex/values'

export const list = query({
  args: { channelId: v.string() },
  handler: async (ctx, args) => {
    return await ctx.db
      .query('messages')
      .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))
      .order('desc')
      .take(100)
  },
})

export const send = mutation({
  args: { channelId: v.string(), content: v.string() },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) throw new Error('Not authenticated')

    return await ctx.db.insert('messages', {
      channelId: args.channelId,
      content: args.content,
      userId: identity.subject,
      createdAt: Date.now(),
    })
  },
})
```

### Using in Components

```typescript theme={null}
import { useQuery, useMutation } from '@/hooks/convex'
import { api } from '@convex/_generated/api'

function ChatRoom({ channelId }: { channelId: string }) {
  const messages = useQuery(api.messages.list, { channelId })
  const sendMessage = useMutation(api.messages.send)
  const [inputText, setInputText] = useState('')

  // messages automatically updates when new messages arrive!

  const handleSend = async () => {
    if (!inputText.trim()) return
    await sendMessage({ channelId, content: inputText })
    setInputText('')
  }

  if (messages === undefined) return <LoadingSpinner />

  return (
    <View style={{ flex: 1 }}>
      <FlatList
        data={messages}
        renderItem={({ item }) => (
          <MessageBubble
            content={item.content}
            isOwn={item.userId === currentUserId}
            timestamp={item.createdAt}
          />
        )}
        keyExtractor={(item) => item._id}
        inverted
      />

      <View style={styles.inputRow}>
        <TextInput
          value={inputText}
          onChangeText={setInputText}
          placeholder="Type a message..."
        />
        <Button onPress={handleSend} title="Send" />
      </View>
    </View>
  )
}
```

### Presence with Convex

The boilerplate includes ready-to-use presence functions in `convex/realtime.ts`:

```typescript theme={null}
import { useQuery, useMutation } from '@/hooks/convex'
import { api } from '@convex/_generated/api'

function OnlineUsers({ channel }: { channel: string }) {
  // Get all users in this channel (auto-updates!)
  const presence = useQuery(api.realtime.getPresence, { channel })

  // Join/leave channel
  const joinPresence = useMutation(api.realtime.joinPresence)
  const leavePresence = useMutation(api.realtime.leavePresence)
  const updatePresence = useMutation(api.realtime.updatePresence)

  useEffect(() => {
    // Join when component mounts
    joinPresence({ channel, state: { status: 'online' } })

    // Leave when component unmounts
    return () => {
      leavePresence({ channel })
    }
  }, [channel])

  return (
    <View>
      <Text>{presence?.count} users online</Text>
      {presence?.users.map(user => (
        <Text key={user.presenceId}>{user.user?.name}</Text>
      ))}
    </View>
  )
}
```

### Presence Functions

| Function                             | Description                            |
| ------------------------------------ | -------------------------------------- |
| `joinPresence({ channel, state })`   | Join a channel with optional state     |
| `updatePresence({ channel, state })` | Update your presence state (heartbeat) |
| `leavePresence({ channel })`         | Leave a channel                        |
| `getPresence({ channel })`           | Get all users in a channel (reactive)  |

### Custom Presence State

Store any data with your presence:

```typescript theme={null}
// Track cursor position
await updatePresence({
  channel: 'document:123',
  state: {
    cursor: { x: 100, y: 200 },
    status: 'editing',
    color: '#ff0000',
  }
})
```

### Broadcast with Convex

Send ephemeral messages to channel subscribers:

```typescript theme={null}
import { useQuery, useMutation } from '@/hooks/convex'
import { api } from '@convex/_generated/api'

function RealtimeUpdates({ channel }: { channel: string }) {
  // Subscribe to broadcasts (auto-updates!)
  const broadcasts = useQuery(api.realtime.subscribeBroadcast, {
    channel,
    since: Date.now() - 30000, // Last 30 seconds
  })

  // Send a broadcast
  const broadcast = useMutation(api.realtime.broadcast)

  const sendCursorMove = (x: number, y: number) => {
    broadcast({
      channel,
      event: 'cursor_move',
      payload: { x, y },
    })
  }

  return (
    <View>
      {broadcasts?.messages.map(msg => (
        <Text key={msg.id}>
          {msg.event}: {JSON.stringify(msg.payload)}
        </Text>
      ))}
    </View>
  )
}
```

### Broadcast Functions

| Function                                 | Description                       |
| ---------------------------------------- | --------------------------------- |
| `broadcast({ channel, event, payload })` | Send a message to all subscribers |
| `subscribeBroadcast({ channel, since })` | Get recent messages (reactive)    |

<Tip>
  **Auto-Cleanup:** Broadcast messages expire after 30 seconds and are automatically cleaned up by a cron job. This keeps your database lean for ephemeral data like cursor positions.
</Tip>

***

## Database Setup

<Tabs>
  <Tab title="Supabase">
    ### Messages Table

    ```sql theme={null}
    CREATE TABLE public.messages (
        id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
        channel_id TEXT NOT NULL,
        user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
        content TEXT NOT NULL,
        metadata JSONB DEFAULT '{}',
        created_at TIMESTAMPTZ DEFAULT NOW(),
        updated_at TIMESTAMPTZ
    );

    ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;

    CREATE POLICY "Users can read messages in their channels"
      ON public.messages FOR SELECT TO authenticated USING (true);

    CREATE POLICY "Users can insert their own messages"
      ON public.messages FOR INSERT TO authenticated
      WITH CHECK (auth.uid() = user_id);

    -- Enable realtime
    ALTER PUBLICATION supabase_realtime ADD TABLE public.messages;

    CREATE INDEX idx_messages_channel_id ON public.messages(channel_id);
    CREATE INDEX idx_messages_created_at ON public.messages(created_at DESC);
    ```

    ### Enable Realtime

    In Supabase Dashboard:

    1. Go to **Database** → **Replication**
    2. Under "Realtime", enable for your tables
    3. Or run: `ALTER PUBLICATION supabase_realtime ADD TABLE your_table;`
  </Tab>

  <Tab title="Convex">
    ### Realtime Schema (Pre-configured)

    The boilerplate includes presence and broadcast tables in `convex/schema.ts`:

    ```typescript theme={null}
    // Presence tracking
    presence: defineTable({
      channel: v.string(),
      userId: v.id("users"),
      state: v.optional(v.any()),
      joinedAt: v.number(),
      lastSeenAt: v.number(),
    })
      .index("by_channel", ["channel"])
      .index("by_channel_user", ["channel", "userId"])
      .index("by_lastSeenAt", ["lastSeenAt"]),

    // Broadcast messages
    broadcasts: defineTable({
      channel: v.string(),
      event: v.string(),
      payload: v.any(),
      senderId: v.optional(v.id("users")),
      createdAt: v.number(),
      expiresAt: v.number(),
    })
      .index("by_channel", ["channel"])
      .index("by_channel_created", ["channel", "createdAt"])
      .index("by_expiresAt", ["expiresAt"]),
    ```

    ### Automatic Cleanup

    Cron jobs in `convex/crons.ts` handle cleanup:

    * **Presence cleanup**: Every minute, removes stale presence older than 60 seconds
    * **Broadcast cleanup**: Every 5 minutes, removes expired broadcast messages

    Convex handles realtime automatically - no additional setup needed!
  </Tab>
</Tabs>

***

## Mock Mode

All realtime hooks work in mock mode for development without backend credentials:

* Messages are stored locally and persist during the session
* Presence shows the current user as online
* Subscriptions simulate connected state

<Tip>
  **Testing Tip:** Use mock mode to build and test your UI before connecting to a real backend.
</Tip>

***

## TypeScript Types

Import types for your realtime data:

```typescript theme={null}
import type {
  RealtimeMessage,
  RealtimeMessageWithUser,
  RealtimeActivity,
  PresenceState,
  TypingState,
} from '@/types/realtime'
```

See `apps/app/app/types/realtime.ts` for all available types.
