Files
gh-whamp-whamp-claude-tools…/skills/pocketbase/references/api/api_realtime.md
2025-11-30 09:06:02 +08:00

13 KiB

Realtime API

Overview

PocketBase provides real-time updates via WebSocket connections, allowing your application to receive instant notifications when data changes.

Connection

Automatic Connection (SDK)

// SDK automatically manages WebSocket connection
const pb = new PocketBase('http://127.0.0.1:8090');

// Connection is established automatically
pb.realtime.connection.addListener('open', () => {
  console.log('Connected to realtime');
});

pb.realtime.connection.addListener('close', () => {
  console.log('Disconnected from realtime');
});

pb.realtime.connection.addListener('error', (error) => {
  console.error('Realtime error:', error);
});

Manual WebSocket Connection

const ws = new WebSocket('ws://127.0.0.1:8090/api/realtime');

ws.onopen = function() {
  console.log('WebSocket connected');
};

ws.onclose = function() {
  console.log('WebSocket disconnected');
};

ws.onerror = function(error) {
  console.error('WebSocket error:', error);
};

ws.onmessage = function(event) {
  const data = JSON.parse(event.data);
  console.log('Real-time event:', data);
};

Subscriptions

Subscribe to Collection

Listen to all changes in a collection:

// Subscribe to all posts changes
pb.collection('posts').subscribe('*', function (e) {
  console.log(e.action);  // 'create', 'update', or 'delete'
  console.log(e.record);  // Changed record

  if (e.action === 'create') {
    // New post created
  } else if (e.action === 'update') {
    // Post updated
  } else if (e.action === 'delete') {
    // Post deleted
  }
});

Subscribe to Specific Record

Listen to changes for a specific record:

// Subscribe to specific post
pb.collection('posts').subscribe('RECORD_ID', function (e) {
  console.log('Post changed:', e.record);
});

// Multiple records
pb.collection('posts').subscribe('ID1', callback);
pb.collection('posts').subscribe('ID2', callback);

Subscribe via Admin Client

// Subscribe using admin client
pb.admin.onChange('records', 'posts', (action, record) => {
  console.log(`${action} on posts:`, record);
});

Event Object

Create Event

{
  "action": "create",
  "record": {
    "id": "RECORD_ID",
    "title": "New Post",
    "content": "Hello",
    "created": "2024-01-01T00:00:00.000Z",
    "updated": "2024-01-01T00:00:00.000Z"
  }
}

Update Event

{
  "action": "update",
  "record": {
    "id": "RECORD_ID",
    "title": "Updated Post",
    "content": "Updated content",
    "created": "2024-01-01T00:00:00.000Z",
    "updated": "2024-01-01T12:00:00.000Z"
  }
}

Delete Event

{
  "action": "delete",
  "record": {
    "id": "RECORD_ID"
  }
}

Unsubscribing

Unsubscribe from Specific Record

// Unsubscribe from specific record
pb.collection('posts').unsubscribe('RECORD_ID');

// Or using the subscription object
const unsubscribe = pb.collection('posts').subscribe('*', callback);
unsubscribe();  // Stop listening

Unsubscribe from Collection

// Unsubscribe from all collection changes
pb.collection('posts').unsubscribe();

Unsubscribe from All Collections

// Stop all subscriptions
pb.collections.unsubscribe();

Realtime with React

Hook Example

import { useEffect, useState } from 'react';

function useRealtime(collection, recordId, callback) {
  useEffect(() => {
    let unsubscribe;

    if (recordId) {
      // Subscribe to specific record
      unsubscribe = pb.collection(collection).subscribe(recordId, callback);
    } else {
      // Subscribe to all collection changes
      unsubscribe = pb.collection(collection).subscribe('*', callback);
    }

    return () => {
      if (unsubscribe) {
        unsubscribe();
      }
    };
  }, [collection, recordId]);
}

// Usage
function PostList() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    // Load initial data
    loadPosts();

    // Subscribe to realtime updates
    pb.collection('posts').subscribe('*', (e) => {
      if (e.action === 'create') {
        setPosts(prev => [e.record, ...prev]);
      } else if (e.action === 'update') {
        setPosts(prev => prev.map(p => p.id === e.record.id ? e.record : p));
      } else if (e.action === 'delete') {
        setPosts(prev => prev.filter(p => p.id !== e.record.id));
      }
    });

    return () => {
      pb.collection('posts').unsubscribe();
    };
  }, []);

  async function loadPosts() {
    const records = await pb.collection('posts').getList(1, 50);
    setPosts(records.items);
  }

  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

Optimized React Example

import { useEffect, useState } from 'react';

function PostDetails({ postId }) {
  const [post, setPost] = useState(null);

  useEffect(() => {
    if (!postId) return;

    // Load initial data
    loadPost();

    // Subscribe to this specific post
    const unsubscribe = pb.collection('posts').subscribe(postId, (e) => {
      setPost(e.record);
    });

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

  async function loadPost() {
    const record = await pb.collection('posts').getOne(postId);
    setPost(record);
  }

  if (!post) return <div>Loading...</div>;

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

Realtime with Vue.js

export default {
  data() {
    return {
      posts: []
    }
  },
  async mounted() {
    await this.loadPosts();

    // Subscribe to realtime updates
    pb.collection('posts').subscribe('*', (e) => {
      if (e.action === 'create') {
        this.posts.unshift(e.record);
      } else if (e.action === 'update') {
        const index = this.posts.findIndex(p => p.id === e.record.id);
        if (index !== -1) {
          this.posts.splice(index, 1, e.record);
        }
      } else if (e.action === 'delete') {
        this.posts = this.posts.filter(p => p.id !== e.record.id);
      }
    });
  },
  beforeUnmount() {
    pb.collection('posts').unsubscribe();
  },
  methods: {
    async loadPosts() {
      const records = await pb.collection('posts').getList(1, 50);
      this.posts = records.items;
    }
  }
}

Realtime with Vanilla JavaScript

const postsList = document.getElementById('posts');

async function loadPosts() {
  const response = await pb.collection('posts').getList(1, 50);
  renderPosts(response.items);
}

function renderPosts(posts) {
  postsList.innerHTML = posts.map(post => `
    <div class="post">
      <h3>${post.title}</h3>
      <p>${post.content}</p>
    </div>
  `).join('');
}

// Subscribe to realtime updates
pb.collection('posts').subscribe('*', (e) => {
  if (e.action === 'create') {
    prependPost(e.record);
  } else if (e.action === 'update') {
    updatePost(e.record);
  } else if (e.action === 'delete') {
    removePost(e.record.id);
  }
});

function prependPost(post) {
  const div = document.createElement('div');
  div.className = 'post';
  div.innerHTML = `<h3>${post.title}</h3><p>${post.content}</p>`;
  postsList.prepend(div);
}

// Initialize
loadPosts();

Use Cases

1. Live Chat

// Subscribe to messages
pb.collection('messages').subscribe('*', (e) => {
  if (e.action === 'create') {
    addMessageToUI(e.record);
  }
});

// Send message
async function sendMessage(content) {
  await pb.collection('messages').create({
    content: content,
    user: pb.authStore.model.id,
    room: roomId
  });
}

2. Notification System

// Subscribe to notifications
pb.collection('notifications').subscribe('*', (e) => {
  if (e.action === 'create' && e.record.user_id === pb.authStore.model.id) {
    showNotification(e.record.message);
    updateBadge();
  }
});

3. Collaborative Editing

// Subscribe to document changes
pb.collection('documents').subscribe('DOCUMENT_ID', (e) => {
  if (e.action === 'update') {
    updateEditor(e.record.content);
  }
});

// Debounce updates
let updateTimeout;
function onEditorChange(content) {
  clearTimeout(updateTimeout);
  updateTimeout = setTimeout(async () => {
    await pb.collection('documents').update('DOCUMENT_ID', {
      content: content
    });
  }, 500);
}

4. Live Dashboard

// Subscribe to metrics changes
pb.collection('metrics').subscribe('*', (e) => {
  if (e.action === 'update') {
    updateDashboard(e.record);
  }
});

// Subscribe to events
pb.collection('events').subscribe('*', (e) => {
  if (e.action === 'create') {
    addEventToFeed(e.record);
  }
});

5. Shopping Cart Updates

// Subscribe to cart changes
pb.collection('cart_items').subscribe('*', (e) => {
  if (e.action === 'create' && e.record.user_id === pb.authStore.model.id) {
    updateCartCount();
  } else if (e.action === 'delete') {
    updateCartCount();
  }
});

Authentication and Realtime

Authenticated Subscriptions

// Subscribe only after authentication
pb.collection('users').authWithPassword('email', 'password').then(() => {
  // Now subscribe to private data
  pb.collection('messages').subscribe('*', (e) => {
    // Will only receive messages user has access to
  });
});

Multiple User Types

// Different subscriptions based on role
if (pb.authStore.model.role === 'admin') {
  // Admin sees all updates
  pb.collection('posts').subscribe('*', handleAdminUpdate);
} else {
  // Regular users see limited updates
  pb.collection('posts').subscribe('*', handleUserUpdate);
}

Filtering Realtime Events

// Client-side filtering
pb.collection('posts').subscribe('*', (e) => {
  // Only show published posts
  if (e.record.status === 'published') {
    updateUI(e.record);
  }
});

// Or use server-side rules (better)

Performance Considerations

1. Limit Subscriptions

// Good - subscribe to specific records needed
pb.collection('posts').subscribe('POST_ID', callback);

// Bad - subscribe to everything
pb.collection('posts').subscribe('*', callback); // Only when necessary

2. Unsubscribe When Done

useEffect(() => {
  const unsubscribe = pb.collection('posts').subscribe('*', callback);

  return () => {
    unsubscribe();  // Clean up
  };
}, []);

3. Batch UI Updates

// Instead of updating on every event
pb.collection('posts').subscribe('*', (e) => {
  updateUI(e.record);  // Triggers re-render every time
});

// Batch updates
const updates = [];
pb.collection('posts').subscribe('*', (e) => {
  updates.push(e.record);

  if (updates.length >= 10) {
    batchUpdateUI(updates);
    updates.length = 0;
  }
});

4. Use Debouncing for Frequent Updates

let updateTimeout;

pb.collection('metrics').subscribe('*', (e) => {
  clearTimeout(updateTimeout);

  updateTimeout = setTimeout(() => {
    updateDashboard();
  }, 100);  // Update at most every 100ms
});

Connection Management

Reconnection Strategy

pb.realtime.connection.addListener('close', () => {
  // Attempt reconnection
  setTimeout(() => {
    pb.realtime.connect();
  }, 5000);  // Reconnect after 5 seconds
});

Manual Connection Control

// Disconnect
pb.realtime.disconnect();

// Reconnect
pb.realtime.connect();

// Check connection status
const isConnected = pb.realtime.connection.isOpen;

Heartbeat

// Keep connection alive
setInterval(() => {
  if (pb.realtime.connection.isOpen) {
    pb.realtime.send({ action: 'ping' });
  }
}, 30000);  // Every 30 seconds

Error Handling

pb.collection('posts').subscribe('*', (e) => {
  try {
    handleEvent(e);
  } catch (error) {
    console.error('Error handling event:', error);
    // Don't let errors break the subscription
  }
});

// Handle connection errors
pb.realtime.connection.addListener('error', (error) => {
  console.error('Realtime connection error:', error);
  // Show error to user or attempt reconnection
});

Security

Server-Side Security

Realtime events respect collection rules:

// Users will only receive events for records they can access
// No need for additional client-side filtering based on permissions

Client-Side Validation

pb.collection('posts').subscribe('*', (e) => {
  // Validate event data
  if (!e.record || !e.action) {
    console.warn('Invalid event:', e);
    return;
  }

  // Process event
  handleEvent(e);
});

Troubleshooting

Not receiving events

  • Check if subscribed to correct collection
  • Verify user is authenticated
  • Check console for errors
  • Ensure WebSocket connection is open

Receiving too many events

  • Unsubscribe from unnecessary subscriptions
  • Filter events client-side
  • Use more specific subscriptions

Memory leaks

  • Always unsubscribe in component cleanup
  • Check for duplicate subscriptions
  • Use useEffect cleanup function

Disconnections

  • Implement reconnection logic
  • Add heartbeat/ping
  • Show connection status to user