https://github.com/cloudcanal/connect
A JavaScript framework for building web applications with PocketBase, including state management and event-driven architecture.
https://github.com/cloudcanal/connect
javascript-framework pocketbase
Last synced: 3 months ago
JSON representation
A JavaScript framework for building web applications with PocketBase, including state management and event-driven architecture.
- Host: GitHub
- URL: https://github.com/cloudcanal/connect
- Owner: cloudcanal
- License: mit
- Created: 2025-12-27T18:58:56.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-01-07T20:59:47.000Z (3 months ago)
- Last Synced: 2026-01-08T21:30:00.501Z (3 months ago)
- Topics: javascript-framework, pocketbase
- Language: TypeScript
- Homepage: https://www.cloudcanal.io/connect
- Size: 161 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Connect
A JavaScript library for building browser applications with [PocketBase](https://pocketbase.io/). Provides unified state management, event handling, and database operations through a simple API.
## Installation
Include via CDN (jsDelivr):
```html
```
The library automatically attaches to `window.cc` and auto-initializes with `window.location.origin` as the PocketBase URL.
## Quick Start
```html
Login
// Listen for auth changes
cc.events.on('auth:change', ({ user, isAuthenticated }) => {
document.getElementById('user-info').textContent =
isAuthenticated
? `Welcome, ${user.email}`
: 'Not logged in';
});
// Handle login button click
document.getElementById('login-btn').addEventListener('click', async () => {
try {
await cc.db.login('user@example.com', 'password123');
} catch (e) {
console.error('Login failed:', e);
}
});
```
---
## Core Concepts
Connect consists of three modules:
| Module | Purpose |
| ----------- | ------------------------------------------------------------------------------ |
| `cc.state` | Key-value store with optional persistence and TTL |
| `cc.events` | Unified event system for custom events and realtime subscriptions |
| `cc.db` | PocketBase wrapper for auth, CRUD operations, and file handling |
All modules work together seamlessly. State changes emit events. Database operations emit events. Subscribing to database events automatically enables realtime updates.
---
## API Reference
### cc.state
A reactive key-value store with optional persistence to `sessionStorage` or `localStorage`.
#### `state.get(key: string): T | undefined`
Retrieve a value from state.
```javascript
const username = cc.state.get('username');
const user = cc.state.get('currentUser'); // Returns object if stored
```
#### `state.set(key: string, value: unknown, options?: StateOptions): void`
Store a value. Emits `state:{key}` event on change.
```javascript
// Memory only (cleared on page refresh)
cc.state.set('tempData', { foo: 'bar' });
// Persist to sessionStorage (cleared when tab closes)
cc.state.set('sessionData', 'value', { persist: 'session' });
// Persist to localStorage (persists across sessions)
cc.state.set('preferences', { theme: 'dark' }, { persist: 'local' });
// With TTL (auto-expires after 5 minutes)
cc.state.set('cache', data, { ttl: 300000 });
// Combine persistence and TTL
cc.state.set('token', 'abc123', { persist: 'local', ttl: 3600000 });
```
**StateOptions:**
| Option | Type | Description |
| --------- | ------------------------ | ---------------------------- |
| `persist` | `'session'` \| `'local'` | Storage backend |
| `ttl` | `number` | Time to live in milliseconds |
#### `state.has(key: string): boolean`
Check if a key exists (and hasn't expired).
```javascript
if (cc.state.has('user')) {
// User data exists
}
```
#### `state.remove(key: string): void`
Remove a key from state. Emits `state:{key}` event.
```javascript
cc.state.remove('tempData');
```
#### `state.list(): Array<{ key: string; storage: 'memory' | 'session' | 'local' }>`
List all state keys and their storage locations (useful for debugging).
```javascript
cc.state.set('user', { name: 'Pat' });
cc.state.set('theme', 'dark', { persist: 'local' });
console.log(cc.state.list());
// [
// { key: 'user', storage: 'memory' },
// { key: 'theme', storage: 'local' }
// ]
```
#### `state.clear(): void`
Clear all state (memory and persisted). Does not emit events.
```javascript
cc.state.clear();
```
---
### cc.events
Unified event system supporting custom events and automatic realtime subscriptions.
#### `events.on(event: string, callback: EventCallback): void`
Subscribe to an event.
```javascript
cc.events.on('auth:login', ({ user }) => {
console.log('User logged in:', user.email);
});
cc.events.on('state:theme', ({ value, oldValue }) => {
console.log('Theme changed from', oldValue, 'to', value);
});
```
#### `events.off(event: string, callback: EventCallback): void`
Unsubscribe from an event.
```javascript
const handler = ({ user }) => console.log(user);
cc.events.on('auth:login', handler);
cc.events.off('auth:login', handler);
```
#### `events.once(event: string, callback: EventCallback): void`
Subscribe to an event once (auto-unsubscribes after first call).
```javascript
cc.events.once('auth:login', ({ user }) => {
showWelcomeModal(user);
});
```
#### `events.emit(event: string, payload?: T): void`
Emit a custom event.
```javascript
cc.events.emit('cart:updated', { itemCount: 5 });
cc.events.emit('notification', { message: 'Item added!', type: 'success' });
```
#### `events.clear(event?: string): void`
Remove all handlers for an event, or all events if no name provided.
```javascript
cc.events.clear('auth:login'); // Clear specific event
cc.events.clear(); // Clear all events
```
#### `events.list(): Array<{ event: string }>`
List all active listeners (useful for debugging).
```javascript
console.log(cc.events.list());
// [
// { event: 'auth:login' },
// { event: 'db:posts:create' }
// ]
```
---
### cc.db
PocketBase wrapper with authentication, CRUD operations, realtime, and file handling.
#### Configuration
```javascript
// Change PocketBase URL (default: window.location.origin)
cc.db.url = 'https://api.example.com';
// Enable auto-cancellation for duplicate requests (default: false)
cc.db.autoCancellation = true;
// Get underlying PocketBase client for advanced usage
const pb = cc.db.client();
```
#### Authentication State
```javascript
// Check if user is authenticated
if (cc.db.isAuthenticated()) {
console.log('User is logged in');
}
// Get current user
const user = cc.db.getUser();
console.log(user.email, user.id);
// With custom user type
const user = cc.db.getUser<{ email: string; role: string }>();
```
#### Sign Up
```javascript
const user = await cc.db.signup('user@example.com', 'password123');
// With additional data
const user = await cc.db.signup('user@example.com', 'password123', {
name: 'John Doe',
role: 'member',
});
```
#### Login
```javascript
// Email/password
const user = await cc.db.login('user@example.com', 'password123');
// OAuth2
const user = await cc.db.loginWithOAuth('google');
const user = await cc.db.loginWithOAuth('github');
```
#### Logout
```javascript
cc.db.logout();
```
#### Token Refresh
```javascript
const user = await cc.db.refreshAuth();
```
#### Password Reset
```javascript
// Request reset email
await cc.db.resetPassword('user@example.com');
// Confirm reset (from email link)
await cc.db.confirmResetPassword(token, 'newPassword123');
```
#### Email Verification
```javascript
// Request verification email
await cc.db.requestVerification('user@example.com');
// Confirm verification (from email link)
await cc.db.confirmVerification(token);
```
#### CRUD Operations
##### Get Single Record
```javascript
const post = await cc.db.get('posts', 'RECORD_ID');
// With expand
const post = await cc.db.get('posts', 'RECORD_ID', { expand: 'author' });
```
##### List Records (Paginated)
```javascript
const result = await cc.db.list('posts');
// { page: 1, perPage: 20, totalItems: 100, totalPages: 5, items: [...] }
// With options
const result = await cc.db.list('posts', {
page: 2,
perPage: 10,
filter: 'status = "published"',
sort: '-created',
expand: 'author,comments',
});
```
##### Get All Records
```javascript
const allPosts = await cc.db.getAll('posts');
// With filter
const myPosts = await cc.db.getAll('posts', {
filter: `author = "${userId}"`,
});
```
##### Get First Matching Record
```javascript
const post = await cc.db.getFirst('posts', 'slug = "hello-world"');
// Returns null if not found (doesn't throw)
```
##### Create Record
```javascript
const post = await cc.db.create('posts', {
title: 'Hello World',
content: 'My first post',
status: 'draft',
});
```
##### Update Record
```javascript
const updated = await cc.db.update('posts', 'RECORD_ID', {
status: 'published',
});
```
##### Delete Record
```javascript
await cc.db.delete('posts', 'RECORD_ID');
```
#### File Uploads
Use `FormData` with `create` or `update`:
```javascript
// Create with file
const form = new FormData();
form.append('title', 'My Image');
form.append('image', fileInput.files[0]);
const record = await cc.db.create('gallery', form);
// Update with file
const form = new FormData();
form.append('avatar', fileInput.files[0]);
await cc.db.update('users', userId, form);
```
#### Get File URL
```javascript
const url = cc.db.getFileUrl(record, record.image);
// With thumbnail
const thumbUrl = cc.db.getFileUrl(record, record.image, { thumb: '100x100' });
```
---
## Events Reference
### Authentication Events
| Event | Payload | Triggered When |
| --------------------- | ---------------------------------------------------- | ------------------------------------------------- |
| `auth:change` | `{ user: DbUser \| null, isAuthenticated: boolean }` | Auth state changes (login, logout, token refresh) |
| `auth:signup` | `{ user: DbUser }` | User signs up |
| `auth:login` | `{ user: DbUser }` | User logs in |
| `auth:logout` | `{ user: DbUser \| null }` | User logs out |
| `auth:refresh` | `{ user: DbUser }` | Auth token refreshed |
| `auth:reset-request` | `{ email: string }` | Password reset requested |
| `auth:reset-confirm` | `{}` | Password reset confirmed |
| `auth:verify-request` | `{ email: string }` | Email verification requested |
| `auth:verify-confirm` | `{}` | Email verification confirmed |
### Database Events (Realtime)
Subscribe to database events to automatically enable realtime updates.
**Collection-wide format:** `db:{collection}:{action}`
**Record-specific format:** `db:{collection}:{action}:{id}` (update/delete only)
| Event Pattern | Payload | Description |
| ----------------------------- | ------------------------- | ----------------------- |
| `db:{collection}:create` | `{ record: RecordModel }` | Any record created |
| `db:{collection}:update` | `{ record: RecordModel }` | Any record updated |
| `db:{collection}:delete` | `{ id: string }` | Any record deleted |
| `db:{collection}:update:{id}` | `{ record: RecordModel }` | Specific record updated |
| `db:{collection}:delete:{id}` | `{ id: string }` | Specific record deleted |
**Examples:**
```javascript
// Listen to all posts updates
cc.events.on('db:posts:create', ({ record }) => {
console.log('New post:', record.title);
});
cc.events.on('db:posts:update', ({ record }) => {
console.log('Post updated:', record.id);
});
// Listen to a specific post only
cc.events.on('db:posts:update:abc123', ({ record }) => {
console.log('Post abc123 was updated:', record.title);
});
cc.events.on('db:posts:delete:abc123', ({ id }) => {
console.log('Post abc123 was deleted');
});
```
**Automatic Realtime Management:**
- Subscribing to any `db:*` event automatically enables PocketBase realtime
- Collection-wide events use `subscribe('*', ...)` for all records
- Record-specific events use `subscribe(recordId, ...)` for targeted updates
- Unsubscribing from the last listener automatically disables the subscription
- No manual subscription management required
### State Events
| Event Pattern | Payload | Description |
| ------------- | --------------------------------------- | ---------------------------- |
| `state:{key}` | `{ value: unknown, oldValue: unknown }` | State key changed or deleted |
**Examples:**
```javascript
cc.events.on('state:theme', ({ value, oldValue }) => {
document.body.className = value;
});
cc.events.on('state:cart', ({ value }) => {
updateCartBadge(value?.items?.length || 0);
});
```
---
## Complete Examples
### User Authentication Flow
```html
Login
Sign Up
Login with Google
Logout
// Update UI on auth changes
cc.events.on('auth:change', ({ user, isAuthenticated }) => {
document.getElementById('auth-form').style.display = isAuthenticated
? 'none'
: 'block';
document.getElementById('user-area').style.display = isAuthenticated
? 'block'
: 'none';
if (user) {
document.getElementById('user-email').textContent = user.email;
}
});
// Login form
document.getElementById('auth-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = new FormData(e.target);
try {
await cc.db.login(form.get('email'), form.get('password'));
} catch (err) {
alert('Login failed: ' + err.message);
}
});
// Sign up
document.getElementById('signup-btn').addEventListener('click', async () => {
const email = document.querySelector('[name="email"]').value;
const password = document.querySelector('[name="password"]').value;
try {
await cc.db.signup(email, password);
await cc.db.login(email, password);
} catch (err) {
alert('Signup failed: ' + err.message);
}
});
// Google OAuth
document.getElementById('google-btn').addEventListener('click', async () => {
try {
await cc.db.loginWithOAuth('google');
} catch (err) {
alert('OAuth failed: ' + err.message);
}
});
// Logout
document.getElementById('logout-btn').addEventListener('click', () => {
cc.db.logout();
});
```
### Realtime Chat Application
```html
Send
const messagesDiv = document.getElementById('messages');
const currentUser = cc.db.getUser();
// Load existing messages
async function loadMessages() {
const messages = await cc.db.getAll('messages', {
sort: 'created',
expand: 'author',
});
messagesDiv.innerHTML = '';
messages.forEach(addMessageToUI);
}
function addMessageToUI(msg) {
const div = document.createElement('div');
div.className = 'message';
div.dataset.id = msg.id;
div.innerHTML = `
<strong>${msg.expand?.author?.name || 'Unknown'}:</strong>
${msg.content}
`;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// Send message
document.getElementById('message-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = new FormData(e.target);
await cc.db.create('messages', {
content: form.get('content'),
author: currentUser.id,
});
e.target.reset();
});
// Realtime: new messages
cc.events.on('db:messages:create', ({ record }) => {
addMessageToUI(record);
});
// Realtime: deleted messages
cc.events.on('db:messages:delete', ({ id }) => {
document.querySelector(`.message[data-id="${id}"]`)?.remove();
});
// Initialize
loadMessages();
```
### Todo List with Persistence
```html
// Load todos from state or fetch from server
async function init() {
let todos = cc.state.get('todos');
if (!todos) {
todos = await cc.db.getAll('todos', {
filter: `user = "${cc.db.getUser()?.id}"`,
});
cc.state.set('todos', todos);
}
renderTodos(todos);
}
function renderTodos(todos) {
const list = document.getElementById('todo-list');
list.innerHTML = todos
.map(
(t) => `
<li data-id="${t.id}">
<input type="checkbox" ${t.completed ? 'checked' : ''}>
<span>${t.title}</span>
<button class="delete-btn">Delete</button>
</li>
`
)
.join('');
}
// React to state changes
cc.events.on('state:todos', ({ value }) => {
renderTodos(value || []);
});
// Add todo
document.getElementById('new-todo').addEventListener('keypress', async (e) => {
if (e.key !== 'Enter' || !e.target.value.trim()) return;
const todo = await cc.db.create('todos', {
title: e.target.value.trim(),
completed: false,
user: cc.db.getUser().id,
});
const todos = cc.state.get('todos') || [];
cc.state.set('todos', [...todos, todo]);
e.target.value = '';
});
// Toggle completion and delete (event delegation)
document.getElementById('todo-list').addEventListener('click', async (e) => {
const li = e.target.closest('li');
if (!li) return;
const id = li.dataset.id;
if (e.target.matches('input[type="checkbox"]')) {
const completed = e.target.checked;
await cc.db.update('todos', id, { completed });
const todos = cc.state
.get('todos')
.map((t) => (t.id === id ? { ...t, completed } : t));
cc.state.set('todos', todos);
}
if (e.target.matches('.delete-btn')) {
await cc.db.delete('todos', id);
const todos = cc.state.get('todos').filter((t) => t.id !== id);
cc.state.set('todos', todos);
}
});
// Sync with realtime updates from other devices
cc.events.on('db:todos:create', ({ record }) => {
const todos = cc.state.get('todos') || [];
if (!todos.find((t) => t.id === record.id)) {
cc.state.set('todos', [...todos, record]);
}
});
cc.events.on('db:todos:update', ({ record }) => {
const todos = cc.state
.get('todos')
.map((t) => (t.id === record.id ? record : t));
cc.state.set('todos', todos);
});
cc.events.on('db:todos:delete', ({ id }) => {
const todos = cc.state.get('todos').filter((t) => t.id !== id);
cc.state.set('todos', todos);
});
init();
```
### Image Gallery with Uploads
```html
Upload
async function loadGallery() {
const images = await cc.db.getAll('gallery', { sort: '-created' });
renderGallery(images);
}
function renderGallery(images) {
const gallery = document.getElementById('gallery');
gallery.innerHTML = images
.map(
(img) => `
<div class="image-card" data-id="${img.id}">
<img src="${cc.db.getFileUrl(img, img.image, {
thumb: '300x300',
})}" alt="${img.caption}">
<p>${img.caption || ''}</p>
<button class="delete-btn">Delete</button>
</div>
`
)
.join('');
}
// Upload image
document.getElementById('upload-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = new FormData(e.target);
form.append('user', cc.db.getUser().id);
await cc.db.create('gallery', form);
e.target.reset();
});
// Delete image (event delegation)
document.getElementById('gallery').addEventListener('click', async (e) => {
if (e.target.matches('.delete-btn')) {
const id = e.target.closest('.image-card').dataset.id;
await cc.db.delete('gallery', id);
}
});
// Realtime updates
cc.events.on('db:gallery:create', loadGallery);
cc.events.on('db:gallery:delete', loadGallery);
loadGallery();
```
---
## Configuration
### PocketBase URL
By default, Connect uses `window.location.origin`. Override for different backend:
```javascript
cc.db.url = 'https://api.myapp.com';
```
### Auto-Cancellation
PocketBase auto-cancels duplicate pending requests by default. Connect disables this:
```javascript
// Default: false (requests not auto-cancelled)
cc.db.autoCancellation = false;
// Enable if you want duplicate requests cancelled
cc.db.autoCancellation = true;
```
### Advanced PocketBase Access
For features not wrapped by Connect, access the underlying client:
```javascript
const pb = cc.db.client();
// Use any PocketBase SDK feature
pb.health.check();
pb.backups.getFullList();
```
---
## TypeScript Support
Connect exports TypeScript types:
```typescript
import {
cc,
state,
events,
db,
StateOptions,
EventCallback,
DbUser,
ListOptions,
ListResult,
} from 'cloud-canal-connect';
// Custom user type
interface MyUser extends DbUser {
role: 'admin' | 'user';
plan: string;
}
const user = cc.db.getUser();
```
---
## License
MIT