Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:06:02 +08:00
commit 02cab85880
53 changed files with 12367 additions and 0 deletions

View File

@@ -0,0 +1,664 @@
# API Rules and Filters
## Overview
PocketBase uses rule expressions to control access to collections and records. Rules are evaluated server-side and determine who can create, read, update, or delete data.
## Rule Types
There are four main rule types for collections:
1. **List Rule** - Controls who can list/query multiple records
2. **View Rule** - Controls who can view individual records
3. **Create Rule** - Controls who can create new records
4. **Update Rule** - Controls who can update records
5. **Delete Rule** - Controls who can delete records
## Rule Syntax
### Basic Comparison Operators
```javascript
// Equality
field = value
field != value
// String matching
field ~ "substring"
field !~ "substring"
field = "exact match"
// Numeric comparison
count > 10
age >= 18
price < 100
quantity != 0
// Date comparison
created >= "2024-01-01"
updated <= "2024-12-31"
published_date != null
```
### Logical Operators
```javascript
// AND
condition1 && condition2
condition1 && condition2 && condition3
// OR
condition1 || condition2
// NOT
!(condition)
status != "draft"
```
### Special Variables
```javascript
@request.auth // Current authenticated user object
@request.auth.id // Current user ID
@request.auth.email // User email
@request.auth.role // User role (admin, authenticated)
@request.auth.verified // Email verification status
@request.timestamp // Current server timestamp
```
### Field References
```javascript
// Reference own field
user_id = @request.auth.id
// Reference nested field (for JSON fields)
settings.theme = "dark"
// Reference array field
tags ~ ["javascript", "react"]
// Reference all elements in array
categoryId ~ ["tech", "programming", "web"]
```
### Array Operations
```javascript
// Check if array contains value
tags ~ "javascript"
// Check if any array element matches condition
categories.id ~ ["cat1", "cat2"]
// Check if array is not empty
images != []
// Check if array is empty
images = []
```
### String Operations
```javascript
// Pattern matching with wildcards
title ~ "Hello*"
// Case-sensitive regex
content ~ /pattern/i
// Starts with
title ~ "^Getting started"
// Contains
description ~ "important"
```
## Common Rule Patterns
### Owner-Based Access Control
**Users can only access their own records**
```javascript
// List Rule - show only user's records in lists
user_id = @request.auth.id
// View Rule - can only view own records
user_id = @request.auth.id
// Create Rule - only authenticated users can create
@request.auth.id != ""
// Update Rule - only owner can update
user_id = @request.auth.id
// Delete Rule - only owner can delete
user_id = @request.auth.id
```
### Public Read, Authenticated Write
**Anyone can read, only authenticated users can create/modify**
```javascript
// List Rule - public can read
status = "published"
// View Rule - public can view published items
status = "published"
// Create Rule - authenticated users only
@request.auth.id != ""
// Update Rule - author or admin can update
author_id = @request.auth.id || @request.auth.role = "admin"
// Delete Rule - author or admin can delete
author_id = @request.auth.id || @request.auth.role = "admin"
```
### Role-Based Access
**Different permissions based on user role**
```javascript
// Admins can do everything
List Rule: true
View Rule: true
Create Rule: @request.auth.role = "admin"
Update Rule: @request.auth.role = "admin"
Delete Rule: @request.auth.role = "admin"
// Moderators can manage non-admin content
List Rule: true
View Rule: true
Create Rule: @request.auth.role = "moderator" || @request.auth.role = "admin"
Update Rule: @request.auth.role = "moderator" || @request.auth.role = "admin"
Delete Rule: @request.auth.role = "admin"
// Regular users have limited access
List Rule: @request.auth.role != ""
View Rule: @request.auth.role != ""
Create Rule: @request.auth.role = "authenticated"
Update Rule: false
Delete Rule: false
```
### Status-Based Access
**Access based on record status**
```javascript
// Only show published content publicly
List Rule: status = "published"
// Drafts visible to authors
List Rule: status = "published" || author_id = @request.auth.id
// Published items visible to all
View Rule: status = "published"
// Authors can edit their own
Update Rule: author_id = @request.auth.id
// Deletion only for drafts
Delete Rule: status = "draft"
```
### Verified User Only
**Only verified users can interact**
```javascript
// Only verified users
Create Rule: @request.auth.verified = true
Update Rule: @request.auth.verified = true
Delete Rule: @request.auth.verified = true
```
### Time-Based Access
**Access based on time constraints**
```javascript
// Only future events
start_date > @request.timestamp
// Only published items or drafts for authors
status = "published" || (status = "draft" && author_id = @request.auth.id)
// Only items from last 30 days
created >= dateSubtract(@request.timestamp, 30, "days")
```
### Complex Multi-Condition Rules
**E-commerce order access**
```javascript
// Customers can view their own orders
List Rule: user_id = @request.auth.id
View Rule: user_id = @request.auth.id
// Staff can view all orders
View Rule: @request.auth.role = "staff" || user_id = @request.auth.id
// Only staff can create orders for customers
Create Rule: @request.auth.role = "staff"
// Customers can update their orders only if pending
Update Rule: (user_id = @request.auth.id && status = "pending") || @request.auth.role = "staff"
// Only staff can cancel orders
Delete Rule: @request.auth.role = "staff"
```
## Filtering in Queries
Rules control access, but you can also filter data in queries.
### Basic Filters
```javascript
// Equality
const records = await pb.collection('posts').getList(1, 50, {
filter: 'status = "published"'
});
// Not equal
filter: 'category != "draft"'
// Multiple conditions
filter: 'status = "published" && created >= "2024-01-01"'
// OR condition
filter: 'category = "tech" || category = "programming"'
```
### String Filters
```javascript
// Contains substring
filter: 'title ~ "PocketBase"'
// Not contains
filter: 'content !~ "spam"'
// Pattern matching
filter: 'title ~ "Getting started*"'
// Regex (case insensitive)
filter: 'content ~ /important/i'
// Starts with
filter: 'email ~ "^admin@"'
```
### Numeric Filters
```javascript
// Greater than
filter: 'price > 100'
// Greater than or equal
filter: 'age >= 18'
// Less than
filter: 'stock < 10'
// Less than or equal
filter: 'price <= 50'
// Between (inclusive)
filter: 'price >= 10 && price <= 100'
```
### Date Filters
```javascript
// After date
filter: 'created >= "2024-01-01"'
// Before date
filter: 'event_date <= "2024-12-31"'
// Date range
filter: 'created >= "2024-01-01" && created <= "2024-12-31"'
// Last 30 days
filter: 'created >= dateSubtract(@request.timestamp, 30, "days")'
// Next 7 days
filter: 'event_date <= dateAdd(@request.timestamp, 7, "days")'
```
### Array Filters
```javascript
// Array contains value
filter: 'tags ~ "javascript"'
// Array contains any of multiple values
filter: 'tags ~ ["javascript", "react", "vue"]'
// Array does not contain value
filter: 'categories !~ "private"'
// Check if array is not empty
filter: 'images != []'
// Check if array is empty
filter: 'comments = []'
```
### Relation Filters
```javascript
// Filter by related record field
filter: 'author.email = "user@example.com"'
// Expand and filter
filter: 'expand.author.role = "admin"'
// Multiple relation levels
filter: 'expand.post.expand.author.role = "moderator"'
```
### NULL Checks
```javascript
// Field is not null
filter: 'published_date != null'
// Field is null
filter: 'archived_date = null'
// Field exists (not null or empty string)
filter: 'deleted != ""'
```
## Sorting
```javascript
// Sort by single field
sort: 'created'
// Sort by field descending
sort: '-created'
// Sort by multiple fields
sort: 'status,-created'
// Sort by numeric field
sort: 'price'
// Sort by string field (alphabetical)
sort: 'title'
// Sort by relation field
sort: 'expand.author.name'
```
## Field Selection
```javascript
// Select specific fields
fields: 'id,title,author,created'
// Exclude large fields
fields: 'id,title,author,-content'
// Select all fields
fields: '*'
// Select with relations
fields: 'id,title,expand.author.name'
```
## Pagination
```javascript
// Get page 1 with 50 items per page
const page1 = await pb.collection('posts').getList(1, 50)
// Get page 2
const page2 = await pb.collection('posts').getList(2, 50)
// Get all (use carefully - can be slow)
const all = await pb.collection('posts').getFullList(200)
// Get with cursor-based pagination (PocketBase 0.20+)
const records = await pb.collection('posts').getList(1, 50, {
filter: 'created >= "2024-01-01"',
sort: 'created'
})
```
## Relation Expansion
```javascript
// Expand single relation
expand: 'author'
// Expand multiple relations
expand: 'author,comments'
// Expand nested relations
expand: 'author,comments.author'
// Access expanded data
const post = await pb.collection('posts').getOne('POST_ID', {
expand: 'author'
});
console.log(post.expand.author.email);
```
## Advanced Filter Functions
```javascript
// Date arithmetic
filter: 'created >= dateSubtract(@request.timestamp, 7, "days")'
// String length
filter: 'length(title) > 10'
// Count array elements
filter: 'count(tags) > 0'
// Case-insensitive matching
filter: 'lower(name) = lower("JOHN")'
// Extract JSON field
filter: 'settings->theme = "dark"'
```
## Performance Considerations
### Indexing for Filters
```sql
-- Create indexes for commonly filtered fields
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_created ON posts(created);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_status_created ON posts(status, created);
```
### Efficient Rules
**Good:**
```javascript
// Simple, indexed field comparison
user_id = @request.auth.id
status = "published"
created >= "2024-01-01"
```
**Avoid (can be slow):**
```javascript
// Complex string matching
title ~ /javascript.*framework/i
// Use equals or prefix matching instead
title = "JavaScript Framework"
// Nested relation checks
expand.post.expand.author.role = "admin"
// Pre-compute or use views
```
### Pagination Best Practices
```javascript
// Always paginate large datasets
const records = await pb.collection('posts').getList(1, 50, {
filter: 'status = "published"',
sort: '-created',
fields: 'id,title,author,created' // Select only needed fields
});
// Use cursor-based pagination for infinite scroll
let cursor = null;
const batch1 = await pb.collection('posts').getList(1, 50, {
sort: 'created'
});
cursor = batch1.items[batch1.items.length - 1].created;
const batch2 = await pb.collection('posts').getList(1, 50, {
filter: `created < "${cursor}"`,
sort: 'created'
});
```
## Real-time and Rules
Real-time subscriptions respect the same rules:
```javascript
// Subscribe to changes
pb.collection('posts').subscribe('*', function(e) {
console.log(e.action); // 'create', 'update', 'delete'
console.log(e.record); // Changed record
});
// User will only receive events for records they have access to
// based on their current rules
```
## Testing Rules
### Test as Different Users
```javascript
// Test public access
const publicPosts = await pb.collection('posts').getList(1, 50);
// Should respect public rules
// Test authenticated access
pb.collection('users').authWithPassword('user@example.com', 'password');
const userPosts = await pb.collection('posts').getList(1, 50);
// Should show more based on rules
// Test admin access
pb.admins.authWithPassword('admin@example.com', 'password');
const adminPosts = await pb.collection('posts').getList(1, 50);
// Should show everything
```
### Rule Testing Checklist
- [ ] Public users see appropriate data
- [ ] Authenticated users see correct data
- [ ] Users can't access others' private data
- [ ] Admins have full access
- [ ] Create rules work for authorized users
- [ ] Create rules block unauthorized users
- [ ] Update rules work correctly
- [ ] Delete rules work correctly
- [ ] Real-time updates respect rules
## Common Pitfalls
### 1. Forgetting List vs View Rules
```javascript
// WRONG - Both rules same
List Rule: user_id = @request.auth.id
View Rule: user_id = @request.auth.id
// RIGHT - Public can view, private in lists
List Rule: status = "published"
View Rule: status = "published" || user_id = @request.auth.id
```
### 2. Using Wrong Comparison
```javascript
// WRONG - string comparison for numbers
price > "100"
// RIGHT - numeric comparison
price > 100
```
### 3. Not Indexing Filtered Fields
```javascript
// If filtering by 'status', ensure index exists
CREATE INDEX idx_posts_status ON posts(status);
```
### 4. Over-restrictive Rules
```javascript
// Too restrictive - breaks functionality
List Rule: false // No one can see anything
// Better - allow authenticated users to read
List Rule: @request.auth.id != ""
```
### 5. Forgetting to Handle NULL
```javascript
// May not work if published_date is null
filter: 'published_date >= "2024-01-01"'
// Better - handle nulls explicitly
filter: 'published_date != null && published_date >= "2024-01-01"'
```
## Security Best Practices
1. **Start with restrictive rules**
```javascript
// Default to no access
Create Rule: @request.auth.role = "admin"
```
2. **Test rules thoroughly**
- Test as different user types
- Verify data isolation
- Check edge cases
3. **Log and monitor**
- Check for unauthorized access attempts
- Monitor rule performance
- Track slow queries
4. **Use views for complex access logic**
- Pre-compute expensive checks
- Simplify rule logic
5. **Regular security audits**
- Review rules periodically
- Check for privilege escalation
- Verify data isolation
## Related Topics
- [Collections](collections.md) - Collection configuration
- [Authentication](authentication.md) - User management
- [Working with Relations](working_with_relations.md) - Relationship patterns
- [Security Rules](../security_rules.md) - Comprehensive security patterns
- [API Records](../api_records.md) - Record CRUD operations

View File

@@ -0,0 +1,583 @@
# Authentication in PocketBase
## Overview
PocketBase provides comprehensive authentication features including:
- Email/password authentication
- OAuth2 integration (Google, GitHub, etc.)
- Magic link authentication
- Email verification
- Password reset
- JWT token management
- Role-based access control
## Auth Collections
User accounts are managed through **Auth Collections**. Unlike base collections, auth collections:
- Have built-in authentication fields
- Support OAuth2 providers
- Provide password management
- Include email verification
- Generate JWT tokens
### Built-in Auth Fields
```json
{
"id": "string (unique)",
"email": "string (required, unique)",
"password": "string (hashed)",
"passwordConfirm": "string (validation only)",
"emailVisibility": "boolean (default: true)",
"verified": "boolean (default: false)",
"created": "datetime (autodate)",
"updated": "datetime (autodate)",
"lastResetSentAt": "datetime",
"verificationToken": "string"
}
```
**Note:** Password fields are never returned in API responses for security.
## Registration
### Email/Password
```javascript
// Register new user
const authData = await pb.collection('users').create({
email: 'user@example.com',
password: 'password123',
passwordConfirm: 'password123',
name: 'John Doe' // custom field
});
// Returns:
// {
// token: "JWT_TOKEN",
// user: { ...user data... }
// }
```
**Features:**
- Automatic password hashing
- Email uniqueness validation
- Email verification (if enabled)
- Custom fields supported
### OAuth2 Registration/Login
```javascript
// With OAuth2 code (from provider redirect)
const authData = await pb.collection('users').authWithOAuth2({
provider: 'google',
code: 'OAUTH2_CODE_FROM_GOOGLE'
});
// With existing access token
const authData = await pb.collection('users').authWithOAuth2({
provider: 'github',
accessToken: 'USER_ACCESS_TOKEN'
});
```
**Supported Providers:**
- Google
- GitHub
- GitLab
- Discord
- Facebook
- Microsoft
- Spotify
- Twitch
- Discord
- Twitter/X
**Custom OAuth2 Configuration:**
1. Go to Auth Collections → OAuth2 providers
2. Add provider with client ID/secret
3. Configure redirect URL
4. Enable provider
### Magic Link Authentication
```javascript
// Send magic link
await pb.collection('users').requestPasswordReset('user@example.com');
// User clicks link (URL contains token)
// Reset password (returns 204 on success)
await pb.collection('users').confirmPasswordReset(
'RESET_TOKEN',
'newPassword123',
'newPassword123'
);
// After confirming, prompt the user to sign in again with the new password.
```
## Login
### Standard Login
```javascript
// Email and password
const authData = await pb.collection('users').authWithPassword(
'user@example.com',
'password123'
);
// Access token and user data
console.log(authData.token); // JWT token
console.log(authData.user); // User record
// Token is automatically stored
console.log(pb.authStore.token); // Access stored token
```
### OAuth2 Login
```javascript
// Same as registration - creates user if doesn't exist
const authData = await pb.collection('users').authWithOAuth2({
provider: 'google',
code: 'OAUTH2_CODE'
});
```
### Magic Link Login
```javascript
// Request magic link
await pb.collection('users').requestVerification('user@example.com');
// User clicks verification link (returns 204 on success)
await pb.collection('users').confirmVerification('VERIFICATION_TOKEN');
// Verification does not log the user in automatically; call authWithPassword or another auth method.
```
## Auth State Management
### Checking Auth Status
```javascript
// Check if user is authenticated
if (pb.authStore.isValid) {
const user = pb.authStore.model;
console.log('User is logged in:', user.email);
} else {
console.log('User is not logged in');
}
// Get current user
const user = pb.authStore.model;
// Refresh auth state
await pb.collection('users').authRefresh();
```
### Auth Store Persistence
The default auth store persists tokens in `localStorage` when available and falls back to an in-memory store otherwise. Call `pb.authStore.clear()` to invalidate the current session. For custom storage implementations, extend the SDK `BaseAuthStore` as described in the [official JS SDK README](https://github.com/pocketbase/js-sdk#auth-store).
### React Auth Hook
```javascript
import { useEffect, useState } from 'react';
import PocketBase from 'pocketbase';
function useAuth(pb) {
const [user, setUser] = useState(pb.authStore.model);
useEffect(() => {
const unsub = pb.authStore.onChange((token, model) => {
setUser(model);
});
return () => unsub();
}, []);
return { user };
}
// Usage
function App() {
const pb = new PocketBase('http://127.0.0.1:8090');
const { user } = useAuth(pb);
return user ? (
<div>Welcome, {user.email}!</div>
) : (
<div>Please log in</div>
);
}
```
## Logout
```javascript
// Clear auth state
pb.authStore.clear();
// After logout, authStore.model will be null
console.log(pb.authStore.model); // null
```
## Protected Routes (Frontend)
### React Router Protection
```javascript
import { Navigate } from 'react-router-dom';
function ProtectedRoute({ children }) {
const { user } = useAuth(pb);
if (!user) {
return <Navigate to="/login" />;
}
return children;
}
// Usage
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
```
### Vanilla JS Protection
```javascript
// Check before API call
function requireAuth() {
if (!pb.authStore.isValid) {
window.location.href = '/login';
return false;
}
return true;
}
// Usage
if (requireAuth()) {
const posts = await pb.collection('posts').getList(1, 50);
}
```
## User Profile Management
### Update User Data
```javascript
// Update user profile
const updated = await pb.collection('users').update(user.id, {
name: 'Jane Doe',
bio: 'Updated bio',
avatar: fileInput.files[0] // File upload
});
// Only authenticated user can update their own profile
// unless using admin API
```
### Change Password
```javascript
// Change password (requires current password)
const updated = await pb.collection('users').update(user.id, {
oldPassword: 'currentPassword123',
password: 'newPassword123',
passwordConfirm: 'newPassword123'
});
```
### Update Email
```javascript
// Update email (triggers verification if enabled)
const updated = await pb.collection('users').update(user.id, {
email: 'newemail@example.com'
});
```
## Email Verification
### Enable Email Verification
1. Go to Auth Collections → Options
2. Enable "Email verification"
3. Customize verification page
4. Save
### Manual Verification
```javascript
// Request verification email
await pb.collection('users').requestVerification('user@example.com');
// User clicks link with token
const authData = await pb.collection('users').confirmVerification('TOKEN_FROM_URL');
```
### Auto-Verify on OAuth
```javascript
// OAuth users can be auto-verified
// Configure in Auth Collections → OAuth2 providers
// Check "Auto-verification"
```
## Password Reset
### Request Reset
```javascript
// Send password reset email
await pb.collection('users').requestPasswordReset('user@example.com');
```
### Confirm Reset
```javascript
// User clicks link from email
// Reset with token from URL
const authData = await pb.collection('users').confirmPasswordReset(
'TOKEN_FROM_URL',
'newPassword123',
'newPassword123'
);
```
## OAuth2 Configuration
### Google OAuth2 Setup
1. **Google Cloud Console**
- Create new project or select existing
- Enable Google+ API
- Create OAuth 2.0 credentials
- Add authorized redirect URIs:
- `http://localhost:8090/api/oauth2/google/callback`
- `https://yourdomain.com/api/oauth2/google/callback`
2. **PocketBase Admin UI**
- Go to Auth Collections → OAuth2 providers
- Click "Google"
- Enter Client ID and Client Secret
- Save
### GitHub OAuth2 Setup
1. **GitHub Developer Settings**
- New OAuth App
- Authorization callback URL:
- `http://localhost:8090/api/oauth2/github/callback`
- Get Client ID and Secret
2. **PocketBase Admin UI**
- Configure GitHub provider
- Enter credentials
### Custom OAuth2 Provider
```javascript
// Most providers follow similar pattern:
// 1. Redirect to provider auth page
// 2. Provider redirects back with code
// 3. Exchange code for access token
// 4. Use access token with PocketBase
// Example: Discord
window.location.href =
`https://discord.com/api/oauth2/authorize?client_id=CLIENT_ID&redirect_uri=${encodeURIComponent('http://localhost:8090/_/')}&response_type=code&scope=identify%20email`;
```
## JWT Token Details
### Token Structure
JWT tokens consist of three parts:
- **Header** - Algorithm and token type
- **Payload** - User data and claims
- **Signature** - HMAC validation
```javascript
// Payload includes (fields may vary depending on the auth collection):
{
"id": "USER_ID",
"collectionId": "COLLECTION_ID",
"collectionName": "users",
"exp": 1234567890, // expires at
"iat": 1234567890 // issued at
}
```
### Token Expiration
- Default expiration: 7 days
- Can be customized in Auth Collections → Options
- Tokens remain valid until `exp`; call `pb.collection('users').authRefresh()` to refresh.
### Manual Token Validation
```javascript
// Check if token is still valid
if (pb.authStore.isValid) {
// Token is valid
const user = pb.authStore.model;
} else {
// Token expired or invalid
// Redirect to login
}
```
## Security Best Practices
### Password Security
```javascript
// Configure in Auth Collections → Options
{
"minPasswordLength": 8,
"requirePasswordUppercase": true,
"requirePasswordLowercase": true,
"requirePasswordNumbers": true,
"requirePasswordSymbols": true
}
```
### Account Security
1. **Enable Email Verification**
- Prevent fake accounts
- Verify user email ownership
2. **Implement Rate Limiting**
- Prevent brute force attacks
- Configure at reverse proxy level
3. **Use HTTPS in Production**
- Encrypt data in transit
- Required for OAuth2
4. **Set Appropriate Token Expiration**
- Balance security and UX
- Consider refresh tokens
5. **Validate OAuth State**
- Prevent CSRF attacks
- Implement proper state parameter
### Common Auth Rules
**Users can only access their own data:**
```
user_id = @request.auth.id
```
**Verified users only:**
```
@request.auth.verified = true
```
**Admins only:**
```
@request.auth.role = 'admin'
```
**Role-based access:**
```
@request.auth.role = 'moderator' || @request.auth.role = 'admin'
```
## Multi-Tenant Authentication
### Workspace/Team Model
```javascript
// collections:
// - users (auth) - email, password
// - workspaces (base) - name, owner_id
// - workspace_members (base) - workspace_id, user_id, role
// Users can access workspaces they're members of:
List Rule: "id != '' && (@request.auth.id != '')"
View Rule: "members.user_id ?~ @request.auth.id"
// On login, filter workspace by user membership
async function getUserWorkspaces() {
const memberships = await pb.collection('workspace_members').getList(1, 100, {
filter: `user_id = "${pb.authStore.model.id}"`
});
const workspaceIds = memberships.items.map(m => m.workspace_id);
return workspaceIds;
}
```
## Auth API Reference
### User Methods
```javascript
// Auth collection methods
pb.collection('users').create() // Register
pb.collection('users').authWithPassword() // Login
pb.collection('users).authWithOAuth2() // OAuth2
pb.collection('users).authRefresh() // Refresh
pb.collection('users).requestVerification() // Send verification
pb.collection('users).confirmVerification() // Verify
pb.collection('users).requestPasswordReset() // Reset request
pb.collection('users).confirmPasswordReset() // Confirm reset
// Admin methods
pb.collection('users').getOne(id) // Get user
pb.collection('users).update(id, data) // Update user
pb.collection('users).delete(id) // Delete user
pb.collection('users').listAuthMethods() // List allowed auth methods and OAuth providers
```
## Troubleshooting
**Login not working**
- Check email/password correctness
- Verify user exists
- Check if account is verified (if verification required)
- Check auth rules don't block access
**OAuth2 redirect errors**
- Verify redirect URI matches exactly
- Check provider configuration
- Ensure HTTPS in production
- Check CORS settings
**Token expired**
- Use authRefresh() to get new token
- Check token expiration time
- Implement auto-refresh logic
**Password reset not working**
- Check if email exists
- Verify reset link wasn't used
- Check spam folder
- Verify email sending configuration
**Can't access protected data**
- Check auth rules
- Verify user is authenticated
- Check user permissions
- Verify collection rules
## Related Topics
- [Collections](collections.md) - Auth collection details
- [API Rules & Filters](api_rules_filters.md) - Security rules
- [Files Handling](files_handling.md) - File uploads
- [Security Rules](../security_rules.md) - Comprehensive access control
- [Going to Production](going_to_production.md) - Production security

View File

@@ -0,0 +1,514 @@
# PocketBase CLI Commands
## Overview
The PocketBase Command Line Interface (CLI) provides powerful tools for managing your PocketBase instances, including server operations, database migrations, user management, and system administration. The CLI is essential for development workflows, production deployments, and automation tasks.
## Installation & Setup
The PocketBase CLI is built into the main PocketBase executable. After downloading PocketBase, the CLI is available via the `./pocketbase` command.
```bash
# Make sure the executable has proper permissions
chmod +x pocketbase
# Verify CLI is working
./pocketbase --help
```
## Global Flags
These flags can be used with any PocketBase CLI command:
| Flag | Description | Default |
|------|-------------|---------|
| `--automigrate` | Enable/disable auto migrations | `true` |
| `--dev` | Enable dev mode (prints logs and SQL statements to console) | `false` |
| `--dir string` | The PocketBase data directory | `"pb_data"` |
| `--encryptionEnv string` | Env variable with 32-char value for app settings encryption | `none` |
| `--hooksDir string` | Directory with JS app hooks | |
| `--hooksPool int` | Total prewarm goja.Runtime instances for JS hooks execution | `15` |
| `--hooksWatch` | Auto restart on pb_hooks file change (no effect on Windows) | `true` |
| `--indexFallback` | Fallback to index.html on missing static path (for SPA pretty URLs) | `true` |
| `--migrationsDir string` | Directory with user-defined migrations | |
| `--publicDir string` | Directory to serve static files | `"pb_public"` |
| `--queryTimeout int` | Default SELECT queries timeout in seconds | `30` |
| `-h, --help` | Show help information | |
| `-v, --version` | Show version information | |
## Core Commands
### serve
Starts the PocketBase web server. This is the most commonly used command for running your application.
```bash
# Basic usage (default: 127.0.0.1:8090)
./pocketbase serve
# Specify custom host and port
./pocketbase serve --http=0.0.0.0:8090
# Serve with specific domain(s)
./pocketbase serve example.com www.example.com
# Enable HTTPS with automatic HTTP to HTTPS redirect
./pocketbase serve --https=0.0.0.0:443
# Development mode with verbose logging
./pocketbase serve --dev
# Production with custom directories
./pocketbase serve --dir=/data/pocketbase --publicDir=/var/www/html
```
#### Serve-Specific Flags
| Flag | Description | Default |
|------|-------------|---------|
| `--http string` | TCP address for HTTP server | Domain mode: `0.0.0.0:80`<br>No domain: `127.0.0.1:8090` |
| `--https string` | TCP address for HTTPS server | Domain mode: `0.0.0.0:443`<br>No domain: empty (no TLS) |
| `--origins strings` | CORS allowed domain origins list | `[*]` |
#### Common Usage Patterns
```bash
# Development server with all origins allowed
./pocketbase serve --dev --origins=http://localhost:3000,http://localhost:8080
# Production server with specific origins
./pocketbase serve --http=0.0.0.0:8090 --origins=https://app.example.com
# Behind reverse proxy (HTTPS handled by proxy)
./pocketbase serve --http=127.0.0.1:8090
```
### migrate
Manages database schema migrations. Essential for version-controlling your database structure and deploying schema changes.
```bash
# Run all available migrations
./pocketbase migrate up
# Revert the last applied migration
./pocketbase migrate down
# Revert the last 3 migrations
./pocketbase migrate down 3
# Create new blank migration template
./pocketbase migrate create add_user_profile_fields
# Create migration from current collections configuration
./pocketbase migrate collections
# Clean up migration history (remove references to deleted files)
./pocketbase migrate history-sync
```
#### Migration Arguments
| Argument | Description |
|----------|-------------|
| `up` | Runs all available migrations |
| `down [number]` | Reverts the last `[number]` applied migrations (default: 1) |
| `create name` | Creates new blank migration template file |
| `collections` | Creates migration file with snapshot of local collections configuration |
| `history-sync` | Ensures `_migrations` history table doesn't reference deleted migration files |
#### Migration Workflow
```bash
# 1. Make schema changes via Admin UI or API
# 2. Create migration to capture changes
./pocketbase migrate collections
# 3. The new migration file appears in migrations directory
# 4. Commit migration file to version control
# Deploy to production:
./pocketbase migrate up
```
> Need to move historical data between environments? Pair schema migrations with the import/export options documented in [Data Migration Workflows](data_migration.md). Keep the schema in sync first, then run the data tools.
### superuser
Manages administrator (superuser) accounts for accessing the PocketBase admin dashboard.
```bash
# Create new superuser interactively
./pocketbase superuser create
# Create superuser with email and password
./pocketbase superuser create admin@example.com password123
# Update existing superuser password
./pocketbase superuser update admin@example.com newpassword123
# Delete superuser
./pocketbase superuser delete admin@example.com
# Create or update (idempotent)
./pocketbase superuser upsert admin@example.com password123
# Generate one-time password for existing superuser
./pocketbase superuser otp admin@example.com
```
#### Superuser Sub-Commands
| Sub-command | Description | Example |
|-------------|-------------|---------|
| `create` | Creates a new superuser | `superuser create email@domain.com password` |
| `update` | Changes password of existing superuser | `superuser update email@domain.com newpassword` |
| `delete` | Deletes an existing superuser | `superuser delete email@domain.com` |
| `upsert` | Creates or updates if email exists | `superuser upsert email@domain.com password` |
| `otp` | Creates one-time password for superuser | `superuser otp email@domain.com` |
### update
Automatically updates PocketBase to the latest available version.
```bash
# Check for and apply latest update
./pocketbase update
```
## Development Workflow
### Setting Up a New Project
```bash
# 1. Create project directory
mkdir my-pocketbase-app
cd my-pocketbase-app
# 2. Download PocketBase
wget https://github.com/pocketbase/pocketbase/releases/latest/download/pocketbase_0.20.0_linux_amd64.zip
unzip pocketbase_0.20.0_linux_amd64.zip
chmod +x pocketbase
# 3. Create initial superuser
./pocketbase superuser create admin@example.com password123
# 4. Start development server
./pocketbase serve --dev
```
### Daily Development Cycle
```bash
# Start server in development mode
./pocketbase serve --dev
# In another terminal, make schema changes via Admin UI
# Then create migration to capture changes
./pocketbase migrate collections
# Test your application
# When ready, commit migration file to version control
```
### Team Collaboration
```bash
# Pull latest changes from version control
git pull
# Run any new migrations
./pocketbase migrate up
# Start development server
./pocketbase serve --dev
```
## Production Deployment
### Production Server Setup
```bash
# 1. Extract PocketBase to production directory
mkdir -p /opt/pocketbase
cp pocketbase /opt/pocketbase/
cd /opt/pocketbase
# 2. Set up proper permissions
chmod +x pocketbase
mkdir -p pb_data pb_public
# 3. Create superuser if not exists
./pocketbase superuser upsert admin@example.com securepassword123
# 4. Run production server
./pocketbase serve --http=0.0.0.0:8090
```
### Using with Systemd
For service setup and production hardening guidance, see [Going to Production](going_to_production.md).
### Environment-Specific Configurations
```bash
# Development
./pocketbase serve --dev --dir=./dev_data
# Staging
./pocketbase serve --http=0.0.0.0:8090 --dir=./staging_data
# Production
./pocketbase serve --http=0.0.0.0:8090 --dir=/data/pocketbase
```
## Advanced Usage
### Custom Directories
```bash
# Custom data and public directories
./pocketbase serve --dir=/var/lib/pocketbase --publicDir=/var/www/pocketbase
# Custom migrations directory
./pocketbase migrate --migrationsDir=/opt/pocketbase/migrations
```
### Security Configuration
```bash
# Enable encryption for app settings
export PB_ENCRYPTION_KEY="your-32-character-encryption-key"
./pocketbase serve --encryptionEnv=PB_ENCRYPTION_KEY
# Restrict CORS origins in production
./pocketbase serve --origins=https://app.example.com,https://admin.example.com
```
### JavaScript Hooks
```bash
# Enable JavaScript hooks with custom directory
./pocketbase serve --hooksDir=./pb_hooks --hooksWatch
# Configure hook pool size for performance
./pocketbase serve --hooksPool=25
```
### Query Timeout Configuration
```bash
# Set longer query timeout for complex operations
./pocketbase serve --queryTimeout=120
```
## Troubleshooting
### Common Issues
#### Permission Denied
```bash
# Make executable
chmod +x pocketbase
# Check file ownership
ls -la pocketbase
```
#### Port Already in Use
```bash
# Check what's using the port
sudo lsof -i :8090
# Use different port
./pocketbase serve --http=127.0.0.1:8080
```
#### Migration Conflicts
```bash
# Check migration status
./pocketbase migrate history-sync
# Re-run migrations if needed
./pocketbase migrate down
./pocketbase migrate up
```
#### Data Directory Issues
```bash
# Ensure data directory exists and is writable
mkdir -p pb_data
chmod 755 pb_data
# Check directory permissions
ls -la pb_data/
```
### Debug Mode
```bash
# Enable development mode for verbose logging
./pocketbase serve --dev
```
Runtime logs print to stdout; when running under systemd, inspect them with `journalctl -u pocketbase -f`.
### Performance Issues
```bash
# Increase query timeout for slow queries
./pocketbase serve --queryTimeout=60
# Increase hooks pool for better concurrency
./pocketbase serve --hooksPool=50
```
## Best Practices
### Development
1. **Always use `--dev` flag** during development for detailed logging
2. **Create migrations** after making schema changes via Admin UI
3. **Commit migration files** to version control
4. **Use different data directories** for different environments
5. **Test migrations** on staging before production
### Production
1. **Never use `--dev` flag** in production
2. **Set up proper user permissions** for the PocketBase process
3. **Configure reverse proxy** (nginx/Caddy) for HTTPS
4. **Set up proper logging** and monitoring
5. **Regular backups** using the backup API
6. **Restrict CORS origins** to specific domains
7. **Use encryption** for sensitive app settings
### Security
1. **Use strong passwords** for superuser accounts
2. **Restrict origins** in production environments
3. **Enable encryption** for app settings
4. **Run as non-root user** whenever possible
5. **Keep PocketBase updated** using the update command
## CLI Scripting Examples
### Automated Setup Script
```bash
#!/bin/bash
# setup-pocketbase.sh
set -e
# Configuration
ADMIN_EMAIL="admin@example.com"
ADMIN_PASSWORD="securepassword123"
DATA_DIR="./pb_data"
echo "🚀 Setting up PocketBase..."
# Create data directory
mkdir -p "$DATA_DIR"
# Create superuser
./pocketbase superuser upsert "$ADMIN_EMAIL" "$ADMIN_PASSWORD"
# Start server
echo "✅ PocketBase setup complete!"
echo "🌐 Admin UI: http://127.0.0.1:8090/_/"
./pocketbase serve
```
### Migration Script
```bash
#!/bin/bash
# migrate.sh
set -e
echo "🔄 Running PocketBase migrations..."
# Run all pending migrations
./pocketbase migrate up
echo "✅ Migrations complete!"
```
### Production Deployment Script
```bash
#!/bin/bash
# deploy-production.sh
set -e
# Stop existing service
sudo systemctl stop pocketbase
# Backup current data
cp -r /opt/pocketbase/pb_data /opt/pocketbase/pb_data.backup.$(date +%Y%m%d)
# Run migrations
/opt/pocketbase/pocketbase migrate up
# Start service
sudo systemctl start pocketbase
echo "✅ PocketBase deployed successfully!"
```
## Integration with Other Tools
### Docker Integration
```bash
# Build Docker image that includes custom migrations
FROM ghcr.io/pocketbase/pocketbase:latest
COPY ./migrations /pb/migrations
COPY ./pb_hooks /pb/pb_hooks
# Run migrations on startup
CMD ["sh", "-c", "./pocketbase migrate up && ./pocketbase serve --http=0.0.0.0:8090"]
```
### CI/CD Pipeline
```yaml
# .github/workflows/deploy.yml
- name: Deploy PocketBase
run: |
./pocketbase migrate up
./pocketbase superuser upsert ${{ secrets.ADMIN_EMAIL }} ${{ secrets.ADMIN_PASSWORD }}
systemctl restart pocketbase
```
See [Backups API](../api/api_backups.md) for backup automation techniques.
---
## Quick Reference
### Essential Commands
```bash
./pocketbase serve --dev # Development server
./pocketbase migrate up # Run migrations
./pocketbase superuser create email pass # Create admin
./pocketbase update # Update PocketBase
```
### Common Flags
```bash
--dev # Development mode
--http=0.0.0.0:8090 # Custom host/port
--dir=custom_data # Custom data directory
--origins=https://domain.com # CORS restrictions
```
### Production Checklist
- [ ] Remove `--dev` flag
- [ ] Set proper file permissions
- [ ] Configure reverse proxy for HTTPS
- [ ] Restrict CORS origins
- [ ] Set up monitoring and backups
- [ ] Create systemd service
- [ ] Test migration workflow

View File

@@ -0,0 +1,544 @@
# Collections in PocketBase
## Overview
Collections are the fundamental data structures in PocketBase, similar to tables in a relational database. They define the schema and behavior of your data.
## Collection Types
### 1. Base Collection
Flexible collection with custom schema. Used for:
- Posts, articles, products
- Comments, messages
- Any application-specific data
**Characteristics:**
- No built-in authentication
- Custom fields only
- Full CRUD operations
- Can be accessed via REST API
### 2. Auth Collection
Special collection for user accounts. Used for:
- User registration and login
- User profiles and settings
- Authentication workflows
**Characteristics:**
- Built-in auth fields (`email`, `password`, `emailVisibility`, `verified`)
- Automatic user ID tracking on creation
- OAuth2 support
- Password management
- Email verification
- Password reset functionality
### 3. View Collection
Read-only collection based on SQL views. Used for:
- Complex joins and aggregations
- Denormalized data for performance
- Reporting and analytics
- Dashboard metrics
**Characteristics:**
- Read-only (no create, update, delete)
- Defined via SQL query
- Auto-updates when source data changes
- Useful for performance optimization
## Creating Collections
### Via Admin UI
1. Navigate to Collections
2. Click "New Collection"
3. Choose collection type
4. Configure name and schema
5. Save
### Via API
```javascript
const collection = await pb.collections.create({
name: 'products',
type: 'base',
schema: [
{
name: 'name',
type: 'text',
required: true
},
{
name: 'price',
type: 'number',
required: true
}
]
});
```
## Schema Field Types
### Text
Short to medium text strings.
```json
{
"name": "title",
"type": "text",
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
```
**Options:**
- `min` - Minimum character length
- `max` - Maximum character length
- `pattern` - Regex pattern for validation
### Number
Integer or decimal numbers.
```json
{
"name": "price",
"type": "number",
"options": {
"min": null,
"max": null,
"noDecimal": false
}
}
```
**Options:**
- `min` - Minimum value
- `max` - Maximum value
- `noDecimal` - Allow only integers
### Email
Email addresses with validation.
```json
{
"name": "contact_email",
"type": "email"
}
```
### URL
URLs with validation.
```json
{
"name": "website",
"type": "url"
}
```
### Date
Date and time values.
```json
{
"name": "published_date",
"type": "date",
"options": {
"min": "",
"max": ""
}
}
```
### Boolean
True/false values.
```json
{
"name": "is_published",
"type": "bool"
}
```
### JSON
Arbitrary JSON data.
```json
{
"name": "metadata",
"type": "json"
}
```
### Relation
Links to records in other collections.
```json
{
"name": "author",
"type": "relation",
"options": {
"collectionId": "AUTH_COLLECTION_ID",
"cascadeDelete": false,
"maxSelect": 1,
"displayFields": null
}
}
```
**Options:**
- `collectionId` - Target collection ID
- `cascadeDelete` - Delete related records when this is deleted
- `maxSelect` - Maximum number of related records (1 or null for unlimited)
- `displayFields` - Fields to display when showing the relation
### File
File uploads and storage.
```json
{
"name": "avatar",
"type": "file",
"options": {
"maxSelect": 1,
"maxSize": 5242880,
"mimeTypes": ["image/*"],
"thumbs": ["100x100", "300x300"]
}
}
```
**Options:**
- `maxSelect` - Maximum number of files
- `maxSize` - Maximum file size in bytes
- `mimeTypes` - Allowed MIME types (array or ["*"] for all)
- `thumbs` - Auto-generate image thumbnails at specified sizes
### Select
Dropdown with predefined options.
```json
{
"name": "status",
"type": "select",
"options": {
"values": ["draft", "published", "archived"],
"maxSelect": 1
}
}
```
**Options:**
- `values` - Array of allowed values
- `maxSelect` - Maximum selections (1 for single select, null for multi-select)
### Autodate
Automatically populated dates.
```json
{
"name": "created",
"type": "autodate",
"options": {
"onCreate": true,
"onUpdate": false
}
}
```
**Options:**
- `onCreate` - Set on record creation
- `onUpdate` - Update on record modification
### Username
Unique usernames (valid only for auth collections).
```json
{
"name": "username",
"type": "username",
"options": {
"min": 3,
"max": null
}
}
```
## Collection Rules
Rules control who can access, create, update, and delete records.
### Types of Rules
1. **List Rule** - Who can list/view multiple records
2. **View Rule** - Who can view individual records
3. **Create Rule** - Who can create new records
4. **Update Rule** - Who can modify records
5. **Delete Rule** - Who can delete records
### Rule Syntax
**Authenticated Users Only**
```
@request.auth.id != ""
```
**Owner-Based Access**
```
user_id = @request.auth.id
```
**Role-Based Access**
```
@request.auth.role = 'admin'
```
**Conditional Access**
```
status = 'published' || @request.auth.id = author_id
```
**Complex Conditions**
```
@request.auth.role = 'moderator' && @request.auth.verified = true
```
### Special Variables
- `@request.auth` - Current authenticated user
- `@request.auth.id` - User ID
- `@request.auth.email` - User email
- `@request.auth.role` - User role
- `@request.auth.verified` - Email verification status
### Rule Examples
**Public Blog Posts**
```
List Rule: status = 'published'
View Rule: status = 'published'
Create Rule: @request.auth.id != ''
Update Rule: author_id = @request.auth.id
Delete Rule: author_id = @request.auth.id
```
**Private User Data**
```
List Rule: user_id = @request.auth.id
View Rule: user_id = @request.auth.id
Create Rule: @request.auth.id != ''
Update Rule: user_id = @request.auth.id
Delete Rule: user_id = @request.auth.id
```
**Admin-Only Content**
```
List Rule: @request.auth.role = 'admin'
View Rule: @request.auth.role = 'admin'
Create Rule: @request.auth.role = 'admin'
Update Rule: @request.auth.role = 'admin'
Delete Rule: @request.auth.role = 'admin'
```
**Moderated Comments**
```
List Rule: status = 'approved' || author_id = @request.auth.id
View Rule: status = 'approved' || author_id = @request.auth.id
Create Rule: @request.auth.id != ''
Update Rule: author_id = @request.auth.id
Delete Rule: author_id = @request.auth.id || @request.auth.role = 'moderator'
```
## Collection Indexes
Indexes improve query performance on frequently searched or sorted fields.
### Creating Indexes
**Via Admin UI**
1. Go to collection settings
2. Click "Indexes" tab
3. Click "New Index"
4. Select fields to index
5. Save
**Via API**
```javascript
await pb.collections.update('COLLECTION_ID', {
indexes: [
'CREATE INDEX idx_posts_status ON posts(status)',
'CREATE INDEX idx_posts_author ON posts(author_id)',
'CREATE INDEX idx_posts_created ON posts(created)'
]
});
```
### Index Best Practices
1. **Index fields used in filters**
```sql
CREATE INDEX idx_posts_status ON posts(status)
```
2. **Index fields used in sorts**
```sql
CREATE INDEX idx_posts_created ON posts(created)
```
3. **Index foreign keys (relations)**
```sql
CREATE INDEX idx_comments_post ON comments(post_id)
```
4. **Composite indexes for multi-field queries**
```sql
CREATE INDEX idx_posts_status_created ON posts(status, created)
```
5. **Don't over-index** - Each index adds overhead to writes
## Collection Options
### General Options
- **Name** - Collection identifier (used in API endpoints)
- **Type** - base, auth, or view
- **System collection** - Built-in collections (users, _pb_users_auth_)
- **List encryption** - Encrypt data in list views
### API Options
- **API keys** - Manage read/write API keys
- **CRUD endpoints** - Enable/disable specific endpoints
- **File access** - Configure public/private file access
### Auth Collection Options
- **Min password length** - Minimum password requirements
- **Password constraints** - Require uppercase, numbers, symbols
- **Email verification** - Require email confirmation
- **OAuth2 providers** - Configure social login
## Managing Collections
### List Collections
```javascript
const collections = await pb.collections.getList(1, 50);
```
### Get Collection
```javascript
const collection = await pb.collections.getOne('COLLECTION_ID');
```
### Update Collection
```javascript
const updated = await pb.collections.update('COLLECTION_ID', {
name: 'new_name',
schema: [
// updated schema
]
});
```
### Delete Collection
```javascript
await pb.collections.delete('COLLECTION_ID');
```
### Export Collection Schema
```javascript
const collection = await pb.collections.getOne('COLLECTION_ID');
const schemaJSON = JSON.stringify(collection.schema, null, 2);
```
## Best Practices
1. **Plan Schema Carefully**
- Design before implementing
- Consider future needs
- Use appropriate field types
2. **Use Relations Wisely**
- Normalize data appropriately
- Set cascadeDelete when appropriate
- Consider performance impact
3. **Set Rules Early**
- Security from the start
- Test rules thoroughly
- Document rule logic
4. **Index Strategically**
- Profile slow queries
- Index commonly filtered fields
- Avoid over-indexing
5. **Use Auth Collections for Users**
- Built-in auth features
- OAuth2 support
- Password management
6. **Use Views for Complex Queries**
- Improve performance
- Simplify frontend code
- Pre-compute expensive joins
## Common Patterns
### Blog/Post System
```
Collections:
- posts (base) - title, content, author, status, published_date
- categories (base) - name, slug, description
- tags (base) - name, slug
- posts_tags (base) - post_id, tag_id (relation join)
```
### E-commerce
```
Collections:
- products (base) - name, price, description, category, stock
- orders (base) - user, items, total, status
- order_items (base) - order, product, quantity, price
- categories (base) - name, parent (self-relation)
```
### Social Network
```
Collections:
- posts (base) - author, content, media, created, visibility
- likes (base) - post, user (unique constraint)
- follows (base) - follower, following (unique constraint)
- users (auth) - built-in auth + profile fields
```
## Troubleshooting
**Collection not showing data**
- Check listRule
- Verify user permissions
- Check if view collection is properly configured
**Slow queries**
- Add database indexes
- Optimize rule conditions
- Use views for complex joins
**Can't create records**
- Check createRule
- Verify required fields
- Ensure user is authenticated
**File uploads failing**
- Check maxSize and mimeTypes
- Verify file field options
- Check user has create permissions
## Related Topics
- [Authentication](authentication.md) - User management
- [API Rules & Filters](api_rules_filters.md) - Security rules syntax
- [Working with Relations](working_with_relations.md) - Field relationships
- [Files Handling](files_handling.md) - File uploads and storage
- [Schema Templates](../templates/schema_templates.md) - Pre-built schemas

View File

@@ -0,0 +1,184 @@
# Data Migration Workflows
PocketBase does not ship with a one-click import/export pipeline, but the core project maintainers outline several supported patterns in [GitHub discussion #6287](https://github.com/pocketbase/pocketbase/discussions/6287). This guide explains how to choose the right workflow, hardens the existing helper scripts, and points to extension patterns you can adapt for larger migrations.
---
## Decision Guide
| Scenario | Recommended Path | Notes |
| --- | --- | --- |
| Small/medium data sets (< 100k records) and you just need JSON dumps | [Web API scripts](#option-1-web-api-scripts) | Works everywhere; slower but simplest to automate |
| You want transactions, schema automation, or better performance | [Custom CLI commands](#option-2-custom-cli-commands) | Implement in JS `pb_hooks` or native Go extensions |
| You must transform data from another live database | [Mini Go program bridging databases](#option-3-mini-go-bridge) | Connect to PocketBase `pb_data` alongside the legacy DB |
| You already have CSV or SQLite dumps | [External tooling](#option-4-external-import-tools) | sqlite3 `.import`, community tools like `pocketbase-import` |
| You need full control and understand PB internals | [Raw SQLite scripts](#option-5-raw-sqlite-scripts) | Only if you know how PB stores complex field types |
> **Tip:** If you are migrating an application that already works and you do not plan on extending it, consider whether the migration effort is worth it—the PocketBase author recommends staying on the stable stack unless you need PB-specific capabilities.
---
## Pre-flight Checklist
1. **Back up `pb_data/` first.** Use `sqlite3` or the Backups API before experimenting.
2. **Create collections and fields up-front.** Use the Admin UI, migrations (`./pocketbase migrate collections`), or extension code so relations, file fields, and validation rules exist before import.
3. **Map unique keys per collection.** Decide which field(s) you will use for upserts (e.g., `email` on `users`).
4. **Audit data types.** PocketBase stores multi-selects and relation sets as JSON arrays, and file fields expect PocketBase-managed file IDs.
5. **Plan authentication.** Admin endpoints require a superuser token; scripts now prompt for credentials.
6. **Run a dry run.** Use the script `--dry-run` flag or custom command to validate payloads before writing.
---
## Option 1: Web API Scripts
Use the hardened Python helpers in `scripts/` when you need a portable solution without custom builds.
### Export
```bash
python scripts/export_data.py \
http://127.0.0.1:8090 \
pb_export \
--email admin@example.com \
--batch-size 500 \
--format ndjson \
--exclude _pb_users,_migrations
```
- Authenticates as an admin (password prompt if omitted).
- Enumerates collections dynamically; filter with `--collections` or `--exclude`.
- Streams records page-by-page and writes per-collection `.json` or `.ndjson` files plus a `manifest.json` summary.
- Use NDJSON for large exports where you want to stream line-by-line elsewhere.
### Import
```bash
python scripts/import_data.py \
http://127.0.0.1:8090 \
pb_export \
--email admin@example.com \
--upsert users=email --upsert orders=orderNumber \
--concurrency 4 \
--batch-size 200 \
--dry-run
```
- Supports `.json` and `.ndjson` dumps.
- Cleans system fields (`id`, `created`, `updated`, `@expand`).
- Optional per-collection upserts via `--upsert collection=field` (use `*=field` as a fallback).
- Batches and runs limited concurrency to reduce HTTP latency, with optional throttling between batches.
- `--dry-run` validates payloads without writing to the database. When satisfied, re-run without the flag.
- Fails fast if a collection is missing unless `--skip-missing` is set.
This approach is intentionally simple and aligns with the "v1" recommendation from the PocketBase maintainer. Expect higher runtimes for large datasets but minimal setup.
---
## Option 2: Custom CLI Commands
Register commands inside `pb_hooks/` or a Go extension to bypass the REST layer and operate inside a database transaction.
### JS `pb_hooks` example
```js
/// <reference path="../pb_data/types.d.ts" />
const { Command } = require("commander");
$app.rootCmd.addCommand(new Command({
use: "data:import <file> <collection>",
run: (cmd, args) => {
const rows = require(args[0]);
const collection = $app.findCollectionByNameOrId(args[1]);
$app.runInTransaction((tx) => {
for (const row of rows) {
const record = new Record(collection);
record.load(row);
tx.save(record);
}
});
},
}));
$app.rootCmd.addCommand(new Command({
use: "data:export <collection> <file>",
run: (cmd, args) => {
const records = $app.findAllRecords(args[0], cmd.getOptionValue("batch") || 1000);
$os.writeFile(args[1], JSON.stringify(records, null, 2), 0o644);
},
}));
```
- Invoke with `./pocketbase data:import ./users.json users`.
- Wrap heavy operations in `runInTransaction` and consider `saveNoValidate` only after cleaning data.
- Extend with chunks, progress logs, or schema checks per your needs.
See also: [`references/go/go_console_commands.md`](../go/go_console_commands.md) for Go equivalents and CLI wiring tips.
---
## Option 3: Mini Go Bridge
For zero-downtime migrations or complex transformations, create a Go program that embeds PocketBase and connects to your legacy database driver (`database/sql`, `pgx`, etc.).
High-level steps:
1. Import `github.com/pocketbase/pocketbase` as a module and boot the app in headless mode.
2. Connect to the legacy database, stream rows, and normalize data types.
3. Use `app.RunInTransaction` plus `app.FindCollectionByNameOrId` to create records directly.
4. Batch writes to avoid exhausting memory; reuse prepared statements for speed.
Refer to [`references/go/go_database.md`](../go/go_database.md) and [`references/go/go_migrations.md`](../go/go_migrations.md) for transaction helpers and schema management patterns.
---
## Option 4: External Import Tools
- **sqlite3 CLI** (`.import`, `.dump`, `.excel`): usable when the source data already matches the PocketBase schema. Ensure collections/fields exist first.
- **Community tool [`michal-kapala/pocketbase-import`](https://github.com/michal-kapala/pocketbase-import)**: handles CSV and flat JSON, creates text fields dynamically, and wraps operations in a transaction.
- **Custom CSV pipelines**: parse CSV with your preferred language, then leverage the REST scripts or CLI commands above.
Always inspect the generated SQLite tables after import to confirm multi-value fields and relation columns are stored as expected.
---
## Option 5: Raw SQLite Scripts
This path edits `pb_data/data.db` directly. Only attempt it if you fully understand PocketBases internal schema conventions:
1. Snapshot the database before touching it.
2. Insert `_collections` metadata before writing to collection tables so the Admin UI and APIs recognize the data.
3. Convert non-SQLite dumps (PostgreSQL/MySQL) to SQLite-compatible syntax.
4. Manually serialize multiselects, relation lists, and JSON fields.
Treat this as a last resort when other methods are impractical.
---
## Validation & Rollback
1. Compare counts between source and target collections (`records/count` endpoint or SQL).
2. Spot-check a few complex records (relations, files, arrays).
3. Run application-level smoke tests or automation scripts.
4. If issues appear, restore the pre-flight backup and iterate.
5. Document the exact command set you used for future recoveries.
---
## Related References
- [`scripts/export_data.py`](../../scripts/export_data.py) authenticated export script with filters, pagination, and NDJSON support.
- [`scripts/import_data.py`](../../scripts/import_data.py) authenticated import script with upsert, batching, and dry-run.
- [`references/go/go_console_commands.md`](../go/go_console_commands.md) extend PocketBase with custom CLI commands.
- [`references/go/go_routing.md`](../go/go_routing.md) expose admin-only import/export endpoints if you prefer HTTP jobs.
- [`references/api/api_records.md`](../api/api_records.md) record filtering syntax used by the scripts.
- [`references/api/api_backups.md`](../api/api_backups.md) full database backup/restore (different from selective migrations).
---
## Summary Checklist
- [ ] Pick a workflow that matches the data volume and complexity.
- [ ] Prepare schema and unique constraints before importing.
- [ ] Run exports with authentication and pagination.
- [ ] Test imports with `--dry-run`, then run again without it.
- [ ] Validate data counts and integrity, keep a rollback plan handy.

View File

@@ -0,0 +1,768 @@
# File Handling in PocketBase
## Overview
PocketBase provides comprehensive file handling capabilities:
- Single and multi-file uploads
- Automatic image thumbnail generation
- File type restrictions
- Size limits
- Public and private file access
- CDN integration support
- Image resizing and optimization
## File Fields
Add file fields to collections via the Admin UI or API:
```json
{
"name": "avatar",
"type": "file",
"options": {
"maxSelect": 1,
"maxSize": 10485760,
"mimeTypes": ["image/*"],
"thumbs": ["100x100", "300x300"]
}
}
```
### File Field Options
#### maxSelect
Maximum number of files allowed:
- `1` - Single file upload
- `null` or `2+` - Multiple files
```json
"maxSelect": 5 // Allow up to 5 files
```
#### maxSize
Maximum file size in bytes:
```json
"maxSize": 10485760 // 10MB
// Common sizes:
5MB = 5242880
10MB = 10485760
50MB = 52428800
100MB = 104857600
```
#### mimeTypes
Allowed MIME types (array):
```json
// Images only
"mimeTypes": ["image/jpeg", "image/png", "image/gif"]
// Images and videos
"mimeTypes": ["image/*", "video/*"]
// Any file type
"mimeTypes": ["*"]
// Specific types
"mimeTypes": [
"image/jpeg",
"image/png",
"application/pdf",
"text/csv"
]
```
#### thumbs
Auto-generate image thumbnails:
```json
"thumbs": [
"100x100", // Small square
"300x300", // Medium square
"800x600", // Large thumbnail
"1200x800" // Extra large
]
// Formats:
// WIDTHxHEIGHT - exact size, may crop
// WIDTHx - width only, maintain aspect ratio
// xHEIGHT - height only, maintain aspect ratio
```
## Uploading Files
### Single File Upload
```javascript
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);
const user = await pb.collection('users').update('USER_ID', formData);
// Access file URL
const avatarUrl = pb.files.getURL(user, user.avatar);
console.log(avatarUrl);
```
### Multiple File Upload
```javascript
const formData = new FormData();
// Add multiple files
formData.append('images', fileInput.files[0]);
formData.append('images', fileInput.files[1]);
formData.append('images', fileInput.files[2]);
const post = await pb.collection('posts').update('POST_ID', formData);
// Access all files
post.images.forEach(image => {
const url = pb.files.getURL(post, image);
console.log(url);
});
```
### Upload with Metadata
```javascript
const formData = new FormData();
formData.append('document', fileInput.files[0], {
filename: 'custom-name.pdf', // Custom filename
type: 'application/pdf',
lastModified: Date.now()
});
const record = await pb.collection('documents').update('DOC_ID', formData);
```
## File URLs
### Get File URL
```javascript
// Basic URL
const url = pb.files.getURL(record, record.avatar);
// With thumbnail
const thumbnailUrl = pb.files.getURL(
record,
record.avatar,
{ thumb: '300x300' }
);
// Custom options
const url = pb.files.getURL(
record,
record.avatar,
{
thumb: '100x100',
expires: 3600 // URL expires in 1 hour (for private files)
}
);
```
### URL Parameters
**For public files:**
```javascript
// Direct access (public files only)
const url = pb.files.getURL(record, record.avatar);
// Returns: http://localhost:8090/api/files/COLLECTION_ID/RECORD_ID/filename.jpg
```
**For private files:**
```javascript
// Temporary signed URL (1 hour expiry)
const url = pb.files.getURL(record, record.avatar, { expires: 3600 });
// Returns: http://localhost:8090/api/files/COLLECTION_ID/RECORD_ID/filename.jpg?token=SIGNED_TOKEN
```
**Thumbnail URLs:**
```javascript
// Automatic thumbnail
const thumbUrl = pb.files.getURL(record, record.avatar, {
thumb: '300x300'
});
// Returns: thumbnail if available
```
## File Access Control
### Public Files
Default behavior - anyone with URL can access:
```javascript
// File is publicly accessible
const url = pb.files.getURL(record, record.avatar);
// Can be shared and accessed by anyone
```
### Private Files
Restrict access to authenticated users:
**1. Configure in Admin UI**
- Go to Collection → File field options
- Enable "Private files"
- Set file rules (e.g., `user_id = @request.auth.id`)
**2. Use signed URLs**
```javascript
// Generate signed URL (expires)
const signedUrl = pb.files.getURL(record, record.avatar, {
expires: 3600 // Expires in 1 hour
});
// Use signed URL in frontend
<img src={signedUrl} alt="Avatar" />
```
**3. Access files with auth token**
```javascript
// Include auth token in requests
const response = await fetch(signedUrl, {
headers: {
'Authorization': `Bearer ${pb.authStore.token}`
}
});
```
### File Rules
Control who can upload/view/delete files:
```javascript
// Owner can only access their files
File Rule: user_id = @request.auth.id
// Public read, authenticated write
List Rule: true
View Rule: true
Create Rule: @request.auth.id != ""
Update Rule: user_id = @request.auth.id
Delete Rule: user_id = @request.auth.id
// Admins only
Create Rule: @request.auth.role = "admin"
Update Rule: @request.auth.role = "admin"
Delete Rule: @request.auth.role = "admin"
```
## Download Files
### Browser Download
```javascript
// Download via browser
const link = document.createElement('a');
link.href = pb.files.getURL(record, record.document);
link.download = record.document;
link.click();
```
### Programmatic Download
```javascript
// Fetch file as blob
const blob = await pb.files.download(record, record.document);
// Or with fetch
const response = await fetch(pb.files.getURL(record, record.document));
const blob = await response.blob();
// Save file
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = record.document;
a.click();
```
## Deleting Files
### Delete Single File
```javascript
// Remove file from record
const updated = await pb.collection('users').update('USER_ID', {
avatar: null // Remove avatar
});
```
### Delete Multiple Files
```javascript
// Remove specific files from array
const updated = await pb.collection('posts').update('POST_ID', {
images: record.images.filter(img => img !== imageToRemove)
});
```
### Delete File on Record Delete
Files are automatically deleted when record is deleted:
```javascript
await pb.collection('posts').delete('POST_ID');
// All associated files are removed automatically
```
## Image Thumbnails
### Automatic Thumbnails
Define in file field options:
```json
{
"name": "images",
"type": "file",
"options": {
"maxSelect": 10,
"maxSize": 10485760,
"mimeTypes": ["image/*"],
"thumbs": ["100x100", "300x300", "800x600"]
}
}
```
### Access Thumbnails
```javascript
// Get specific thumbnail size
const smallThumb = pb.files.getURL(post, post.images[0], {
thumb: '100x100'
});
const mediumThumb = pb.files.getURL(post, post.images[0], {
thumb: '300x300'
});
// Auto-select best thumbnail
const thumb = pb.files.getURL(post, post.images[0], {
thumb: '300x300' // Returns thumbnail or original if not available
});
```
### Thumbnail Formats
- `WxH` - Crop to exact dimensions
- `Wx` - Width only, maintain aspect ratio
- `xH` - Height only, maintain aspect ratio
- `Wx0` - Width, no height limit
- `0xH` - Height, no width limit
## Frontend Integration
### React Image Component
```javascript
import { useState } from 'react';
function ImageUpload() {
const [file, setFile] = useState(null);
const [uploadedUrl, setUploadedUrl] = useState('');
const handleUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('avatar', file);
const updated = await pb.collection('users').update('USER_ID', formData);
setUploadedUrl(pb.files.getURL(updated, updated.avatar));
};
return (
<div>
<input type="file" onChange={handleUpload} />
{uploadedUrl && <img src={uploadedUrl} alt="Avatar" />}
</div>
);
}
```
### Vue.js File Upload
```javascript
<template>
<div>
<input type="file" @change="handleUpload" />
<img v-if="uploadedUrl" :src="uploadedUrl" alt="Avatar" />
</div>
</template>
<script>
export default {
data() {
return {
uploadedUrl: ''
}
},
methods: {
async handleUpload(e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('avatar', file);
const updated = await pb.collection('users').update('USER_ID', formData);
this.uploadedUrl = pb.files.getURL(updated, updated.avatar);
}
}
}
</script>
```
### Vanilla JavaScript
```html
<input type="file" id="fileInput" />
<img id="preview" />
<script>
const fileInput = document.getElementById('fileInput');
const preview = document.getElementById('preview');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('avatar', file);
const updated = await pb.collection('users').update('USER_ID', formData);
const avatarUrl = pb.files.getURL(updated, updated.avatar);
preview.src = avatarUrl;
});
</script>
```
## File Validation
### Client-Side Validation
```javascript
function validateFile(file) {
const maxSize = 10485760; // 10MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (file.size > maxSize) {
alert('File too large. Max size is 10MB.');
return false;
}
if (!allowedTypes.includes(file.type)) {
alert('Invalid file type. Only images allowed.');
return false;
}
return true;
}
// Usage
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (validateFile(file)) {
// Proceed with upload
}
});
```
### Server-Side Validation
Configure in file field options:
- Max file size
- Allowed MIME types
- File access rules
## CDN Integration
### Using External CDN
```javascript
// PocketBase behind CDN
const pb = new PocketBase('https://cdn.yoursite.com');
// Or proxy files through CDN
const cdnUrl = `https://cdn.yoursite.com${pb.files.getURL(record, record.avatar)}`;
```
### Cloudflare R2 / AWS S3
PocketBase can work with S3-compatible storage:
```javascript
// In production config
export default {
dataDir: '/path/to/data',
// S3 configuration
s3: {
endpoint: 'https://s3.amazonaws.com',
bucket: 'your-bucket',
region: 'us-east-1',
accessKey: 'YOUR_KEY',
secretKey: 'YOUR_SECRET'
}
}
```
## File Storage Locations
### Local Storage
Default - files stored in `pb_data/db/files/`:
```bash
pb_data/
db/
files/
collection_id/
record_id/
filename1.jpg
filename2.png
```
### Cloud Storage
Configure in `pocketbase.js` config:
```javascript
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090', {
files: {
// S3 or S3-compatible
endpoint: 'https://your-s3-endpoint',
bucket: 'your-bucket',
region: 'your-region',
accessKey: 'your-access-key',
secretKey: 'your-secret-key'
}
});
```
## File Metadata
### Access File Information
```javascript
const post = await pb.collection('posts').getOne('POST_ID');
// File objects contain:
{
"@collectionId": "...",
"@collectionName": "...",
"id": "file-id",
"name": "filename.jpg",
"title": "Original filename",
"size": 1048576, // File size in bytes
"type": "image/jpeg", // MIME type
"width": 1920, // Image width (if image)
"height": 1080, // Image height (if image)
"created": "2024-01-01T00:00:00.000Z",
"updated": "2024-01-01T00:00:00.000Z"
}
```
### Custom File Metadata
Store additional file information:
```javascript
// When uploading
const formData = new FormData();
formData.append('document', file);
formData.append('description', 'My document'); // Custom field
const record = await pb.collection('documents').create(formData);
// Access later
console.log(record.description);
```
## Progress Tracking
### Upload with Progress
```javascript
function uploadWithProgress(file, onProgress) {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
xhr.addEventListener('load', async () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
// Handle success
}
});
const formData = new FormData();
formData.append('avatar', file);
xhr.open('PATCH', `${pb.baseUrl}/api/collections/users/records/USER_ID`);
xhr.send(formData);
}
// Usage
uploadWithProgress(file, (progress) => {
console.log(`Upload progress: ${progress}%`);
});
```
## Security Best Practices
### 1. Set File Size Limits
```json
"maxSize": 10485760 // 10MB
```
### 2. Restrict MIME Types
```json
"mimeTypes": ["image/jpeg", "image/png"] // Specific types only
```
### 3. Use Private Files for Sensitive Data
- Enable "Private files" option
- Use signed URLs with expiration
- Implement proper file rules
### 4. Validate File Content
```javascript
// Check file type
if (!file.type.startsWith('image/')) {
throw new Error('Only images allowed');
}
// Check file extension
const validExtensions = ['.jpg', '.jpeg', '.png', '.gif'];
if (!validExtensions.some(ext => file.name.toLowerCase().endsWith(ext))) {
throw new Error('Invalid file extension');
}
```
### 5. Sanitize Filenames
```javascript
// Remove special characters
const sanitizedName = file.name.replace(/[^a-zA-Z0-9.]/g, '_');
// Generate unique filename
const uniqueName = `${Date.now()}_${sanitizedName}`;
```
### 6. Implement File Rules
```javascript
// Only owners can upload
File Rule: user_id = @request.auth.id
// Public read, authenticated write
File Rule: @request.auth.id != ""
```
### 7. Monitor File Usage
- Track storage usage
- Monitor for abuse
- Set up alerts for unusual activity
## Common Use Cases
### User Avatars
```json
{
"name": "avatar",
"type": "file",
"options": {
"maxSelect": 1,
"maxSize": 5242880,
"mimeTypes": ["image/*"],
"thumbs": ["100x100", "300x300"]
}
}
```
### Document Storage
```json
{
"name": "documents",
"type": "file",
"options": {
"maxSelect": 10,
"maxSize": 52428800,
"mimeTypes": ["application/pdf", "text/*", "application/msword"]
}
}
```
### Product Images
```json
{
"name": "images",
"type": "file",
"options": {
"maxSelect": 10,
"maxSize": 10485760,
"mimeTypes": ["image/*"],
"thumbs": ["300x300", "800x800"]
}
}
```
### Media Gallery
```json
{
"name": "media",
"type": "file",
"options": {
"maxSelect": 50,
"maxSize": 104857600,
"mimeTypes": ["image/*", "video/*"]
}
}
```
## Troubleshooting
**Upload fails with 413 (Payload Too Large)**
- File exceeds maxSize limit
- Increase maxSize in field options
- Or split large file into smaller chunks
**File type rejected**
- Check mimeTypes in field options
- Verify actual file type (not just extension)
- Update allowed types
**Private file returns 403**
- Ensure user is authenticated
- Use signed URL with expiration
- Check file rules allow access
**Thumbnail not generating**
- Verify file is an image
- Check thumbs array in field options
- Ensure PocketBase has GD/ImageMagick extension
**Slow file uploads**
- Check network connection
- Reduce file size
- Use CDN for large files
- Enable compression
## Related Topics
- [Collections](collections.md) - File field configuration
- [Authentication](authentication.md) - User file access
- [API Files](../api_files.md) - File API endpoints
- [Security Rules](../security_rules.md) - File access control
- [Going to Production](going_to_production.md) - Production file storage

View File

@@ -0,0 +1,246 @@
# Getting Started with PocketBase
## Overview
PocketBase is an open-source backend consisting of:
- **SQLite database** with real-time subscriptions
- **Built-in Admin Dashboard UI** (single-page application)
- **Authentication** (email/password, OAuth2, magic link)
- **File storage** with automatic image resizing
- **RESTful APIs** with CORS support
- **WebSocket** for real-time updates
- **Admin dashboard** for data management
## Quick Setup
### Option 1: Download Binary
```bash
# Download latest release
wget https://github.com/pocketbase/pocketbase/releases/latest/download/pocketbase_0.20.0_linux_amd64.zip
# Unzip
unzip pocketbase_0.20.0_linux_amd64.zip
# Serve on port 8090
./pocketbase serve --http=0.0.0.0:8090
```
Visit http://127.0.0.1:8090/_/ to access the admin dashboard.
💡 **Want to master the PocketBase CLI?** See the comprehensive [CLI Commands Guide](cli_commands.md) for detailed information on `serve`, `migrate`, `superuser`, and all CLI commands.
### Option 2: Docker
```bash
docker run -d \
-v pb_data:/pb_data \
-p 8090:8090 \
--name pocketbase \
ghcr.io/pocketbase/pocketbase:latest serve --http=0.0.0.0:8090
```
### Option 3: Docker Compose
Create `docker-compose.yml`:
```yaml
version: '3.8'
services:
pocketbase:
image: ghcr.io/pocketbase/pocketbase:latest
command: serve --http=0.0.0.0:8090
volumes:
- ./pb_data:/pb_data
ports:
- "8090:8090"
```
Run:
```bash
docker-compose up -d
```
## First Steps in Admin Dashboard
1. **Create Admin Account**
- Navigate to http://localhost:8090/_/
- Enter email and password
- Click "Create and Login"
2. **Configure Settings**
- Go to Settings → CORS
- Add your frontend domain (e.g., `http://localhost:3000`)
- Click "Save"
3. **Create Your First Collection**
- Go to Collections → New Collection
- Choose between:
- **Base collection** - flexible schema
- **Auth collection** - for user management
- **View collection** - read-only computed data
## Basic Concepts
### Collections
Collections are like tables in a traditional database. Each collection has:
- **Schema** - fields and their types
- **Rules** - access control (read, write, delete)
- **Indexes** - performance optimization
- **Options** - additional settings
### Records
Records are individual entries in a collection, similar to rows in a table. Each record:
- Has a unique `id`
- Contains data based on collection schema
- Has built-in fields: `id`, `created`, `updated`
### Authentication
User accounts can be created through:
- Email/Password registration
- OAuth2 providers (Google, GitHub, etc.)
- Magic link authentication
### Files
File fields allow:
- Single or multiple file uploads
- Automatic thumbnail generation
- MIME type restrictions
- Size limits
## Frontend Integration
### JavaScript SDK
```html
<script src="https://cdn.jsdelivr.net/npm/pocketbase@latest/dist/pocketbase.umd.js"></script>
<script>
const pb = new PocketBase('http://127.0.0.1:8090');
// Example: Register user
const authData = await pb.collection('users').create({
email: 'test@example.com',
password: 'password123',
passwordConfirm: 'password123'
});
// Example: Login
const authData = await pb.collection('users').authWithPassword(
'test@example.com',
'password123'
);
// Example: Create record
const record = await pb.collection('posts').create({
title: 'My First Post',
content: 'Hello world!'
});
</script>
```
### React Integration
```bash
npm install pocketbase
```
```javascript
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
// React hook for auth state
function useAuth() {
const [user, setUser] = React.useState(pb.authStore.model);
React.useEffect(() => {
const unsub = pb.authStore.onChange(() => {
setUser(pb.authStore.model);
});
return () => unsub();
}, []);
return { user };
}
// React component
function Posts() {
const [posts, setPosts] = React.useState([]);
React.useEffect(() => {
loadPosts();
// Subscribe to real-time updates
pb.collection('posts').subscribe('*', () => {
loadPosts();
});
return () => pb.collection('posts').unsubscribe();
}, []);
async function loadPosts() {
const records = await pb.collection('posts').getList(1, 50);
setPosts(records.items);
}
return posts.map(post => <div key={post.id}>{post.title}</div>);
}
```
## Next Steps
- **Schema Design** - Define your data structure (see `collections.md`)
- **Authentication** - Set up user management (see `authentication.md`)
- **Security Rules** - Control data access (see `security_rules.md`)
- **API Integration** - Build your frontend (see `api_records.md`)
- **Production Setup** - Deploy to production (see `going_to_production.md`)
## Common First Tasks
### Task: Create a Blog
1. Create `posts` collection (auth collection)
2. Add fields: `title`, `content`, `published` (bool)
3. Set rules: public read, author write
4. Create first post via Admin UI or API
### Task: User Profiles
1. Users collection already exists (auth collection)
2. Add profile fields: `name`, `bio`, `avatar`
3. Set rules: user can update own profile
4. Build profile page in frontend
### Task: Comments System
1. Create `comments` collection (base collection)
2. Add fields: `post`, `author`, `content`
3. Create relation to posts collection
4. Set rules: public read, authenticated write
## Troubleshooting
**Can't access admin dashboard**
- Check if PocketBase is running
- Verify port 8090 is not blocked
- Try http://127.0.0.1:8090/_/ instead of localhost
**CORS errors in frontend**
- Go to Settings → CORS
- Add your frontend domain
- Save changes
**Can't create records**
- Check collection rules
- Verify user is authenticated
- Check required fields are provided
**File uploads failing**
- Check file size limits
- Verify MIME types allowed
- Ensure user has create permissions
## Resources
- [Official Docs](https://pocketbase.io/docs/)
- [Examples](https://github.com/pocketbase/examples)
- [Discord Community](https://discord.gg/G5Vd6UF)
- [GitHub Repository](https://github.com/pocketbase/pocketbase)

View File

@@ -0,0 +1,733 @@
# Going to Production with PocketBase
## Overview
This guide covers production deployment, optimization, security, and maintenance for PocketBase applications.
## Deployment Options
### 1. Docker Deployment
#### Production Docker Compose
Create `docker-compose.prod.yml`:
```yaml
version: '3.8'
services:
pocketbase:
image: ghcr.io/pocketbase/pocketbase:latest
command: serve --https=0.0.0.0:443 --http=0.0.0.0:80
volumes:
- ./pb_data:/pb_data
environment:
- PB_PUBLIC_DIR=/pb_public
ports:
- "80:80"
- "443:443"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
caddy:
image: caddy:2-alpine
depends_on:
- pocketbase
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./pb_data:/pb_data:ro
ports:
- "80:80"
- "443:443"
restart: unless-stopped
```
#### Caddyfile Configuration
Create `Caddyfile`:
```caddy
yourdomain.com {
encode gzip
reverse_proxy pocketbase:8090
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
}
}
```
### 2. Reverse Proxy (Nginx)
#### Nginx Configuration
```nginx
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### 3. Cloud Platform Deployment
#### Railway
```yaml
# railway.toml
[build]
builder = "NIXPACKS"
[deploy]
startCommand = "./pocketbase serve --https=0.0.0.0:$PORT"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
```
#### Fly.io
```toml
# fly.toml
app = "your-app-name"
[build]
builder = "paketobuildpacks/builder:base"
[[services]]
internal_port = 8090
protocol = "tcp"
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.ports]]
handlers = ["http"]
port = 80
[env]
PB_PUBLIC_DIR = "/app/public"
```
#### DigitalOcean App Platform
```yaml
name: pocketbase-app
services:
- name: pocketbase
source_dir: /
github:
repo: your-username/pocketbase-repo
branch: main
run_command: ./pocketbase serve --https=0.0.0.0:$PORT
environment_slug: ubuntu-js
instance_count: 1
instance_size_slug: basic-xxs
envs:
- key: PB_PUBLIC_DIR
value: /app/public
http_port: 8090
```
## Environment Configuration
### Environment Variables
```bash
# .env
PB_DATA_DIR=/pb_data
PB_PUBLIC_DIR=/pb_public
# Optional: Database encryption
PB_ENCRYPTION_KEY=your-32-character-encryption-key
# Optional: Email configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-app-password
# Optional: CORS
PB_CORS_ORIGINS=https://yourdomain.com,https://admin.yourdomain.com
```
### Custom PocketBase Configuration
Create `pocketbase.js`:
```javascript
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
// Custom configuration
pb.baseOptions = {
files: {
// S3 configuration
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET,
region: process.env.S3_REGION,
accessKey: process.env.S3_ACCESS_KEY,
secretKey: process.env.S3_SECRET_KEY,
},
// Custom auth settings
auth: {
tokenExpDays: 7,
},
};
export default pb;
```
## Security Hardening
### 1. Enable HTTPS
Always use HTTPS in production:
```bash
# Using Let's Encrypt with Certbot
certbot --nginx -d yourdomain.com -d api.yourdomain.com
```
### 2. Security Headers
Configure in reverse proxy (see Nginx/Caddy configuration above):
```
Strict-Transport-Security
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Content-Security-Policy
Referrer-Policy
Permissions-Policy
```
### 3. File Upload Security
Configure file restrictions:
```json
{
"maxSize": 10485760, // 10MB
"mimeTypes": [
"image/jpeg",
"image/png",
"image/gif"
],
"privateFiles": true // Enable for sensitive files
}
```
### 4. Database Encryption
Enable field-level encryption for sensitive data:
```javascript
// Enable encryption in PocketBase config
export default {
dataDir: '/pb_data',
encryptionEnv: 'PB_ENCRYPTION_KEY',
}
```
### 5. Rate Limiting
Implement at reverse proxy level:
```nginx
# Nginx rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:8090;
}
```
```caddy
# Caddy rate limiting
{
限流 yourdomain.com 100 # 100 requests per second
}
```
### 6. Admin Access Restrictions
Restrict admin UI access:
```nginx
# Allow only specific IP
location /_/ {
allow 192.168.1.0/24;
deny all;
proxy_pass http://127.0.0.1:8090;
}
```
## Performance Optimization
### 1. Database Indexing
Add indexes for frequently queried fields:
```sql
-- Users table
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created ON users(created);
-- Posts table
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_created ON posts(created);
-- Comments table
CREATE INDEX idx_comments_post ON comments(post_id);
CREATE INDEX idx_comments_created ON comments(created);
```
### 2. Query Optimization
```javascript
// Select only needed fields
const posts = await pb.collection('posts').getList(1, 50, {
fields: 'id,title,author,created,-content' // Exclude large fields
});
// Use filters instead of fetching all
const recentPosts = await pb.collection('posts').getList(1, 50, {
filter: 'created >= "2024-01-01"',
sort: '-created'
});
// Paginate properly
const page1 = await pb.collection('posts').getList(1, 50);
const page2 = await pb.collection('posts').getList(2, 50);
```
### 3. Caching
Implement caching for frequently accessed data:
```javascript
// Client-side caching
const cache = new Map();
async function getCachedPost(id) {
if (cache.has(id)) {
return cache.get(id);
}
const post = await pb.collection('posts').getOne(id);
cache.set(id, post);
return post;
}
// Clear cache on updates
pb.collection('posts').subscribe('*', (e) => {
cache.delete(e.record.id);
});
```
### 4. CDN for Static Assets
Use CDN for file storage:
```javascript
// Configure CDN
const CDN_URL = 'https://cdn.yourdomain.com';
const fileUrl = `${CDN_URL}${pb.files.getURL(record, record.file)}`;
```
### 5. Connection Pooling
Configure in proxy:
```nginx
upstream pocketbase {
server 127.0.0.1:8090;
keepalive 32;
}
location / {
proxy_pass http://pocketbase;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
```
## Monitoring and Logging
### 1. Health Check Endpoint
```bash
# Check health
curl https://yourdomain.com/api/health
# Returns: {"code":200,"data":{"status":"ok","metrics":{"clients":0}}}
```
### 2. Application Logs
```bash
# View logs
docker logs -f pocketbase
# Or redirect to file
docker logs -f pocketbase > /var/log/pocketbase.log
```
### 3. Monitoring Setup
#### Prometheus Metrics
```javascript
// Custom metrics endpoint
app.OnServe().Add("GET", "/metrics", func(e *core.ServeEvent) error {
// Return Prometheus metrics
return e.Next()
})
```
#### Log Aggregation
Configure log shipping:
```yaml
# Filebeat configuration
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/pocketbase.log
fields:
service: pocketbase
fields_under_root: true
output.elasticsearch:
hosts: ["elasticsearch:9200"]
```
### 4. Error Tracking
Integrate with Sentry:
```javascript
// JavaScript SDK
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "YOUR_SENTRY_DSN",
environment: "production"
});
// Capture errors
try {
await pb.collection('posts').getList(1, 50);
} catch (error) {
Sentry.captureException(error);
throw error;
}
```
## Backup Strategy
### 1. Automated Backups
```bash
#!/bin/bash
# backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/pocketbase"
# Create backup
mkdir -p $BACKUP_DIR
cp -r /pb_data $BACKUP_DIR/pb_data_$DATE
# Upload to cloud storage
aws s3 sync $BACKUP_DIR s3://your-backup-bucket/pocketbase/
# Keep only last 7 days
find $BACKUP_DIR -type d -mtime +7 -exec rm -rf {} +
```
Add to crontab:
```bash
# Daily backup at 2 AM
0 2 * * * /path/to/backup.sh
```
### 2. Point-in-Time Recovery
```bash
# Restore from backup
cd /path/to/new/pocketbase
cp -r /backups/pocketbase/pb_data_YYYYMMDD_HHMMSS/* ./pb_data/
./pocketbase migrate up
```
### 3. Cross-Region Replication
```yaml
# Docker Compose with backup service
services:
pocketbase:
image: ghcr.io/pocketbase/pocketbase:latest
volumes:
- ./pb_data:/pb_data
backup:
image: alpine:latest
volumes:
- ./pb_data:/data
- ./backups:/backups
command: |
sh -c '
while true; do
tar czf /backups/pb_$$(date +%Y%m%d_%H%M%S).tar.gz -C /data .
sleep 3600
done
'
```
## Scaling Considerations
### 1. Vertical Scaling
Increase server resources:
```yaml
# Docker Compose
services:
pocketbase:
deploy:
resources:
limits:
memory: 2G
cpus: '2'
reservations:
memory: 1G
cpus: '1'
```
### 2. Horizontal Scaling (Read Replicas)
For read-heavy workloads:
```nginx
# Nginx upstream
upstream pocketbase_read {
server primary:8090;
server replica1:8090;
server replica2:8090;
}
location /api/records {
proxy_pass http://pocketbase_read;
}
```
### 3. Database Scaling
Consider database sharding for very large datasets:
```sql
-- Partition large tables
CREATE TABLE posts_2024 PARTITION OF posts
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
```
## Common Production Issues
### Issue 1: Out of Memory
```bash
# Monitor memory usage
docker stats pocketbase
# Increase memory limit
docker run --memory=2g pocketbase
```
### Issue 2: Disk Space Full
```bash
# Check disk usage
df -h
# Clean old logs
journalctl --vacuum-time=7d
# Rotate logs
logrotate -f /etc/logrotate.conf
```
### Issue 3: Slow Queries
```sql
-- Analyze slow queries
EXPLAIN QUERY PLAN SELECT * FROM posts WHERE status = 'published';
-- Add missing indexes
CREATE INDEX idx_posts_status ON posts(status);
```
### Issue 4: SSL Certificate Issues
```bash
# Renew Let's Encrypt certificate
certbot renew --nginx
# Check certificate expiration
openssl x509 -in /path/to/cert.pem -text -noout | grep "Not After"
```
### Issue 5: CORS Errors
Update CORS settings in Admin UI:
- Go to Settings → CORS
- Add production domains
- Save changes
## Maintenance
### Regular Tasks
1. **Weekly**
- Review application logs
- Check disk usage
- Verify backup integrity
- Monitor performance metrics
2. **Monthly**
- Update PocketBase to latest version
- Security audit of collections and rules
- Review and optimize slow queries
- Test disaster recovery procedures
3. **Quarterly**
- Security penetration testing
- Performance optimization review
- Infrastructure cost review
- Update documentation
### Update Procedure
```bash
# 1. Create backup
./backup.sh
# 2. Update PocketBase
docker pull ghcr.io/pocketbase/pocketbase:latest
# 3. Stop current instance
docker-compose down
# 4. Start with new image
docker-compose up -d
# 5. Verify functionality
curl https://yourdomain.com/api/health
# 6. Check logs
docker logs -f pocketbase
```
## CI/CD Pipeline
### GitHub Actions Example
```yaml
name: Deploy PocketBase
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Deploy to production
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
script: |
cd /path/to/pocketbase
docker-compose pull
docker-compose up -d
./backup.sh
```
## Best Practices Checklist
- [ ] HTTPS enabled with valid certificate
- [ ] Security headers configured
- [ ] File upload restrictions in place
- [ ] Database encryption enabled for sensitive data
- [ ] Rate limiting configured
- [ ] Admin UI access restricted
- [ ] Database indexes added for performance
- [ ] Automated backups scheduled
- [ ] Monitoring and alerting set up
- [ ] Logs aggregated and monitored
- [ ] Environment variables configured
- [ ] CORS settings updated for production
- [ ] SSL certificate auto-renewal configured
- [ ] Disaster recovery procedure documented
- [ ] Performance benchmarks established
## Related Topics
- [Getting Started](getting_started.md) - Initial setup
- [Authentication](authentication.md) - Security best practices
- [Files Handling](files_handling.md) - File storage security
- [Security Rules](../security_rules.md) - Access control
- [API Rules & Filters](api_rules_filters.md) - Query optimization

View File

@@ -0,0 +1,802 @@
# Working with Relations in PocketBase
## Overview
Relations create links between collections, allowing you to:
- Link records across collections
- Create one-to-one relationships
- Create one-to-many relationships
- Create many-to-many relationships
- Maintain data integrity
- Build complex data models
## Relation Field Types
### 1. One-to-One (Single Relation)
Each record relates to exactly one record in another collection.
**Example:** User → Profile
- Each user has one profile
- Each profile belongs to one user
```json
{
"name": "profile",
"type": "relation",
"options": {
"collectionId": "PROFILE_COLLECTION_ID",
"maxSelect": 1,
"cascadeDelete": true
}
}
```
### 2. One-to-Many (Single Record, Multiple Related)
One record relates to many records in another collection.
**Example:** Post → Comments
- One post has many comments
- Each comment belongs to one post
**On Post Collection:**
```json
{
"name": "comments",
"type": "relation",
"options": {
"collectionId": "COMMENTS_COLLECTION_ID",
"maxSelect": null,
"cascadeDelete": true
}
}
```
**On Comments Collection:**
```json
{
"name": "post",
"type": "relation",
"options": {
"collectionId": "POSTS_COLLECTION_ID",
"maxSelect": 1,
"cascadeDelete": true
}
}
```
### 3. Many-to-Many (Junction Table)
Multiple records relate to multiple records in another collection.
**Example:** Posts ↔ Tags
- One post has many tags
- One tag belongs to many posts
**Junction Collection (posts_tags):**
```json
{
"name": "post",
"type": "relation",
"options": {
"collectionId": "POSTS_COLLECTION_ID",
"maxSelect": 1
}
}
```
```json
{
"name": "tag",
"type": "relation",
"options": {
"collectionId": "TAGS_COLLECTION_ID",
"maxSelect": 1
}
}
```
## Relation Field Options
### collectionId
Target collection ID:
```json
"collectionId": "abcd1234abcd1234abcd1234"
```
### maxSelect
Maximum number of related records:
- `1` - Single relation
- `null` or `2+` - Multiple relations
```json
"maxSelect": 1 // One-to-one
"maxSelect": null // One-to-many
"maxSelect": 5 // Limited multiple
```
### cascadeDelete
Delete related records when this record is deleted:
```json
"cascadeDelete": true // Delete comments when post deleted
"cascadeDelete": false // Keep comments when post deleted
```
### displayFields
Fields to show when displaying relation:
```json
"displayFields": ["name", "email"]
```
## Creating Relations
### One-to-Many Example
**Collections:**
1. `posts` collection
2. `comments` collection
**Posts Schema:**
```json
[
{
"name": "title",
"type": "text",
"required": true
},
{
"name": "content",
"type": "text"
}
]
```
**Comments Schema:**
```json
[
{
"name": "post",
"type": "relation",
"options": {
"collectionId": "POSTS_COLLECTION_ID",
"maxSelect": 1
}
},
{
"name": "author",
"type": "relation",
"options": {
"collectionId": "USERS_COLLECTION_ID",
"maxSelect": 1
}
},
{
"name": "content",
"type": "text",
"required": true
}
]
```
### Create Related Records
```javascript
// Create post
const post = await pb.collection('posts').create({
title: 'My First Post',
content: 'Hello world!'
});
// Create comment with relation
const comment = await pb.collection('comments').create({
post: post.id, // Link to post
author: pb.authStore.model.id, // Link to current user
content: 'Great post!'
});
```
## Querying Relations
### Get Record with Related Data
```javascript
// Get post with comments expanded
const post = await pb.collection('posts').getOne(postId, {
expand: 'comments'
});
console.log(post.title);
post.expand.comments.forEach(comment => {
console.log(comment.content);
});
```
### Filter by Related Field
```javascript
// Get all comments for a specific post
const comments = await pb.collection('comments').getList(1, 50, {
filter: 'post = "' + postId + '"'
});
// Or use expand
const post = await pb.collection('posts').getOne(postId, {
expand: 'comments'
});
const comments = post.expand.comments;
```
### Filter by Nested Relation
```javascript
// Get posts where author email is specific value
const posts = await pb.collection('posts').getList(1, 50, {
filter: 'expand.author.email = "user@example.com"'
});
```
### Filter by Relation's Related Field
```javascript
// Get comments on posts by specific author
const comments = await pb.collection('comments').getList(1, 50, {
filter: 'expand.post.author.email = "user@example.com"'
});
```
## Updating Relations
### Update One-to-One Relation
```javascript
// Create profile for user
const profile = await pb.collection('profiles').create({
bio: 'My bio',
user: userId // Link to user
});
```
### Update One-to-Many Relation
```javascript
// Add comment to post
const comment = await pb.collection('comments').create({
post: postId,
content: 'New comment'
});
// Comments are automatically added to post's comments array
```
### Update Many-to-Many Relations
```javascript
// Create post
const post = await pb.collection('posts').create({
title: 'My Post',
content: 'Content'
});
// Create junction record for tag
await pb.collection('posts_tags').create({
post: post.id,
tag: tagId
});
// Get all tags for post
const tags = await pb.collection('posts_tags').getList(1, 100, {
filter: 'post = "' + post.id + '"',
expand: 'tag'
});
const tagNames = tags.items.map(item => item.expand.tag.name);
```
## Expanding Relations
### Basic Expand
```javascript
// Expand single level
const post = await pb.collection('posts').getOne(postId, {
expand: 'comments'
});
// Expand multiple relations
const post = await pb.collection('posts').getOne(postId, {
expand: 'comments,author'
});
```
### Nested Expand
```javascript
// Expand two levels deep
const comments = await pb.collection('comments').getList(1, 50, {
expand: 'post.author'
});
comments.items.forEach(comment => {
console.log(comment.content); // Comment content
console.log(comment.expand.post.title); // Post title
console.log(comment.expand.post.expand.author); // Author object
});
```
### Selective Field Expansion
```javascript
// Expand and select specific fields
const posts = await pb.collection('posts').getList(1, 50, {
expand: 'author',
fields: 'id,title,expand.author.name,expand.author.email'
});
```
## Deleting Relations
### Delete with Cascade
```javascript
// If cascadeDelete is true, deleting post deletes comments
await pb.collection('posts').delete(postId);
// All comments with post = this postId are deleted
```
### Delete Without Cascade
```javascript
// If cascadeDelete is false, delete comment manually
await pb.collection('comments').delete(commentId);
// Post remains
```
### Update to Remove Relation
```javascript
// Remove relation by setting to null (for optional relations)
const updated = await pb.collection('comments').update(commentId, {
post: null
});
```
## Many-to-Many Pattern
### Approach 1: Junction Collection
**Collections:**
- `posts`
- `tags`
- `posts_tags` (junction)
**posts_tags Schema:**
```json
[
{
"name": "post",
"type": "relation",
"options": {
"collectionId": "POSTS_COLLECTION_ID",
"maxSelect": 1
}
},
{
"name": "tag",
"type": "relation",
"options": {
"collectionId": "TAGS_COLLECTION_ID",
"maxSelect": 1
}
}
]
```
**Operations:**
```javascript
// Add tag to post
await pb.collection('posts_tags').create({
post: postId,
tag: tagId
});
// Get all tags for post
const postTags = await pb.collection('posts_tags').getList(1, 100, {
filter: 'post = "' + postId + '"',
expand: 'tag'
});
const tags = postTags.items.map(item => item.expand.tag);
// Remove tag from post
await pb.collection('posts_tags').delete(junctionRecordId);
```
### Approach 2: Array Field (Advanced)
**Posts Collection:**
```json
{
"name": "tags",
"type": "json" // Store tag IDs in JSON array
}
```
**Operations:**
```javascript
// Add tag
const post = await pb.collection('posts').getOne(postId);
const tags = post.tags || [];
tags.push(tagId);
await pb.collection('posts').update(postId, {
tags: tags
});
// Filter by tag
const posts = await pb.collection('posts').getList(1, 50, {
filter: 'tags ?~ "' + tagId + '"'
});
```
## Common Patterns
### User Posts Pattern
**Collections:**
- `users` (auth)
- `posts` (base)
**Posts Schema:**
```json
[
{
"name": "author",
"type": "relation",
"options": {
"collectionId": "USERS_COLLECTION_ID",
"maxSelect": 1
}
},
{
"name": "title",
"type": "text"
},
{
"name": "content",
"type": "text"
}
]
```
**Operations:**
```javascript
// Create post as current user
const post = await pb.collection('posts').create({
author: pb.authStore.model.id,
title: 'My Post',
content: 'Content'
});
// Get my posts
const myPosts = await pb.collection('posts').getList(1, 50, {
filter: 'author = "' + pb.authStore.model.id + '"'
});
// Get post with author info
const post = await pb.collection('posts').getOne(postId, {
expand: 'author'
});
console.log(post.expand.author.email);
```
### E-commerce Order Pattern
**Collections:**
- `users` (auth)
- `products` (base)
- `orders` (base)
- `order_items` (base)
**Order Items Schema:**
```json
[
{
"name": "order",
"type": "relation",
"options": {
"collectionId": "ORDERS_COLLECTION_ID",
"maxSelect": 1
}
},
{
"name": "product",
"type": "relation",
"options": {
"collectionId": "PRODUCTS_COLLECTION_ID",
"maxSelect": 1
}
},
{
"name": "quantity",
"type": "number"
},
{
"name": "price",
"type": "number"
}
]
```
**Operations:**
```javascript
// Create order
const order = await pb.collection('orders').create({
user: userId,
status: 'pending',
total: 0
});
// Add items to order
let total = 0;
for (const item of cart) {
await pb.collection('order_items').create({
order: order.id,
product: item.productId,
quantity: item.quantity,
price: item.price
});
total += item.quantity * item.price;
}
// Update order total
await pb.collection('orders').update(order.id, {
total: total
});
// Get order with items
const orderWithItems = await pb.collection('orders').getOne(orderId, {
expand: 'items,items.product,user'
});
```
### Social Media Follow Pattern
**Collections:**
- `users` (auth)
- `follows` (base)
**Follows Schema:**
```json
[
{
"name": "follower",
"type": "relation",
"options": {
"collectionId": "USERS_COLLECTION_ID",
"maxSelect": 1
}
},
{
"name": "following",
"type": "relation",
"options": {
"collectionId": "USERS_COLLECTION_ID",
"maxSelect": 1
}
}
]
```
**Operations:**
```javascript
// Follow user
await pb.collection('follows').create({
follower: currentUserId,
following: targetUserId
});
// Unfollow
await pb.collection('follows').delete(followId);
// Get people I follow
const following = await pb.collection('follows').getList(1, 100, {
filter: 'follower = "' + currentUserId + '"',
expand: 'following'
});
const followingUsers = following.items.map(item => item.expand.following);
// Get my followers
const followers = await pb.collection('follows').getList(1, 100, {
filter: 'following = "' + currentUserId + '"',
expand: 'follower'
});
```
## Self-Referencing Relations
Create hierarchical data (categories, organizational structure):
```json
{
"name": "parent",
"type": "relation",
"options": {
"collectionId": "CATEGORIES_COLLECTION_ID",
"maxSelect": 1,
"cascadeDelete": false
}
}
```
**Operations:**
```javascript
// Create category with parent
const child = await pb.collection('categories').create({
name: 'JavaScript',
parent: parentCategoryId
});
// Get all top-level categories
const topLevel = await pb.collection('categories').getList(1, 50, {
filter: 'parent = ""'
});
// Get children of category
const children = await pb.collection('categories').getList(1, 50, {
filter: 'parent = "' + parentId + '"'
});
```
## Relation Rules
Control who can create, update, or delete relations:
### Owner-Based Rules
```javascript
// Comments collection
Create Rule: @request.auth.id != ""
Update Rule: author = @request.auth.id
Delete Rule: author = @request.auth.id
// Posts collection
Update Rule: author = @request.auth.id || @request.auth.role = "admin"
Delete Rule: author = @request.auth.id || @request.auth.role = "admin"
```
### Prevent Relation Changes
```javascript
// Once created, relation cannot be changed
Update Rule: false
```
### Read-Only Relations
```javascript
// Anyone can read, only admins can modify
View Rule: true
Update Rule: @request.auth.role = "admin"
```
## Performance Optimization
### Index Related Fields
```sql
-- Index foreign keys for faster joins
CREATE INDEX idx_comments_post ON comments(post_id);
CREATE INDEX idx_comments_author ON comments(author_id);
CREATE INDEX idx_follows_follower ON follows(follower_id);
CREATE INDEX idx_follows_following ON follows(following_id);
```
### Use Views for Complex Queries
Create a view for frequently accessed relation data:
```sql
CREATE VIEW post_with_stats AS
SELECT
p.*,
(SELECT COUNT(*) FROM comments c WHERE c.post = p.id) as comment_count,
(SELECT COUNT(*) FROM likes l WHERE l.post = p.id) as like_count
FROM posts p;
```
### Limit Expand Depth
```javascript
// Instead of
expand: 'comments,comments.author,comments.author.profile'
// Use
expand: 'comments,author'
// Then load author.profile separately if needed
```
### Paginate Relations
```javascript
// For large relation arrays
const page1 = await pb.collection('posts').getOne(postId, {
expand: 'comments',
filter: 'created >= "2024-01-01"' // Filter comments
});
const page2 = await pb.collection('comments').getList(1, 50, {
filter: 'post = "' + postId + '" && created >= "2024-01-15"'
});
```
## Troubleshooting
**Relation not showing in expand**
- Check collectionId is correct
- Verify relation field name
- Check if related record exists
- Ensure user has permission to access related record
**Can't create relation**
- Check createRule on both collections
- Verify user is authenticated
- Ensure target record exists
- Check maxSelect limit
**Slow relation queries**
- Add database indexes
- Reduce expand depth
- Use views for complex queries
- Consider denormalization for performance
**Circular reference errors**
- Avoid circular relation definitions
- Use views to flatten data
- Limit expand depth
## Best Practices
1. **Plan your data model**
- Sketch relationships before implementing
- Consider query patterns
- Plan for scalability
2. **Use cascadeDelete wisely**
- True for dependent data (comments → posts)
- False for independent references (posts → authors)
3. **Index foreign keys**
- Always index fields used in relations
- Improves join performance
4. **Limit expand depth**
- 2-3 levels max
- Use views for deeper expansions
5. **Consider denormalization**
- Store frequently accessed data directly
- Use views or triggers to keep in sync
6. **Use junction tables for many-to-many**
- Most flexible approach
- Easy to query and update
7. **Test relation rules thoroughly**
- Verify permissions work correctly
- Test cascadeDelete behavior
## Related Topics
- [Collections](collections.md) - Collection design
- [API Rules & Filters](api_rules_filters.md) - Security rules
- [Schema Templates](../templates/schema_templates.md) - Pre-built relation schemas
- [API Records](../api_records.md) - CRUD with relations