Initial commit
This commit is contained in:
147
.claude/skills/pocketbase-best-practices/rules/realtime-auth.md
Normal file
147
.claude/skills/pocketbase-best-practices/rules/realtime-auth.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: Authenticate Realtime Connections
|
||||
impact: MEDIUM
|
||||
impactDescription: Secure subscriptions respecting API rules
|
||||
tags: realtime, authentication, security, subscriptions
|
||||
---
|
||||
|
||||
## Authenticate Realtime Connections
|
||||
|
||||
Realtime subscriptions respect collection API rules. Ensure the connection is authenticated before subscribing to protected data.
|
||||
|
||||
**Incorrect (subscribing without auth context):**
|
||||
|
||||
```javascript
|
||||
// Subscribing before authentication
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
// This will fail or return no data if collection requires auth
|
||||
pb.collection('private_messages').subscribe('*', (e) => {
|
||||
// Won't receive events - not authenticated!
|
||||
console.log(e.record);
|
||||
});
|
||||
|
||||
// Later user logs in, but subscription doesn't update
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
// Existing subscription still unauthenticated!
|
||||
```
|
||||
|
||||
**Correct (authenticated subscriptions):**
|
||||
|
||||
```javascript
|
||||
// Subscribe after authentication
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
async function initRealtime() {
|
||||
// First authenticate
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
|
||||
// Now subscribe - will use auth context
|
||||
pb.collection('private_messages').subscribe('*', (e) => {
|
||||
// Receives events for messages user can access
|
||||
console.log('New message:', e.record);
|
||||
});
|
||||
}
|
||||
|
||||
// Re-subscribe after auth changes
|
||||
function useAuthenticatedRealtime() {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const unsubRef = useRef(null);
|
||||
|
||||
// Watch auth changes
|
||||
useEffect(() => {
|
||||
const removeListener = pb.authStore.onChange((token, record) => {
|
||||
// Unsubscribe old connection
|
||||
if (unsubRef.current) {
|
||||
unsubRef.current();
|
||||
unsubRef.current = null;
|
||||
}
|
||||
|
||||
// Re-subscribe with new auth context if logged in
|
||||
if (record) {
|
||||
setupSubscription();
|
||||
} else {
|
||||
setMessages([]);
|
||||
}
|
||||
}, true);
|
||||
|
||||
return () => {
|
||||
removeListener();
|
||||
if (unsubRef.current) unsubRef.current();
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function setupSubscription() {
|
||||
unsubRef.current = await pb.collection('private_messages').subscribe('*', (e) => {
|
||||
handleMessage(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle auth token refresh with realtime
|
||||
pb.realtime.subscribe('PB_CONNECT', async (e) => {
|
||||
console.log('Realtime connected');
|
||||
|
||||
// Verify auth is still valid
|
||||
if (pb.authStore.isValid) {
|
||||
try {
|
||||
await pb.collection('users').authRefresh();
|
||||
} catch {
|
||||
pb.authStore.clear();
|
||||
// Redirect to login
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**API rules apply to subscriptions:**
|
||||
|
||||
```javascript
|
||||
// Collection rule: listRule: 'owner = @request.auth.id'
|
||||
|
||||
// User A subscribed
|
||||
await pb.collection('users').authWithPassword('a@test.com', 'password');
|
||||
pb.collection('notes').subscribe('*', handler);
|
||||
// Only receives events for notes where owner = User A
|
||||
|
||||
// Events from other users' notes are filtered out automatically
|
||||
```
|
||||
|
||||
**Subscription authorization flow:**
|
||||
|
||||
1. SSE connection established (no auth check)
|
||||
2. First subscription triggers authorization
|
||||
3. Auth token from `pb.authStore` is used
|
||||
4. Collection rules evaluated for each event
|
||||
5. Only matching events sent to client
|
||||
|
||||
**Handling auth expiration:**
|
||||
|
||||
```javascript
|
||||
// Setup disconnect handler
|
||||
pb.realtime.onDisconnect = (subscriptions) => {
|
||||
console.log('Disconnected, had subscriptions:', subscriptions);
|
||||
|
||||
// Check if auth expired
|
||||
if (!pb.authStore.isValid) {
|
||||
// Token expired - need to re-authenticate
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection issue - realtime will auto-reconnect
|
||||
// Re-subscribe after reconnection
|
||||
pb.realtime.subscribe('PB_CONNECT', () => {
|
||||
resubscribeAll(subscriptions);
|
||||
});
|
||||
};
|
||||
|
||||
function resubscribeAll(subscriptions) {
|
||||
subscriptions.forEach(sub => {
|
||||
const [collection, topic] = sub.split('/');
|
||||
pb.collection(collection).subscribe(topic, handlers[sub]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [PocketBase Realtime Auth](https://pocketbase.io/docs/api-realtime/)
|
||||
Reference in New Issue
Block a user