Initial commit
This commit is contained in:
664
skills/pocketbase/references/core/api_rules_filters.md
Normal file
664
skills/pocketbase/references/core/api_rules_filters.md
Normal 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
|
||||
583
skills/pocketbase/references/core/authentication.md
Normal file
583
skills/pocketbase/references/core/authentication.md
Normal 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
|
||||
514
skills/pocketbase/references/core/cli_commands.md
Normal file
514
skills/pocketbase/references/core/cli_commands.md
Normal 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
|
||||
544
skills/pocketbase/references/core/collections.md
Normal file
544
skills/pocketbase/references/core/collections.md
Normal 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
|
||||
184
skills/pocketbase/references/core/data_migration.md
Normal file
184
skills/pocketbase/references/core/data_migration.md
Normal 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 PocketBase’s 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.
|
||||
768
skills/pocketbase/references/core/files_handling.md
Normal file
768
skills/pocketbase/references/core/files_handling.md
Normal 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
|
||||
246
skills/pocketbase/references/core/getting_started.md
Normal file
246
skills/pocketbase/references/core/getting_started.md
Normal 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)
|
||||
733
skills/pocketbase/references/core/going_to_production.md
Normal file
733
skills/pocketbase/references/core/going_to_production.md
Normal 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
|
||||
802
skills/pocketbase/references/core/working_with_relations.md
Normal file
802
skills/pocketbase/references/core/working_with_relations.md
Normal 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
|
||||
Reference in New Issue
Block a user