Initial commit

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

View File

@@ -0,0 +1,14 @@
{
"name": "pocketbase",
"description": "Comprehensive PocketBase development and deployment toolkit with 40+ reference files covering setup, API integration, Go extensions, security rules, schema templates, and deployment guides",
"version": "0.0.2",
"author": {
"name": "Will Hampson"
},
"skills": [
"./skills"
],
"agents": [
"./agents"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# pocketbase
Comprehensive PocketBase development and deployment toolkit with 40+ reference files covering setup, API integration, Go extensions, security rules, schema templates, and deployment guides

View File

@@ -0,0 +1,410 @@
---
name: pocketbase-docs-researcher
description: Use this agent proactively when you need comprehensive research on PocketBase topics. This agent is optimized to use the pocketbase skill in the pocketbase-plugin which contains 40+ reference files. It efficiently searches, combines, and synthesizes information from the skill's organized structure to provide detailed implementation guidance. Examples: <example>User asks: 'How do I implement real-time subscriptions in PocketBase?' Assistant: 'I'll research the realtime API and provide comprehensive implementation guidance.'</example> <example>User asks: 'What's the best way to structure a blog schema?' Assistant: 'I'll consult the schema templates and collection design patterns to provide you with an optimal solution.'</example>
tools: Bash, Glob, Grep, Read, Edit, Write, NotebookEdit, WebFetch, TodoWrite, WebSearch, BashOutput, KillShell, AskUserQuestion, Skill, SlashCommand, mcp__context7__resolve-library-id, mcp__context7__get-library-docs
model: inherit
color: purple
---
# Researcher-Pocketbase Agent
## Overview
You are a specialized PocketBase research agent optimized to use the comprehensive **pocketbase skill** which contains 40+ modular reference files. Your expertise lies in efficiently navigating this knowledge base, extracting relevant information from multiple files, and synthesizing it into comprehensive, actionable guidance. PocketBase is an open source backend in 1 file. It provides a realtime database, authentication, file storage, an admin dashboard and is extendable to much more.
## Core Competency: Skill Navigation
**The PocketBase Skill Structure:**
```
pocketbase/SKILL.md (table of contents)
├── references/core/ (7 foundational files)
├── references/api/ (9 API endpoint files)
├── references/go/ (17 Go extension files)
├── references/sdk/ (3 SDK files)
├── references/templates/ (schema templates)
└── references/ (security rules, API reference)
```
**Your Primary Research Tools:**
1. **Start with SKILL.md** - Always begin here for navigation and overview
2. **Match Query to Topic Areas** - Use the table of contents to identify relevant files
3. **Read Multiple Files** - Combine information from related files for complete answers
4. **Cross-Reference** - Link related concepts across different files
5. **Extract Examples** - Find and adapt practical code examples
## Research Methodology
### Step 1: Analyze the Query
Categorize the research request:
**Setup & Configuration**
- Initial setup → `getting_started.md`
- CLI commands → `cli_commands.md`
- Production deployment → `going_to_production.md`
- Data modeling → `collections.md`
**Data Management**
- Collections → `collections.md`
- Relations → `working_with_relations.md`
- Files → `files_handling.md`
**Security & Access Control**
- Authentication → `authentication.md`
- Security rules → `api_rules_filters.md` + `security_rules.md`
**API Integration**
- CRUD operations → `api_records.md`
- Real-time updates → `api_realtime.md`
- File handling → `api_files.md`
- Other endpoints → `api_collections.md`, `api_settings.md`, etc.
**Frontend Development**
- JavaScript SDK → `js_sdk.md`
- Schema templates → `templates/schema_templates.md`
**Backend Development**
- Go extensions → `go_overview.md` and other `go_*.md` files
### Step 2: Navigate the Skill
**For Quick Lookup:**
1. Check SKILL.md table of contents for direct file links
2. Use Quick Reference Index for common tasks
3. Match query patterns to suggested files
**For Comprehensive Research:**
1. Start with core concept file (e.g., `authentication.md`)
2. Read related API file (e.g., `api_records.md`)
3. Check security guidance (`security_rules.md`)
4. Review examples in template files if applicable
5. Consider Go extensions for advanced needs
**For Complex Topics:**
1. Identify all relevant files from table of contents
2. Create information extraction plan
3. Read and extract from each file systematically
4. Synthesize into comprehensive guide
### Step 3: Extract and Synthesize
**Information Extraction Patterns:**
**From Core Files (references/core/):**
- Foundational concepts
- Setup procedures
- Configuration options
- Best practices
- Common patterns
**From API Files (references/api/):**
- Endpoint details
- Request/response formats
- Code examples
- SDK usage
**From Go Files (references/go/):**
- Custom extensions
- Event hooks
- Advanced functionality
- Implementation details
**From Templates (references/templates/):**
- Pre-built schemas
- Common application patterns
- Starting points for projects
### Step 4: Combine Multiple Sources
**Cross-File Research Example:**
Topic: "How to build a multi-tenant SaaS with role-based access"
1. **Core Concepts** - Read `collections.md` for data modeling
2. **Authentication** - Review `authentication.md` for user management
3. **Security Rules** - Study `api_rules_filters.md` and `security_rules.md` for access control
4. **Relations** - Check `working_with_relations.md` for multi-tenant patterns
5. **Schema Templates** - Look at `schema_templates.md` for SaaS models
6. **Production** - Review `going_to_production.md` for deployment considerations
## Query Classification Guide
### "How do I..." Questions
→ Start with relevant core file, supplement with API examples
- How do I create a blog? → `schema_templates.md` + `collections.md`
- How do I set up authentication? → `authentication.md`
- How do I upload files? → `files_handling.md`
- How do I add real-time updates? → `api_realtime.md`
- How do I start the server? → `cli_commands.md` (serve command)
- How do I run migrations? → `cli_commands.md` (migrate command)
- How do I create admin user? → `cli_commands.md` (superuser command)
### "How to configure..." Questions
→ Focus on configuration-focused files
- How to configure production? → `going_to_production.md`
- How to set up security rules? → `api_rules_filters.md`
- How to create custom endpoints? → Go extension files
- How to schedule jobs? → `api_crons.md` + `go_jobs_scheduling.md`
- How to configure CLI flags? → `cli_commands.md` (global flags section)
### "What's the best way to..." Questions
→ Combine best practices from multiple files
- Best data structure? → `collections.md` + `working_with_relations.md`
- Best security approach? → `security_rules.md` + `api_rules_filters.md`
- Best query optimization? → `api_records.md` (Performance Tips)
- Best file handling? → `files_handling.md`
### "Error:..." Questions
→ Identify error type, find solutions in relevant files
- CORS errors → `authentication.md` + `going_to_production.md`
- Permission denied → `api_rules_filters.md` + `security_rules.md`
- File upload fails → `files_handling.md`
- Slow queries → `api_records.md` (Performance Tips)
- Server won't start → `cli_commands.md` (troubleshooting section)
- Migration errors → `cli_commands.md` (migration troubleshooting)
### "Need to implement..." Questions
→ Focus on implementation guides
- User roles? → `authentication.md` + `security_rules.md`
- File uploads? → `files_handling.md`
- Real-time chat? → `api_realtime.md`
- Custom logic? → Go extension files
- Deployment automation? → `cli_commands.md` (scripting examples)
## Information Synthesis Process
### For Implementation Guides
Structure your response with:
1. **Overview** - What and why (from core concepts)
2. **Setup** - How to configure (from setup files)
3. **Implementation** - Step-by-step with code (from API files)
4. **Security** - Access control and rules (from security files)
5. **Best Practices** - Optimization and patterns (from best practices sections)
6. **Examples** - Real-world scenarios (from templates/examples)
7. **Troubleshooting** - Common issues and solutions
8. **Next Steps** - Related topics for further exploration
### For Code Examples
**Source Code From:**
- API files for endpoint usage
- Core files for configuration
- Template files for complete examples
- Go files for custom extensions
**For Each Example:**
- Show complete, runnable code
- Include necessary imports
- Add explanatory comments
- Provide usage context
- Include error handling
### For Best Practices
**Combine From:**
- Best practices sections in core files
- Performance tips in API files
- Security guidance in security files
- Production recommendations
**Organize As:**
- Do's and Don'ts
- Performance considerations
- Security requirements
- Scalability guidance
- Maintenance tips
## Output Format
Your research output should be a comprehensive markdown document with:
### 1. Executive Summary
- Brief overview of the topic
- Why it matters
- What you'll learn
### 2. Core Concepts
- Essential terminology
- Fundamental principles
- Key architecture patterns
### 3. Implementation Guide
- Step-by-step instructions
- Complete code examples
- Configuration details
- Security considerations
### 4. Code Examples
- Practical, copy-paste ready
- Multiple use cases
- Frontend and backend examples
- Real-world scenarios
### 5. Best Practices
- Industry-standard approaches
- Performance optimization
- Security recommendations
- Scalability patterns
### 6. Common Patterns
- Repeated use cases
- Template approaches
- Reusable components
### 7. Troubleshooting
- Frequently encountered issues
- Solutions and workarounds
- Debugging techniques
### 8. Related Resources
- Cross-references to other PocketBase topics
- Suggested follow-up reading
- Additional documentation
### 9. Version Notes
- PocketBase 0.30.4 specific features
- Version differences
- Migration considerations
## Quality Standards
### Information Accuracy
- Extract information directly from skill reference files
- Verify code examples compile and work
- Ensure version-specific accuracy (0.30.4)
- Cross-reference multiple sources for validation
### Completeness
- Address all aspects of the question
- Include both basic and advanced approaches
- Cover success and error cases
- Provide context for decision-making
### Clarity
- Use clear, concise language
- Organize information logically
- Include visual structure (headers, lists, code blocks)
- Provide actionable guidance
### Practicality
- Focus on real-world implementation
- Include complete, working examples
- Address common edge cases
- Provide troubleshooting guidance
## Context Awareness
### For Educational Projects
- Emphasize learning objectives
- Explain why, not just how
- Include alternative approaches
- Encourage experimentation
### For Production Systems
- Focus on security and performance
- Include monitoring and maintenance
- Provide backup and recovery guidance
- Address scalability concerns
### For Rapid Prototyping
- Provide quick-start templates
- Suggest schema templates
- Recommend best practices for iteration
- Balance speed with maintainability
## Skill-Specific Guidance
### Using the Modular Structure
**When Reading Files:**
1. Start with relevant section headers
2. Focus on code examples
3. Review best practices
4. Check troubleshooting
5. Note related topics
**When Combining Information:**
1. Identify common themes across files
2. Resolve conflicting recommendations
3. Build comprehensive understanding
4. Create unified guidance
**When Providing References:**
- Cite specific file paths
- Include section anchors
- Suggest related files
- Recommend reading order
### Efficient Navigation
**Quick Tasks (< 5 minutes):**
- Check SKILL.md Quick Reference Index
- Read 1-2 most relevant files
- Extract key information
- Provide focused answer
**Standard Tasks (5-15 minutes):**
- Identify 3-5 relevant files
- Read core concepts and examples
- Synthesize information
- Provide comprehensive answer
**Complex Tasks (15+ minutes):**
- Map all related files
- Read systematically
- Extract and organize information
- Create detailed guide with cross-references
## Research Workflow Checklist
### Before Research
- [ ] Understand the user's specific use case
- [ ] Identify the topic category
- [ ] Plan which files to read
- [ ] Estimate research scope
### During Research
- [ ] Start with SKILL.md table of contents
- [ ] Read core concept file first
- [ ] Supplement with API and example files
- [ ] Extract relevant code examples
- [ ] Note best practices and pitfalls
- [ ] Identify related topics
### After Research
- [ ] Synthesize information into coherent guide
- [ ] Ensure all questions are answered
- [ ] Include practical code examples
- [ ] Provide troubleshooting guidance
- [ ] Suggest related resources
- [ ] Verify accuracy and completeness
## Advanced Techniques
### Pattern Recognition
- Recognize common query patterns
- Match to established file organization
- Build on previous research patterns
- Develop expertise over time
### Cross-File Synthesis
- Identify related concepts across files
- Build comprehensive understanding
- Create unified guidance
- Bridge different aspects
### Adaptive Research
- Adjust depth based on user needs
- Provide overview for beginners
- Provide detail for experts
- Scale complexity appropriately
## Goal
Your ultimate goal is to provide the most comprehensive, accurate, and practical PocketBase guidance by efficiently leveraging the modular skill's 40+ reference files. You should feel like a PocketBase expert who can answer any question by drawing from this extensive knowledge base and synthesizing it into actionable guidance.
Every research task should result in a document that enables successful implementation, whether the user is a beginner learning the basics or an expert implementing advanced features.

241
plugin.lock.json Normal file
View File

@@ -0,0 +1,241 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:Whamp/whamp-claude-tools:pocketbase-plugin",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "8f50eac8c955da2691206d32ae2bc82443255aac",
"treeHash": "3021d3fb740f185b39ce7dbd89a3f443212020be59fb4992812d3ec531e8b87b",
"generatedAt": "2025-11-28T10:12:57.164355Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "pocketbase",
"description": "Comprehensive PocketBase development and deployment toolkit with 40+ reference files covering setup, API integration, Go extensions, security rules, schema templates, and deployment guides",
"version": "0.0.2"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "d45f373016b09cb2ca7bb8eafa4582d0d4e77f421dc8bf243cc623a49e236921"
},
{
"path": "agents/pocketbase-docs-researcher.md",
"sha256": "079c8342d2d1c2476fc6ff62f9763e80b654391cf1b9c20f6e6dc95852f3df1d"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "3603c1a41409c04fed935d433ac8fd8e08d5d9260d9b484100ae0734abedc6d3"
},
{
"path": "skills/pocketbase/SKILL.md",
"sha256": "b8589eed71a27fec41f58d755a34393f4c8c185b72b237d5b289a2a739223178"
},
{
"path": "skills/pocketbase/references/schema_templates.md",
"sha256": "81e5c1d4dbc7a5228bdc86c5c7f210aa89d66e0e6fadcade9389290462a22afc"
},
{
"path": "skills/pocketbase/references/api_reference.md",
"sha256": "1e05161690daa685098c7631d744e0880020f7d5fb4aece51748cbb6a2d44b42"
},
{
"path": "skills/pocketbase/references/security_rules.md",
"sha256": "114c2a4a02e70b2fa6ab4719b8816e42380f04ac3249f4ba9d683a20491772c5"
},
{
"path": "skills/pocketbase/references/go/go_migrations.md",
"sha256": "5c29835910e41891b1f898f33d78c5857093a594cfc867bb0df541d5b3ef8780"
},
{
"path": "skills/pocketbase/references/go/go_logging.md",
"sha256": "18d3db1a2fb91173fea02bda9fabceee483afc12ed91e5059863fb5e3ff95cdf"
},
{
"path": "skills/pocketbase/references/go/go_event_hooks.md",
"sha256": "86ba27d56a5592b7f1eab89eb012ddb169b663a4fcd34684eea4134197657a25"
},
{
"path": "skills/pocketbase/references/go/go_collections.md",
"sha256": "3d744d49d063e6b3ea6840fb40d08563504158b3fbd827245cab553d5cb3a1ab"
},
{
"path": "skills/pocketbase/references/go/go_jobs_scheduling.md",
"sha256": "5c163d3faac051d4d3dfd309f739df50df9a63c28b2ea4b8033d5bc1304514a0"
},
{
"path": "skills/pocketbase/references/go/go_record_proxy.md",
"sha256": "301bff1cad9b026f0cd321eedf42e61300eb80a641f1d47e8e066e0b1a75f3e0"
},
{
"path": "skills/pocketbase/references/go/go_overview.md",
"sha256": "aaf96f20269fc12e5186c8b1ea5d07c35ccec0558b5d5e4c9bd8b70bb6997beb"
},
{
"path": "skills/pocketbase/references/go/go_records.md",
"sha256": "b847edcf9d04f71e74b4de4658fbde7da148094ab56362767c7ebea7dd51a52a"
},
{
"path": "skills/pocketbase/references/go/go_routing.md",
"sha256": "53a503102ac28ef9e7f965860781441dafa79f7748a6a4cdd0fccb524e7e0461"
},
{
"path": "skills/pocketbase/references/go/go_miscellaneous.md",
"sha256": "43566333cfba5e7680c4a3d4a57591dcac10cde85777a1f97fff6f01c38997b7"
},
{
"path": "skills/pocketbase/references/go/go_sending_emails.md",
"sha256": "3dec705a632a4babbd28e06526f356865dacfa60c53734b6e7b43959c4d77fbe"
},
{
"path": "skills/pocketbase/references/go/go_rendering_templates.md",
"sha256": "5c945cd6e05ffc1e905244d07c076fedf41317f78bc8c07038e88d8ff22a9c1a"
},
{
"path": "skills/pocketbase/references/go/go_database.md",
"sha256": "3c7205568deb0f4c5beed246280eba9147d5ab22e87834eaa7c6c27df98bc3f6"
},
{
"path": "skills/pocketbase/references/go/go_filesystem.md",
"sha256": "066189e105fbf91f0203f0d655db4b55a2084e5279e2929ac5aacfdcf6dc4716"
},
{
"path": "skills/pocketbase/references/go/go_realtime.md",
"sha256": "13f594a8dfa91a13697ccb4408b922b47b977dc5ffd0a6698315faacd2598f37"
},
{
"path": "skills/pocketbase/references/go/go_console_commands.md",
"sha256": "06d63d13db72b93fe9ad1a535acbab94fbcafd78e2dbef836c3380dcdbb21068"
},
{
"path": "skills/pocketbase/references/go/go_testing.md",
"sha256": "b98bef0db6f38a06122d9560ebb5dd86d775aea6b56feda0a862583d967f6ea2"
},
{
"path": "skills/pocketbase/references/go/go_sdk.md",
"sha256": "0959bfb5c13b0a1f4603144ce4a42d0984ee7f80dba02a998a1fae34eac4f0e1"
},
{
"path": "skills/pocketbase/references/core/authentication.md",
"sha256": "19882495b18d025b8410b0a0dc0d64834331da46cee6557723b6cbe084930484"
},
{
"path": "skills/pocketbase/references/core/files_handling.md",
"sha256": "365069d8a2dd5bbed1f558f560136baf0ac83e6f1dc7596ea825d23a853769ed"
},
{
"path": "skills/pocketbase/references/core/collections.md",
"sha256": "dd2ce3ddc07d6921a6f3a85c51c511ac25b85a38309186674f52fd0404bddc94"
},
{
"path": "skills/pocketbase/references/core/data_migration.md",
"sha256": "9f06d24660bdda7708f6ba5f8f7754f8595a2b1c3732b63c2f9644b07a751605"
},
{
"path": "skills/pocketbase/references/core/going_to_production.md",
"sha256": "8cfcec043ec09f7c89f85d48275becd03900c8c5e8c5d95793e50925d1d17245"
},
{
"path": "skills/pocketbase/references/core/api_rules_filters.md",
"sha256": "a0532aa1fc8dc52e9d3d9652ae346ed66b8530596b62a5a67405332cac34a2e8"
},
{
"path": "skills/pocketbase/references/core/getting_started.md",
"sha256": "9f2a49cc23e90f2e9e9256dd11a8866f039cb101f134103e6ef5c23f64b7b3a1"
},
{
"path": "skills/pocketbase/references/core/cli_commands.md",
"sha256": "4b02543faac079439afdc22053ff03b3c01f1412b259fd986129921c96588fe4"
},
{
"path": "skills/pocketbase/references/core/working_with_relations.md",
"sha256": "2a52ea8e14bf9339e37d6399b4f16da713fefcd616f8989aeee2f2ca903f778e"
},
{
"path": "skills/pocketbase/references/sdk/js_sdk.md",
"sha256": "1f92bcf7504e4486e81f1c56b586d0a7739705d1def31bc833549e6bd8db6da3"
},
{
"path": "skills/pocketbase/references/sdk/dart_sdk.md",
"sha256": "d08e13c7450ad50b20b49cb0a4330e8dbf0f82365b07b6ad23908977cf8a28eb"
},
{
"path": "skills/pocketbase/references/api/api_crons.md",
"sha256": "3ab6f43cc1a56bee71eb89b75858c6c8a810b14bb2c59ddc6baa207841c5826a"
},
{
"path": "skills/pocketbase/references/api/api_health.md",
"sha256": "3213f163ccb98f13dc37e82037f057b13c7eb33b7d8e12addad7e535a1bb458c"
},
{
"path": "skills/pocketbase/references/api/api_collections.md",
"sha256": "d2acfb80ac7435baa0817654b122a0180c489577e53f98096159a05dea9f0efd"
},
{
"path": "skills/pocketbase/references/api/api_realtime.md",
"sha256": "ef878da1f73da0204665f421a5a31ee80f2f6aa8ffafec15259237c8a1e827f6"
},
{
"path": "skills/pocketbase/references/api/api_backups.md",
"sha256": "cf2d7b186df3b8a7aa8a58666a19aadd8c50460d29ba07990da2cc6c059e1c2e"
},
{
"path": "skills/pocketbase/references/api/api_logs.md",
"sha256": "93e6c8f957033eebe041555338251306c4b8e22d7693ce1fd065d0600891ee08"
},
{
"path": "skills/pocketbase/references/api/api_files.md",
"sha256": "313385675417cc4c0bdafbb2bbec917559882d7e9cf5a5894f245509a735ebf7"
},
{
"path": "skills/pocketbase/references/api/api_records.md",
"sha256": "28882ac314109087a5b2126e07cc0963dc409cbc24ed4f43b1274c34f20c057a"
},
{
"path": "skills/pocketbase/references/api/api_settings.md",
"sha256": "18985639d0c2e08d439e3e5843e32d820c126aa06d1c0caba8f21772cf31cdab"
},
{
"path": "skills/pocketbase/scripts/export_data.py",
"sha256": "8072600d18a16cca09bab217b2b203f890a9a81c5a7cff95235355bb86896380"
},
{
"path": "skills/pocketbase/scripts/import_data.py",
"sha256": "2d604a12a9281e10ba9b1e6979b5926ee928b739315c1c2fb1d7a6b42d694472"
},
{
"path": "skills/pocketbase/scripts/setup_pocketbase.sh",
"sha256": "5d44b55a7af88ec2a680e93c8a89955358a61ddc630eb24264ae1ff90ea7c9fa"
},
{
"path": "skills/pocketbase/assets/frontend-template.html",
"sha256": "00344488358ab311d2f5e213cb3ae5ada2adf969d979a92dc1e083be8b91ffd3"
},
{
"path": "skills/pocketbase/assets/Caddyfile",
"sha256": "ca2dbc96fa0d043e63d880c16346a414d69eb1b892b0c666023d2396b1f2c414"
},
{
"path": "skills/pocketbase/assets/docker-compose.yml",
"sha256": "73cf68054800530efff41f3ad28eac180a140cd8a9fd3f0d9182e5b21c1b608b"
},
{
"path": "skills/pocketbase/assets/collection-schema-template.json",
"sha256": "48553e141f9b1ffd4478d5d535dddf2b461688d69ae2bad68d8281451b648959"
}
],
"dirSha256": "3021d3fb740f185b39ce7dbd89a3f443212020be59fb4992812d3ec531e8b87b"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

359
skills/pocketbase/SKILL.md Normal file
View File

@@ -0,0 +1,359 @@
---
name: pocketbase
description: Comprehensive PocketBase development and deployment skill providing setup guides, schema templates, security patterns, API examples, data management scripts, and real-time integration patterns for building backend services with PocketBase.
---
# PocketBase Skill - Comprehensive Reference
## Overview
This skill provides modular, searchable documentation for PocketBase development. Use the table of contents below to navigate to specific topics. The researcher-pocketbase agent can efficiently search and extract information from the reference files based on your query. PocketBase is an open source backend in 1 file. It provides a realtime database, authentication, file storage, an admin dashboard and is extendable to much more.
## Quick Navigation
### For Newcomers
→ Start with [Getting Started](references/core/getting_started.md) for initial setup and basic concepts
→ Review [Collections](references/core/collections.md) to understand data modeling
→ See [Authentication](references/core/authentication.md) for user management
→ Check [API Rules & Filters](references/core/api_rules_filters.md) for security
### For Implementation
→ Browse [Schema Templates](references/templates/schema_templates.md) for pre-built data models
→ Use [Records API](references/api/api_records.md) for CRUD operations
→ Implement [Real-time](references/api/api_realtime.md) for live updates
→ Follow [Files Handling](references/core/files_handling.md) for file uploads
→ Read [Working with Relations](references/core/working_with_relations.md) for data relationships
### For Production
→ See [Going to Production](references/core/going_to_production.md) for deployment
→ Review [Security Rules](references/security_rules.md) for access control
→ Check [API Reference](references/api_reference.md) for complete API documentation
### For Advanced Users
→ Explore [Go Extensions](references/go/go_overview.md) for custom functionality
→ Study [Event Hooks](references/go/go_event_hooks.md) for automation
→ Learn [Database Operations](references/go/go_database.md) for advanced queries
→ Plan data migrations with [Data Migration Workflows](references/core/data_migration.md)
---
## Table of Contents
### Core Concepts & Setup
| Topic | Description | When to Use |
|-------|-------------|-------------|
| [Getting Started](references/core/getting_started.md) | Initial setup, quick start, basic concepts | First time using PocketBase, initial configuration |
| [CLI Commands](references/core/cli_commands.md) | PocketBase CLI, serve, migrate, admin, superuser | Development workflow, server management, migrations |
| [Collections](references/core/collections.md) | Collection types, schema design, rules, indexes | Designing data models, creating collections |
| [Authentication](references/core/authentication.md) | User registration, login, OAuth2, JWT tokens | Building user accounts, login systems |
| [API Rules & Filters](references/core/api_rules_filters.md) | Security rules, filtering, sorting, query optimization | Controlling data access, writing efficient queries |
| [Files Handling](references/core/files_handling.md) | File uploads, thumbnails, CDN, security | Managing file uploads, image processing |
| [Working with Relations](references/core/working_with_relations.md) | One-to-many, many-to-many, data relationships | Building complex data models, linking collections |
| [Going to Production](references/core/going_to_production.md) | Deployment, security hardening, monitoring, backups | Moving from development to production |
| [Data Migration Workflows](references/core/data_migration.md) | Import/export strategies, scripts, and tooling | Planning and executing data migrations |
---
### API Reference
| Endpoint | Description | Reference File |
|----------|-------------|----------------|
| Records API | CRUD operations, pagination, filtering, batch operations | [api_records.md](references/api/api_records.md) |
| Realtime API | WebSocket subscriptions, live updates, event handling | [api_realtime.md](references/api/api_realtime.md) |
| Files API | File uploads, downloads, thumbnails, access control | [api_files.md](references/api/api_files.md) |
| Collections API | Manage collections, schemas, rules, indexes | [api_collections.md](references/api/api_collections.md) |
| Settings API | App configuration, CORS, SMTP, general settings | [api_settings.md](references/api/api_settings.md) |
| Logs API | Access logs, authentication logs, request logs | [api_logs.md](references/api/api_logs.md) |
| Crons API | Background jobs, scheduled tasks, automation | [api_crons.md](references/api/api_crons.md) |
| Backups API | Database backups, data export, disaster recovery | [api_backups.md](references/api/api_backups.md) |
| Health API | System health, metrics, performance monitoring | [api_health.md](references/api/api_health.md) |
---
### SDKs & Development Tools
| SDK | Description | Reference File |
|-----|-------------|----------------|
| JavaScript SDK | Frontend integration, React, Vue, vanilla JS | [js_sdk.md](references/sdk/js_sdk.md) |
| Go SDK | Server-side integration, custom apps | [go_sdk.md](references/sdk/go_sdk.md) |
| Dart SDK | Mobile app integration (Flutter, etc.) | [dart_sdk.md](references/sdk/dart_sdk.md) |
---
### Go Extension Framework
| Topic | Description | Reference File |
|-------|-------------|----------------|
| Go Overview | Project structure, basic concepts, getting started | [go_overview.md](references/go/go_overview.md) |
| Event Hooks | Before/After hooks, custom logic, automation | [go_event_hooks.md](references/go/go_event_hooks.md) |
| Routing | Custom API endpoints, middleware, handlers | [go_routing.md](references/go/go_routing.md) |
| Database | Query builder, transactions, advanced queries | [go_database.md](references/go/go_database.md) |
| Records | Record CRUD, validation, custom fields | [go_records.md](references/go/go_records.md) |
| Collections | Dynamic schemas, collection management | [go_collections.md](references/go/go_collections.md) |
| Migrations | Schema changes, version control, deployment | [go_migrations.md](references/go/go_migrations.md) |
| Jobs & Scheduling | Background tasks, cron jobs, queues | [go_jobs_scheduling.md](references/go/go_jobs_scheduling.md) |
| Sending Emails | SMTP configuration, templated emails | [go_sending_emails.md](references/go/go_sending_emails.md) |
| Rendering Templates | HTML templates, email templates, PDFs | [go_rendering_templates.md](references/go/go_rendering_templates.md) |
| Console Commands | CLI commands, migrations, maintenance | [go_console_commands.md](references/go/go_console_commands.md) |
| Realtime | Custom realtime logic, event handling | [go_realtime.md](references/go/go_realtime.md) |
| File System | File storage, CDN, external storage providers | [go_filesystem.md](references/go/go_filesystem.md) |
| Logging | Structured logging, monitoring, debugging | [go_logging.md](references/go/go_logging.md) |
| Testing | Unit tests, integration tests, test helpers | [go_testing.md](references/go/go_testing.md) |
| Miscellaneous | Advanced features, utilities, tips | [go_miscellaneous.md](references/go/go_miscellaneous.md) |
| Record Proxy | Dynamic record behavior, computed fields | [go_record_proxy.md](references/go/go_record_proxy.md) |
---
### Reference Materials
| File | Description | Contents |
|------|-------------|----------|
| [Security Rules](references/security_rules.md) | Comprehensive security patterns | Owner-based access, role-based access, API rules |
| [Schema Templates](references/templates/schema_templates.md) | Pre-built data models | Blog, E-commerce, Social Network, Forums, Task Management |
| [API Reference](references/api_reference.md) | Complete API documentation | All endpoints, parameters, examples |
---
### Development Resources
| Resource | Description | Location |
|----------|-------------|----------|
| Scripts | Executable utilities for development | `scripts/` directory |
| Assets | Templates and configuration files | `assets/` directory |
| Docker Config | Production-ready Docker setup | `assets/docker-compose.yml` |
| Caddy Config | Automatic HTTPS configuration | `assets/Caddyfile` |
| Frontend Template | HTML/JS integration example | `assets/frontend-template.html` |
| Collection Schema | Blank collection template | `assets/collection-schema-template.json` |
---
## How to Use This Skill
### For the Researcher-Pocketbase Agent
This skill is designed for efficient information retrieval. When researching PocketBase topics:
1. **Start with Core Concepts** - Review `references/core/` for foundational knowledge
2. **Find API Details** - Use `references/api/` for specific API endpoints
3. **Look up Go Extensions** - Check `references/go/` for custom functionality
4. **Find Examples** - Reference `references/templates/` and `references/security_rules.md`
### Topic Categories
**Setup & Configuration**
- Getting Started → Initial setup and basic concepts
- CLI Commands → Development workflow, server management
- Going to Production → Deployment and production configuration
- Collections → Data model design
**Data Management**
- Collections → Creating and managing collections
- Working with Relations → Linking data across collections
- Files Handling → File uploads and storage
**Security & Access Control**
- Authentication → User management
- API Rules & Filters → Access control and queries
- Security Rules → Comprehensive security patterns
**API Integration**
- Records API → CRUD operations
- Realtime API → Live updates
- Files API → File management
- Other API endpoints → Settings, logs, backups, health
**Frontend Development**
- JavaScript SDK → Web integration
- Schema Templates → Pre-built data models
- Frontend Template → Integration example
**Backend Development**
- Go SDK → Server-side integration
- Go Extensions → Custom functionality
- Console Commands → CLI tools
### Common Query Patterns
**"How do I..."**
- How do I create a blog? → Schema Templates
- How do I set up authentication? → Authentication
- How do I upload files? → Files Handling
- How do I add real-time updates? → Realtime API
**"How to configure..."**
- How to configure production? → Going to Production
- How to set up security rules? → API Rules & Filters, Security Rules
- How to create custom endpoints? → Go Routing
- How to schedule jobs? → Jobs & Scheduling
**"What's the best way to..."**
- What's the best way to structure my data? → Collections, Working with Relations
- What's the best way to secure my API? → API Rules & Filters, Security Rules
- What's the best way to optimize queries? → API Rules & Filters
- What's the best way to handle files? → Files Handling
**"Error:..."**
- CORS errors → Authentication, Going to Production
- Permission errors → API Rules & Filters, Security Rules
- File upload errors → Files Handling
- Slow queries → API Rules & Filters (indexing)
**"Need to implement..."**
- Need user roles? → Authentication, Security Rules
- Need file uploads? → Files Handling
- Need real-time chat? → Realtime API
- Need custom logic? → Go Extensions
---
## Quick Reference Index
### Common Tasks
- [Set up PocketBase](references/core/getting_started.md#quick-setup)
- [Master the CLI](references/core/cli_commands.md#overview)
- [Create collection](references/core/collections.md#creating-collections)
- [Add authentication](references/core/authentication.md#registration)
- [Write security rules](references/core/api_rules_filters.md#common-rule-patterns)
- [Upload files](references/core/files_handling.md#uploading-files)
- [Create relations](references/core/working_with_relations.md#creating-relations)
- [Query data](references/api/api_records.md#read-records)
- [Set up real-time](references/api/api_realtime.md#subscriptions)
- [Deploy to production](references/core/going_to_production.md#deployment-options)
### Code Examples
- [React integration](references/core/getting_started.md#react-integration)
- [Vue.js integration](references/core/getting_started.md#vuejs-example)
- [Create record](references/api/api_records.md#create-record)
- [Filter queries](references/api/api_records.md#filtering)
- [File upload](references/core/files_handling.md#single-file-upload)
- [Custom endpoint](references/go/go_overview.md#custom-api-endpoints)
- [Event hook](references/go/go_overview.md#event-hooks)
### Best Practices
- [Schema design](references/core/collections.md#best-practices)
- [Security rules](references/security_rules.md#best-practices)
- [Query optimization](references/api/api_records.md#performance-tips)
- [Production deployment](references/core/going_to_production.md#best-practices-checklist)
---
## File Locations
### Core Documentation
```
/references/core/
├── getting_started.md # Initial setup and concepts
├── cli_commands.md # CLI commands and server management
├── collections.md # Data modeling and collections
├── authentication.md # User management
├── api_rules_filters.md # Security and querying
├── files_handling.md # File uploads and storage
├── working_with_relations.md # Data relationships
└── going_to_production.md # Deployment guide
```
### API Reference
```
/references/api/
├── api_records.md # CRUD operations
├── api_realtime.md # WebSocket subscriptions
├── api_files.md # File management
├── api_collections.md # Collection operations
├── api_settings.md # App configuration
├── api_logs.md # Logging
├── api_crons.md # Background jobs
├── api_backups.md # Backups
└── api_health.md # Health checks
```
### Go Extensions
```
/references/go/
├── go_overview.md # Getting started
├── go_event_hooks.md # Event system
├── go_routing.md # Custom routes
├── go_database.md # Database operations
├── go_records.md # Record management
├── go_collections.md # Collection management
├── go_migrations.md # Schema changes
├── go_jobs_scheduling.md # Background tasks
├── go_sending_emails.md # Email integration
├── go_rendering_templates.md # Templates
├── go_console_commands.md # CLI tools
├── go_realtime.md # Custom realtime
├── go_filesystem.md # File storage
├── go_logging.md # Logging
├── go_testing.md # Testing
├── go_miscellaneous.md # Advanced topics
└── go_record_proxy.md # Dynamic behavior
```
### SDKs
```
/references/sdk/
├── js_sdk.md # JavaScript
├── go_sdk.md # Go
└── dart_sdk.md # Dart
```
### Templates & Security
```
/references/
├── security_rules.md # Security patterns
├── templates/
│ └── schema_templates.md # Pre-built schemas
└── api_reference.md # Complete API docs
```
---
## Information Architecture
This skill follows a modular architecture designed for progressive disclosure:
1. **Quick Access** - SKILL.md provides immediate overview and navigation
2. **Focused Topics** - Each reference file covers one specific area
3. **Cross-References** - Files link to related topics
4. **Examples First** - Practical code examples in each file
5. **Searchable** - Clear titles and descriptions for quick lookup
### When to Use Each Section
**references/core/**
- New users starting with PocketBase
- Understanding fundamental concepts
- Basic implementation tasks
**references/api/**
- Implementing specific API features
- Looking up endpoint details
- API integration examples
**references/go/**
- Building custom PocketBase extensions
- Advanced functionality
- Custom business logic
**references/sdk/**
- Frontend or mobile integration
- SDK-specific features
- Language-specific examples
**references/security_rules.md**
- Complex access control scenarios
- Security best practices
- Multi-tenant applications
**references/templates/**
- Rapid prototyping
- Common application patterns
- Pre-built data models
---
## Notes for Researchers
This skill contains over 30 reference files covering all aspects of PocketBase development. The researcher-pocketbase agent should:
1. Match queries to appropriate topic areas
2. Extract specific information from relevant files
3. Provide comprehensive answers using multiple references
4. Suggest related topics for further reading
5. Identify best practices and common pitfalls
Each reference file is self-contained with examples, explanations, and best practices. Use the table of contents above to quickly navigate to the most relevant information for any PocketBase-related question.

View File

@@ -0,0 +1,27 @@
# PocketBase Caddy Configuration
# This provides automatic HTTPS and reverse proxy
yourdomain.com {
# Redirect HTTP to HTTPS
redir https://{host}{uri} permanent
# Proxy to PocketBase
reverse_proxy pocketbase:8090
# Enable gzip compression
encode gzip
# Set security headers
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
}
# For development (HTTP only)
localhost {
reverse_proxy pocketbase:8090
encode gzip
}

View File

@@ -0,0 +1,104 @@
{
"name": "CollectionName",
"type": "base",
"system": false,
"schema": [
{
"id": "field_id",
"name": "Field Name",
"type": "text",
"required": true,
"options": {
"min": 1,
"max": 200
}
},
{
"id": "text_field",
"name": "Text Field",
"type": "text",
"required": false,
"options": {
"min": 0,
"max": 1000
}
},
{
"id": "number_field",
"name": "Number Field",
"type": "number",
"required": false,
"options": {
"min": 0,
"max": 100
}
},
{
"id": "bool_field",
"name": "Boolean Field",
"type": "bool",
"required": true
},
{
"id": "email_field",
"name": "Email Field",
"type": "email",
"required": false
},
{
"id": "url_field",
"name": "URL Field",
"type": "url",
"required": false
},
{
"id": "date_field",
"name": "Date Field",
"type": "date",
"required": false
},
{
"id": "json_field",
"name": "JSON Field",
"type": "json",
"required": false
},
{
"id": "select_field",
"name": "Select Field",
"type": "select",
"required": false,
"options": {
"values": ["option1", "option2", "option3"]
}
},
{
"id": "relation_field",
"name": "Relation Field",
"type": "relation",
"required": false,
"options": {
"collectionId": "collection_id_here",
"cascadeDelete": false,
"maxSelect": 1
}
},
{
"id": "file_field",
"name": "File Field",
"type": "file",
"required": false,
"options": {
"maxSelect": 1,
"maxSize": 10485760,
"mimeTypes": ["image/jpeg", "image/png"],
"thumbs": ["100x100", "300x300"]
}
}
],
"listRule": "",
"viewRule": "",
"createRule": "@request.auth.id != ''",
"updateRule": "field = @request.auth.id",
"deleteRule": "field = @request.auth.id"
}

View File

@@ -0,0 +1,35 @@
version: '3.8'
services:
pocketbase:
image: ghcr.io/pocketbase/pocketbase:latest
command:
- serve
- --http=0.0.0.0:8090
container_name: pocketbase
restart: unless-stopped
ports:
- "8090:8090"
volumes:
- ./pb_data:/pb/pb_data
environment:
- PB_PUBLIC_DIR=/pb/public
# Optional: Add a reverse proxy (Caddy)
caddy:
image: caddy:2
container_name: pocketbase-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- pocketbase
volumes:
caddy_data:
caddy_config:

View File

@@ -0,0 +1,275 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PocketBase Frontend Template</title>
<script src="https://unpkg.com/pocketbase@latest/dist/pocketbase.umd.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.hidden {
display: none;
}
input, textarea, button {
width: 100%;
padding: 10px;
margin: 5px 0;
box-sizing: border-box;
}
button {
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.error {
color: red;
margin: 10px 0;
}
.success {
color: green;
margin: 10px 0;
}
#posts {
margin-top: 20px;
}
.post {
border-left: 3px solid #007bff;
padding-left: 15px;
margin: 15px 0;
}
</style>
</head>
<body>
<h1>PocketBase Frontend Template</h1>
<!-- Auth Section -->
<div id="auth-section" class="card">
<h2>Authentication</h2>
<div id="login-form">
<h3>Login</h3>
<input type="email" id="login-email" placeholder="Email" required>
<input type="password" id="login-password" placeholder="Password" required>
<button onclick="login()">Login</button>
<div id="login-message"></div>
</div>
<div id="register-form" style="margin-top: 20px;">
<h3>Register</h3>
<input type="email" id="register-email" placeholder="Email" required>
<input type="text" id="register-name" placeholder="Name" required>
<input type="password" id="register-password" placeholder="Password" required>
<input type="password" id="register-password-confirm" placeholder="Confirm Password" required>
<button onclick="register()">Register</button>
<div id="register-message"></div>
</div>
</div>
<!-- User Info Section -->
<div id="user-section" class="card hidden">
<h2>Welcome, <span id="user-email"></span></h2>
<button onclick="logout()">Logout</button>
</div>
<!-- Create Post Section -->
<div id="create-section" class="card hidden">
<h2>Create Post</h2>
<input type="text" id="post-title" placeholder="Post Title" required>
<textarea id="post-content" placeholder="Post Content" rows="5" required></textarea>
<button onclick="createPost()">Create Post</button>
<div id="create-message"></div>
</div>
<!-- Posts Section -->
<div id="posts" class="card hidden">
<h2>Posts</h2>
<button onclick="loadPosts()">Refresh Posts</button>
<div id="posts-list"></div>
</div>
<script>
// Initialize PocketBase client
const pb = new PocketBase('http://127.0.0.1:8090');
// Check if user is already logged in
checkAuth();
async function checkAuth() {
if (pb.authStore.isValid) {
const user = pb.authStore.model;
document.getElementById('auth-section').classList.add('hidden');
document.getElementById('user-section').classList.remove('hidden');
document.getElementById('create-section').classList.remove('hidden');
document.getElementById('posts').classList.remove('hidden');
document.getElementById('user-email').textContent = user.email;
loadPosts();
}
}
async function login() {
const email = document.getElementById('login-email').value;
const password = document.getElementById('login-password').value;
const messageDiv = document.getElementById('login-message');
try {
const authData = await pb.collection('users').authWithPassword(email, password);
messageDiv.className = 'success';
messageDiv.textContent = 'Login successful!';
checkAuth();
} catch (e) {
messageDiv.className = 'error';
messageDiv.textContent = e.data?.message || 'Login failed. Please check your credentials.';
}
}
async function register() {
const email = document.getElementById('register-email').value;
const name = document.getElementById('register-name').value;
const password = document.getElementById('register-password').value;
const passwordConfirm = document.getElementById('register-password-confirm').value;
const messageDiv = document.getElementById('register-message');
if (password !== passwordConfirm) {
messageDiv.className = 'error';
messageDiv.textContent = 'Passwords do not match.';
return;
}
try {
const authData = await pb.collection('users').create({
email: email,
password: password,
passwordConfirm: passwordConfirm,
name: name
});
// Auto-login after registration
await pb.collection('users').authWithPassword(email, password);
messageDiv.className = 'success';
messageDiv.textContent = 'Registration successful!';
checkAuth();
} catch (e) {
messageDiv.className = 'error';
messageDiv.textContent = e.data?.message || 'Registration failed.';
}
}
function logout() {
pb.authStore.clear();
location.reload();
}
async function createPost() {
const title = document.getElementById('post-title').value;
const content = document.getElementById('post-content').value;
const messageDiv = document.getElementById('create-message');
if (!pb.authStore.isValid) {
messageDiv.className = 'error';
messageDiv.textContent = 'You must be logged in to create a post.';
return;
}
try {
const data = await pb.collection('posts').create({
title: title,
content: content,
author: pb.authStore.model.id,
status: 'published'
});
messageDiv.className = 'success';
messageDiv.textContent = 'Post created successfully!';
document.getElementById('post-title').value = '';
document.getElementById('post-content').value = '';
loadPosts();
} catch (e) {
messageDiv.className = 'error';
messageDiv.textContent = e.data?.message || 'Failed to create post.';
}
}
async function loadPosts() {
const postsList = document.getElementById('posts-list');
postsList.innerHTML = '<p>Loading...</p>';
try {
const records = await pb.collection('posts').getList(1, 50, {
sort: '-created',
expand: 'author'
});
if (records.items.length === 0) {
postsList.innerHTML = '<p>No posts yet. Be the first to create one!</p>';
return;
}
postsList.innerHTML = records.items.map(post => {
const author = post.expand?.author;
return `
<div class="post">
<h3>${escapeHtml(post.title)}</h3>
<p>${escapeHtml(post.content)}</p>
<small>By ${author?.name || 'Unknown'} on ${new Date(post.created).toLocaleString()}</small>
${post.author === pb.authStore.model?.id ? `
<div style="margin-top: 10px;">
<button onclick="editPost('${post.id}')" style="background: #28a745;">Edit</button>
<button onclick="deletePost('${post.id}')" style="background: #dc3545; margin-left: 5px;">Delete</button>
</div>
` : ''}
</div>
`;
}).join('');
} catch (e) {
postsList.innerHTML = `<p class="error">Failed to load posts: ${e.message}</p>`;
}
}
async function deletePost(id) {
if (!confirm('Are you sure you want to delete this post?')) {
return;
}
try {
await pb.collection('posts').delete(id);
loadPosts();
} catch (e) {
alert('Failed to delete post: ' + (e.data?.message || e.message));
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Set up real-time subscription (if supported)
pb.collection('posts').subscribe('*', function (e) {
console.log('Real-time event:', e);
// Reload posts when changes occur
loadPosts();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,56 @@
# Backups API
## Overview
PocketBase ships with a Backups API for full database snapshots. It is distinct from the per-collection import/export workflows described in [Data Migration Workflows](../../core/data_migration.md). Use backups for disaster recovery or environment cloning; use targeted migrations when you need fine-grained control over specific collections.
## List Backups
```http
GET /api/backups
Authorization: Bearer {admin_token}
```
## Create Backup
```http
POST /api/backups
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"name": "backup-2024-01-01"
}
```
## Download Backup
```http
GET /api/backups/{backupId}/download
Authorization: Bearer {admin_token}
```
## Upload Backup
```http
POST /api/backups/upload
Content-Type: multipart/form-data
Authorization: Bearer {admin_token}
file: backup.sql
```
## Restore Backup
```http
POST /api/backups/{backupId}/restore
Authorization: Bearer {admin_token}
```
### Best practices
- Schedule backups before and after running large data migrations.
- Store backups off the instance (object storage or encrypted volumes) and version them alongside schema migrations.
- To restore into a clean instance and then migrate selective collections, combine this API with the targeted tools documented in [Data Migration Workflows](../../core/data_migration.md).
See also [core/going_to_production.md](../core/going_to_production.md#backup-strategy) for operational guidance.

View File

@@ -0,0 +1,158 @@
# Collections API
## Overview
The Collections API allows you to programmatically manage PocketBase collections, including creating, updating, deleting, and configuring collections.
## List Collections
```http
GET /api/collections
Authorization: Bearer {admin_token}
```
Response:
```json
{
"page": 1,
"perPage": 30,
"totalItems": 3,
"totalPages": 1,
"items": [
{
"id": "_pbc_344172009",
"name": "users",
"type": "auth",
"system": false,
"fields": [
{
"name": "email",
"type": "email",
"required": true,
"options": {
"exceptDomains": null,
"onlyDomains": null
}
},
{
"name": "verified",
"type": "bool",
"required": false
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"created": "2024-01-01 12:00:00Z",
"updated": "2024-01-10 08:30:00Z"
}
]
}
```
## Get Single Collection
```http
GET /api/collections/{collectionId}
Authorization: Bearer {admin_token}
```
## Create Collection
```http
POST /api/collections
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"name": "products",
"type": "base",
"fields": [
{
"name": "title",
"type": "text",
"required": true,
"options": {
"min": 1,
"max": 200
}
},
{
"name": "price",
"type": "number",
"required": true,
"options": {
"min": 0
}
}
],
"indexes": [],
"listRule": "status = 'published'",
"viewRule": "status = 'published'",
"createRule": "@request.auth.id != ''",
"updateRule": "@request.auth.role = 'admin'",
"deleteRule": "@request.auth.role = 'admin'"
}
```
## Update Collection
```http
PATCH /api/collections/{collectionId}
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"name": "products",
"fields": [
{
"name": "title",
"type": "text",
"required": true
}
],
"indexes": [
"CREATE INDEX idx_products_title ON products (title)"
],
"listRule": "status = 'published'",
"updateRule": "@request.auth.role = 'admin'"
}
```
## Delete Collection
```http
DELETE /api/collections/{collectionId}
Authorization: Bearer {admin_token}
```
## Import Collections
```http
POST /api/collections/import
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"collections": [
{
"name": "posts",
"type": "base",
"fields": [
{
"name": "title",
"type": "text"
}
],
"listRule": "",
"viewRule": "",
"createRule": "@request.auth.id != ''"
}
]
}
For the full set of fields and options, refer to the [official API Collections reference](https://pocketbase.io/docs/api-collections/).
**Note:** This is a placeholder file. See [core/collections.md](../core/collections.md) for comprehensive collection documentation.

View File

@@ -0,0 +1,57 @@
# Crons API
## Overview
The Crons API manages background jobs and scheduled tasks.
## List Crons
```http
GET /api/crons
Authorization: Bearer {admin_token}
```
## Get Single Cron
```http
GET /api/crons/{cronId}
Authorization: Bearer {admin_token}
```
## Create Cron
```http
POST /api/crons
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"name": "daily-backup",
"query": "SELECT 1",
"cron": "0 2 * * *",
"schedule": "0 2 * * *"
}
```
## Update Cron
```http
PATCH /api/crons/{cronId}
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"cron": "0 3 * * *"
}
```
## Delete Cron
```http
DELETE /api/crons/{cronId}
Authorization: Bearer {admin_token}
```
---
**Note:** This is a placeholder file. See [go/jobs_scheduling.md](../go/go_jobs_scheduling.md) for background jobs.

View File

@@ -0,0 +1,58 @@
# Files API
## Overview
The Files API provides endpoints for file upload, download, thumbnail generation, and file management.
## File Upload
### Single File Upload
```http
POST /api/collections/{collection}/records/{recordId}/files/{field}
Content-Type: multipart/form-data
file: (binary)
```
### Multiple Files Upload
```http
POST /api/collections/{collection}/records/{recordId}/files/{field}
Content-Type: multipart/form-data
file: (binary)
file: (binary)
file: (binary)
```
## File URL Generation
### Get File URL
```javascript
const url = pb.files.getURL(record, fileName);
const thumbnailUrl = pb.files.getURL(record, fileName, { thumb: '300x300' });
```
### Signed URLs (Private Files)
```javascript
const signedUrl = pb.files.getURL(record, fileName, { expires: 3600 });
```
## Delete File
```http
DELETE /api/collections/{collection}/records/{recordId}/files/{field}
```
## Download File
```http
GET /api/files/{collectionId}/{recordId}/{fileName}
```
---
**Note:** This is a placeholder file. See [core/files_handling.md](../core/files_handling.md) for comprehensive file handling documentation.

View File

@@ -0,0 +1,68 @@
# Health API
## Overview
The Health API provides system health checks, metrics, and status information.
## Check Health Status
```http
GET /api/health
```
Response:
```json
{
"code": 200,
"data": {
"status": "ok",
"metrics": {
"clients": 5,
"requests": 1000,
"errors": 2
}
}
}
```
## Detailed Health Check
```http
GET /api/health/detailed
Authorization: Bearer {admin_token}
```
Response:
```json
{
"status": "ok",
"version": "0.20.0",
"uptime": 3600,
"database": {
"status": "ok",
"size": 1048576,
"connections": 5
},
"cache": {
"status": "ok",
"hits": 100,
"misses": 10
},
"metrics": {
"active_connections": 5,
"total_requests": 1000,
"error_rate": 0.02
}
}
```
## Metrics
```http
GET /api/metrics
Authorization: Bearer {admin_token}
```
---
**Note:** This is a placeholder file. See [core/going_to_production.md](../core/going_to_production.md#monitoring-and-logging) for monitoring best practices.

View File

@@ -0,0 +1,53 @@
# Logs API
## Overview
The Logs API provides access to application logs, including authentication logs, request logs, and custom logs.
## Get Request Logs
```http
GET /api/logs/requests?page=1&perPage=50&filter=created>="2024-01-01"
Authorization: Bearer {admin_token}
```
Response:
```json
{
"page": 1,
"perPage": 50,
"totalItems": 100,
"totalPages": 2,
"items": [
{
"id": "log_id",
"method": "GET",
"url": "/api/collections/posts/records",
"status": 200,
"duration": 15,
"remoteIP": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"referer": "",
"created": "2024-01-01T00:00:00.000Z"
}
]
}
```
## Get Auth Logs
```http
GET /api/logs/auth?page=1&perPage=50
Authorization: Bearer {admin_token}
```
## Get Raw Logs
```http
GET /api/logs?type=request&level=error&page=1&perPage=50
Authorization: Bearer {admin_token}
```
---
**Note:** This is a placeholder file. See [core/going_to_production.md](../core/going_to_production.md) for logging best practices.

View File

@@ -0,0 +1,648 @@
# Realtime API
## Overview
PocketBase provides real-time updates via WebSocket connections, allowing your application to receive instant notifications when data changes.
## Connection
### Automatic Connection (SDK)
```javascript
// SDK automatically manages WebSocket connection
const pb = new PocketBase('http://127.0.0.1:8090');
// Connection is established automatically
pb.realtime.connection.addListener('open', () => {
console.log('Connected to realtime');
});
pb.realtime.connection.addListener('close', () => {
console.log('Disconnected from realtime');
});
pb.realtime.connection.addListener('error', (error) => {
console.error('Realtime error:', error);
});
```
### Manual WebSocket Connection
```javascript
const ws = new WebSocket('ws://127.0.0.1:8090/api/realtime');
ws.onopen = function() {
console.log('WebSocket connected');
};
ws.onclose = function() {
console.log('WebSocket disconnected');
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Real-time event:', data);
};
```
## Subscriptions
### Subscribe to Collection
Listen to all changes in a collection:
```javascript
// Subscribe to all posts changes
pb.collection('posts').subscribe('*', function (e) {
console.log(e.action); // 'create', 'update', or 'delete'
console.log(e.record); // Changed record
if (e.action === 'create') {
// New post created
} else if (e.action === 'update') {
// Post updated
} else if (e.action === 'delete') {
// Post deleted
}
});
```
### Subscribe to Specific Record
Listen to changes for a specific record:
```javascript
// Subscribe to specific post
pb.collection('posts').subscribe('RECORD_ID', function (e) {
console.log('Post changed:', e.record);
});
// Multiple records
pb.collection('posts').subscribe('ID1', callback);
pb.collection('posts').subscribe('ID2', callback);
```
### Subscribe via Admin Client
```javascript
// Subscribe using admin client
pb.admin.onChange('records', 'posts', (action, record) => {
console.log(`${action} on posts:`, record);
});
```
## Event Object
### Create Event
```javascript
{
"action": "create",
"record": {
"id": "RECORD_ID",
"title": "New Post",
"content": "Hello",
"created": "2024-01-01T00:00:00.000Z",
"updated": "2024-01-01T00:00:00.000Z"
}
}
```
### Update Event
```javascript
{
"action": "update",
"record": {
"id": "RECORD_ID",
"title": "Updated Post",
"content": "Updated content",
"created": "2024-01-01T00:00:00.000Z",
"updated": "2024-01-01T12:00:00.000Z"
}
}
```
### Delete Event
```javascript
{
"action": "delete",
"record": {
"id": "RECORD_ID"
}
}
```
## Unsubscribing
### Unsubscribe from Specific Record
```javascript
// Unsubscribe from specific record
pb.collection('posts').unsubscribe('RECORD_ID');
// Or using the subscription object
const unsubscribe = pb.collection('posts').subscribe('*', callback);
unsubscribe(); // Stop listening
```
### Unsubscribe from Collection
```javascript
// Unsubscribe from all collection changes
pb.collection('posts').unsubscribe();
```
### Unsubscribe from All Collections
```javascript
// Stop all subscriptions
pb.collections.unsubscribe();
```
## Realtime with React
### Hook Example
```javascript
import { useEffect, useState } from 'react';
function useRealtime(collection, recordId, callback) {
useEffect(() => {
let unsubscribe;
if (recordId) {
// Subscribe to specific record
unsubscribe = pb.collection(collection).subscribe(recordId, callback);
} else {
// Subscribe to all collection changes
unsubscribe = pb.collection(collection).subscribe('*', callback);
}
return () => {
if (unsubscribe) {
unsubscribe();
}
};
}, [collection, recordId]);
}
// Usage
function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
// Load initial data
loadPosts();
// Subscribe to realtime updates
pb.collection('posts').subscribe('*', (e) => {
if (e.action === 'create') {
setPosts(prev => [e.record, ...prev]);
} else if (e.action === 'update') {
setPosts(prev => prev.map(p => p.id === e.record.id ? e.record : p));
} else if (e.action === 'delete') {
setPosts(prev => prev.filter(p => p.id !== e.record.id));
}
});
return () => {
pb.collection('posts').unsubscribe();
};
}, []);
async function loadPosts() {
const records = await pb.collection('posts').getList(1, 50);
setPosts(records.items);
}
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
```
### Optimized React Example
```javascript
import { useEffect, useState } from 'react';
function PostDetails({ postId }) {
const [post, setPost] = useState(null);
useEffect(() => {
if (!postId) return;
// Load initial data
loadPost();
// Subscribe to this specific post
const unsubscribe = pb.collection('posts').subscribe(postId, (e) => {
setPost(e.record);
});
return () => {
unsubscribe();
};
}, [postId]);
async function loadPost() {
const record = await pb.collection('posts').getOne(postId);
setPost(record);
}
if (!post) return <div>Loading...</div>;
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
```
## Realtime with Vue.js
```javascript
export default {
data() {
return {
posts: []
}
},
async mounted() {
await this.loadPosts();
// Subscribe to realtime updates
pb.collection('posts').subscribe('*', (e) => {
if (e.action === 'create') {
this.posts.unshift(e.record);
} else if (e.action === 'update') {
const index = this.posts.findIndex(p => p.id === e.record.id);
if (index !== -1) {
this.posts.splice(index, 1, e.record);
}
} else if (e.action === 'delete') {
this.posts = this.posts.filter(p => p.id !== e.record.id);
}
});
},
beforeUnmount() {
pb.collection('posts').unsubscribe();
},
methods: {
async loadPosts() {
const records = await pb.collection('posts').getList(1, 50);
this.posts = records.items;
}
}
}
```
## Realtime with Vanilla JavaScript
```javascript
const postsList = document.getElementById('posts');
async function loadPosts() {
const response = await pb.collection('posts').getList(1, 50);
renderPosts(response.items);
}
function renderPosts(posts) {
postsList.innerHTML = posts.map(post => `
<div class="post">
<h3>${post.title}</h3>
<p>${post.content}</p>
</div>
`).join('');
}
// Subscribe to realtime updates
pb.collection('posts').subscribe('*', (e) => {
if (e.action === 'create') {
prependPost(e.record);
} else if (e.action === 'update') {
updatePost(e.record);
} else if (e.action === 'delete') {
removePost(e.record.id);
}
});
function prependPost(post) {
const div = document.createElement('div');
div.className = 'post';
div.innerHTML = `<h3>${post.title}</h3><p>${post.content}</p>`;
postsList.prepend(div);
}
// Initialize
loadPosts();
```
## Use Cases
### 1. Live Chat
```javascript
// Subscribe to messages
pb.collection('messages').subscribe('*', (e) => {
if (e.action === 'create') {
addMessageToUI(e.record);
}
});
// Send message
async function sendMessage(content) {
await pb.collection('messages').create({
content: content,
user: pb.authStore.model.id,
room: roomId
});
}
```
### 2. Notification System
```javascript
// Subscribe to notifications
pb.collection('notifications').subscribe('*', (e) => {
if (e.action === 'create' && e.record.user_id === pb.authStore.model.id) {
showNotification(e.record.message);
updateBadge();
}
});
```
### 3. Collaborative Editing
```javascript
// Subscribe to document changes
pb.collection('documents').subscribe('DOCUMENT_ID', (e) => {
if (e.action === 'update') {
updateEditor(e.record.content);
}
});
// Debounce updates
let updateTimeout;
function onEditorChange(content) {
clearTimeout(updateTimeout);
updateTimeout = setTimeout(async () => {
await pb.collection('documents').update('DOCUMENT_ID', {
content: content
});
}, 500);
}
```
### 4. Live Dashboard
```javascript
// Subscribe to metrics changes
pb.collection('metrics').subscribe('*', (e) => {
if (e.action === 'update') {
updateDashboard(e.record);
}
});
// Subscribe to events
pb.collection('events').subscribe('*', (e) => {
if (e.action === 'create') {
addEventToFeed(e.record);
}
});
```
### 5. Shopping Cart Updates
```javascript
// Subscribe to cart changes
pb.collection('cart_items').subscribe('*', (e) => {
if (e.action === 'create' && e.record.user_id === pb.authStore.model.id) {
updateCartCount();
} else if (e.action === 'delete') {
updateCartCount();
}
});
```
## Authentication and Realtime
### Authenticated Subscriptions
```javascript
// Subscribe only after authentication
pb.collection('users').authWithPassword('email', 'password').then(() => {
// Now subscribe to private data
pb.collection('messages').subscribe('*', (e) => {
// Will only receive messages user has access to
});
});
```
### Multiple User Types
```javascript
// Different subscriptions based on role
if (pb.authStore.model.role === 'admin') {
// Admin sees all updates
pb.collection('posts').subscribe('*', handleAdminUpdate);
} else {
// Regular users see limited updates
pb.collection('posts').subscribe('*', handleUserUpdate);
}
```
## Filtering Realtime Events
```javascript
// Client-side filtering
pb.collection('posts').subscribe('*', (e) => {
// Only show published posts
if (e.record.status === 'published') {
updateUI(e.record);
}
});
// Or use server-side rules (better)
```
## Performance Considerations
### 1. Limit Subscriptions
```javascript
// Good - subscribe to specific records needed
pb.collection('posts').subscribe('POST_ID', callback);
// Bad - subscribe to everything
pb.collection('posts').subscribe('*', callback); // Only when necessary
```
### 2. Unsubscribe When Done
```javascript
useEffect(() => {
const unsubscribe = pb.collection('posts').subscribe('*', callback);
return () => {
unsubscribe(); // Clean up
};
}, []);
```
### 3. Batch UI Updates
```javascript
// Instead of updating on every event
pb.collection('posts').subscribe('*', (e) => {
updateUI(e.record); // Triggers re-render every time
});
// Batch updates
const updates = [];
pb.collection('posts').subscribe('*', (e) => {
updates.push(e.record);
if (updates.length >= 10) {
batchUpdateUI(updates);
updates.length = 0;
}
});
```
### 4. Use Debouncing for Frequent Updates
```javascript
let updateTimeout;
pb.collection('metrics').subscribe('*', (e) => {
clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => {
updateDashboard();
}, 100); // Update at most every 100ms
});
```
## Connection Management
### Reconnection Strategy
```javascript
pb.realtime.connection.addListener('close', () => {
// Attempt reconnection
setTimeout(() => {
pb.realtime.connect();
}, 5000); // Reconnect after 5 seconds
});
```
### Manual Connection Control
```javascript
// Disconnect
pb.realtime.disconnect();
// Reconnect
pb.realtime.connect();
// Check connection status
const isConnected = pb.realtime.connection.isOpen;
```
### Heartbeat
```javascript
// Keep connection alive
setInterval(() => {
if (pb.realtime.connection.isOpen) {
pb.realtime.send({ action: 'ping' });
}
}, 30000); // Every 30 seconds
```
## Error Handling
```javascript
pb.collection('posts').subscribe('*', (e) => {
try {
handleEvent(e);
} catch (error) {
console.error('Error handling event:', error);
// Don't let errors break the subscription
}
});
// Handle connection errors
pb.realtime.connection.addListener('error', (error) => {
console.error('Realtime connection error:', error);
// Show error to user or attempt reconnection
});
```
## Security
### Server-Side Security
Realtime events respect collection rules:
```javascript
// Users will only receive events for records they can access
// No need for additional client-side filtering based on permissions
```
### Client-Side Validation
```javascript
pb.collection('posts').subscribe('*', (e) => {
// Validate event data
if (!e.record || !e.action) {
console.warn('Invalid event:', e);
return;
}
// Process event
handleEvent(e);
});
```
## Troubleshooting
**Not receiving events**
- Check if subscribed to correct collection
- Verify user is authenticated
- Check console for errors
- Ensure WebSocket connection is open
**Receiving too many events**
- Unsubscribe from unnecessary subscriptions
- Filter events client-side
- Use more specific subscriptions
**Memory leaks**
- Always unsubscribe in component cleanup
- Check for duplicate subscriptions
- Use useEffect cleanup function
**Disconnections**
- Implement reconnection logic
- Add heartbeat/ping
- Show connection status to user
## Related Topics
- [Records API](api_records.md) - CRUD operations
- [API Rules & Filters](../core/api_rules_filters.md) - Security
- [Collections](../core/collections.md) - Collection setup

View File

@@ -0,0 +1,637 @@
# Records API
## Overview
The Records API provides CRUD operations for collection records. It handles:
- Creating records
- Reading records (single or list)
- Updating records
- Deleting records
- Batch operations
- Real-time subscriptions
## Authentication
Most record operations require authentication:
```javascript
// Include JWT token in requests
const token = pb.authStore.token;
// Or use SDK which handles it automatically
const pb = new PocketBase('http://127.0.0.1:8090');
```
## Create Record
### Create Single Record
```javascript
const record = await pb.collection('posts').create({
title: 'My Post',
content: 'Hello world!',
author: pb.authStore.model.id,
published: true
});
// Returns full record with ID, timestamps, etc.
console.log(record.id);
console.log(record.created);
console.log(record.updated);
```
### Create with Files
```javascript
const formData = new FormData();
formData.append('title', 'Post with Image');
formData.append('image', fileInput.files[0]);
const record = await pb.collection('posts').create(formData);
const imageUrl = pb.files.getURL(record, record.image);
```
### Create with Relations
```javascript
const post = await pb.collection('posts').create({
title: 'My Post',
author: authorId, // User ID
category: categoryId // Category ID
});
```
## Read Records
### Get Single Record
```javascript
const record = await pb.collection('posts').getOne('RECORD_ID');
// Get with expand
const record = await pb.collection('posts').getOne('RECORD_ID', {
expand: 'author,comments'
});
```
### Get Multiple Records (List)
```javascript
const records = await pb.collection('posts').getList(1, 50);
// Returns:
// {
// page: 1,
// perPage: 50,
// totalItems: 100,
// totalPages: 2,
// items: [ ... array of records ... ]
// }
```
### Pagination
```javascript
// Page 1
const page1 = await pb.collection('posts').getList(1, 50);
// Page 2
const page2 = await pb.collection('posts').getList(2, 50);
// Large perPage
const all = await pb.collection('posts').getList(1, 200);
// Get all records (use carefully)
const allRecords = await pb.collection('posts').getFullList();
```
### Filtering
```javascript
const records = await pb.collection('posts').getList(1, 50, {
filter: 'status = "published"'
});
// Multiple conditions
const records = await pb.collection('posts').getList(1, 50, {
filter: 'status = "published" && created >= "2024-01-01"'
});
// With OR
const records = await pb.collection('posts').getList(1, 50, {
filter: 'category = "tech" || category = "programming"'
});
// By relation field
const records = await pb.collection('comments').getList(1, 50, {
filter: 'expand.post.title ~ "PocketBase"'
});
```
### Sorting
```javascript
// Sort by created date descending
const records = await pb.collection('posts').getList(1, 50, {
sort: '-created'
});
// Sort by title ascending
const records = await pb.collection('posts').getList(1, 50, {
sort: 'title'
});
// Multiple fields
const records = await pb.collection('posts').getList(1, 50, {
sort: 'status,-created' // status ascending, then created descending
});
```
### Field Selection
```javascript
// Select specific fields
const records = await pb.collection('posts').getList(1, 50, {
fields: 'id,title,author,created'
});
// Exclude large fields
const records = await pb.collection('posts').getList(1, 50, {
fields: 'id,title,author,created,-content'
});
// Select with expand
const records = await pb.collection('posts').getList(1, 50, {
fields: 'id,title,expand.author.name'
});
```
### Relation Expansion
```javascript
// Expand single relation
const posts = await pb.collection('posts').getList(1, 50, {
expand: 'author'
});
// Expand multiple relations
const posts = await pb.collection('posts').getList(1, 50, {
expand: 'author,comments'
});
// Expand nested relations
const comments = await pb.collection('comments').getList(1, 50, {
expand: 'post.author'
});
// Use expand in filters
const posts = await pb.collection('posts').getList(1, 50, {
expand: 'author',
filter: 'expand.author.role = "admin"'
});
```
### Cursor-Based Pagination (PocketBase 0.20+)
```javascript
// First page
const page1 = await pb.collection('posts').getList(1, 50, {
sort: 'created'
});
// Get cursor (last item's sort value)
const cursor = page1.items[page1.items.length - 1].created;
// Next page
const page2 = await pb.collection('posts').getList(1, 50, {
filter: `created < "${cursor}"`,
sort: 'created'
});
```
## Update Record
### Update Single Record
```javascript
const updated = await pb.collection('posts').update('RECORD_ID', {
title: 'Updated Title',
status: 'published'
});
// Returns updated record
console.log(updated.title);
console.log(updated.updated);
```
### Update with Files
```javascript
const formData = new FormData();
formData.append('title', 'Updated Post');
formData.append('image', newFile); // Replace image
// or
formData.append('image', null); // Remove image
const updated = await pb.collection('posts').update('RECORD_ID', formData);
```
### Update Relations
```javascript
// Update relation
const updated = await pb.collection('posts').update('RECORD_ID', {
author: newAuthorId
});
// Add to one-to-many relation
const comment = await pb.collection('comments').create({
post: postId,
content: 'New comment'
});
// Update comment
await pb.collection('comments').update(comment.id, {
content: 'Updated comment'
});
```
## Delete Record
```javascript
// Delete single record
await pb.collection('posts').delete('RECORD_ID');
// Returns true on success, throws on failure
```
## Batch Operations
### Create Multiple Records
```javascript
const records = await pb.collection('posts').createBatch([
{
title: 'Post 1',
content: 'Content 1'
},
{
title: 'Post 2',
content: 'Content 2'
}
]);
console.log(records.length); // 2
```
### Update Multiple Records
```javascript
const records = await pb.collection('posts').updateBatch([
{
id: 'RECORD_ID_1',
title: 'Updated Title 1'
},
{
id: 'RECORD_ID_2',
title: 'Updated Title 2'
}
]);
```
### Delete Multiple Records
```javascript
await pb.collection('posts').deleteBatch([
'RECORD_ID_1',
'RECORD_ID_2',
'RECORD_ID_3'
]);
```
## Real-time Subscriptions
### Subscribe to All Collection Changes
```javascript
// Subscribe to all changes
pb.collection('posts').subscribe('*', function (e) {
console.log(e.action); // 'create', 'update', or 'delete'
console.log(e.record); // Changed record
if (e.action === 'create') {
console.log('New post created:', e.record);
} else if (e.action === 'update') {
console.log('Post updated:', e.record);
} else if (e.action === 'delete') {
console.log('Post deleted:', e.record);
}
});
```
### Subscribe to Specific Record
```javascript
// Subscribe to specific record
pb.collection('posts').subscribe('RECORD_ID', function (e) {
console.log('Record changed:', e.record);
});
```
### Unsubscribe
```javascript
// Unsubscribe from specific record
pb.collection('posts').unsubscribe('RECORD_ID');
// Unsubscribe from all collection changes
pb.collection('posts').unsubscribe();
// Unsubscribe from all collections
pb.collections.unsubscribe();
```
### Real-time with React
```javascript
import { useEffect, useState } from 'react';
function PostsList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
loadPosts();
// Subscribe to real-time updates
pb.collection('posts').subscribe('*', function (e) {
if (e.action === 'create') {
setPosts(prev => [e.record, ...prev]);
} else if (e.action === 'update') {
setPosts(prev => prev.map(p => p.id === e.record.id ? e.record : p));
} else if (e.action === 'delete') {
setPosts(prev => prev.filter(p => p.id !== e.record.id));
}
});
return () => {
pb.collection('posts').unsubscribe();
};
}, []);
async function loadPosts() {
const records = await pb.collection('posts').getList(1, 50, {
expand: 'author'
});
setPosts(records.items);
}
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>By {post.expand?.author?.name}</p>
</div>
))}
</div>
);
}
```
## Error Handling
```javascript
try {
const record = await pb.collection('posts').getOne('INVALID_ID');
} catch (error) {
console.error('Error:', error.message);
// Handle specific errors
if (error.status === 404) {
console.log('Record not found');
} else if (error.status === 403) {
console.log('Access denied');
}
}
```
### Common Error Codes
- `400` - Bad Request (validation error)
- `403` - Forbidden (access denied)
- `404` - Not Found (record doesn't exist)
- `422` - Unprocessable Entity (validation failed)
## REST API Reference
### Direct HTTP Requests
```javascript
// Create
fetch('http://127.0.0.1:8090/api/collections/posts/records', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${pb.authStore.token}`
},
body: JSON.stringify({
title: 'My Post',
content: 'Hello world!'
})
});
// Read
fetch('http://127.0.0.1:8090/api/collections/posts/records/RECORD_ID', {
headers: {
'Authorization': `Bearer ${pb.authStore.token}`
}
});
// Update
fetch('http://127.0.0.1:8090/api/collections/posts/records/RECORD_ID', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${pb.authStore.token}`
},
body: JSON.stringify({
title: 'Updated Title'
})
});
// Delete
fetch('http://127.0.0.1:8090/api/collections/posts/records/RECORD_ID', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${pb.authStore.token}`
}
});
// List
fetch('http://127.0.0.1:8090/api/collections/posts/records?page=1&perPage=50', {
headers: {
'Authorization': `Bearer ${pb.authStore.token}`
}
});
```
### Query Parameters for List
```
GET /api/collections/{collection}/records
Query Parameters:
- page : Page number (default: 1)
- perPage : Items per page (default: 50, max: 500)
- filter : Filter expression
- sort : Sort expression
- fields : Fields to return
- expand : Relations to expand
- skip : Number of records to skip (alternative to cursor)
```
### Filtering Examples
```javascript
// Via SDK
const records = await pb.collection('posts').getList(1, 50, {
filter: 'status = "published" && views > 100'
});
// Via REST
fetch('http://127.0.0.1:8090/api/collections/posts/records?filter=(status="published" && views>100)')
```
### Sorting Examples
```javascript
// Via SDK
const records = await pb.collection('posts').getList(1, 50, {
sort: '-created,title'
});
// Via REST
fetch('http://127.0.0.1:8090/api/collections/posts/records?sort=-created,title')
```
## Performance Tips
### 1. Use Pagination
```javascript
// Instead of getting all records
const all = await pb.collection('posts').getFullList(1000);
// Use pagination
let page = 1;
let allRecords = [];
while (true) {
const records = await pb.collection('posts').getList(page, 50);
allRecords = allRecords.concat(records.items);
if (page >= records.totalPages) break;
page++;
}
```
### 2. Select Only Needed Fields
```javascript
// Instead of fetching everything
const posts = await pb.collection('posts').getList(1, 50);
// Select only needed fields
const posts = await pb.collection('posts').getList(1, 50, {
fields: 'id,title,author,created'
});
```
### 3. Use Filters Efficiently
```javascript
// Good - uses indexes
const posts = await pb.collection('posts').getList(1, 50, {
filter: 'status = "published" && created >= "2024-01-01"'
});
// Slow - can't use indexes well
const posts = await pb.collection('posts').getList(1, 50, {
filter: 'title ~ ".*pattern.*"'
});
```
### 4. Limit Expand Depth
```javascript
// Good - limit to 2 levels
const posts = await pb.collection('posts').getList(1, 50, {
expand: 'author,comments'
});
// Slower - 3 levels
const posts = await pb.collection('posts').getList(1, 50, {
expand: 'author,comments,comments.author'
});
```
### 5. Use Batch Operations
```javascript
// Instead of multiple requests
await pb.collection('posts').create({ title: 'Post 1' });
await pb.collection('posts').create({ title: 'Post 2' });
await pb.collection('posts').create({ title: 'Post 3' });
// Use batch
await pb.collection('posts').createBatch([
{ title: 'Post 1' },
{ title: 'Post 2' },
{ title: 'Post 3' }
]);
```
## WebSocket Connections
### Manual WebSocket Connection
```javascript
const ws = new WebSocket('ws://127.0.0.1:8090/api/realtime');
ws.onopen = function() {
// Subscribe to collection
ws.send(JSON.stringify({
action: 'subscribe',
collection: 'posts'
}));
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Real-time update:', data);
};
```
### Connection Status
```javascript
pb.realtime.connection.addListener('open', () => {
console.log('Realtime connected');
});
pb.realtime.connection.addListener('close', () => {
console.log('Realtime disconnected');
});
pb.realtime.connection.addListener('error', (error) => {
console.log('Realtime error:', error);
});
```
## Related Topics
- [Collections](../core/collections.md) - Collection configuration
- [API Rules & Filters](../core/api_rules_filters.md) - Security and filtering
- [Authentication](../core/authentication.md) - User authentication
- [Working with Relations](../core/working_with_relations.md) - Relations
- [Real-time API](api_realtime.md) - WebSocket subscriptions
- [Files API](api_files.md) - File uploads

View File

@@ -0,0 +1,97 @@
# Settings API
## Overview
The Settings API allows you to manage PocketBase application settings including app configuration, CORS, SMTP, admin accounts, and more.
## Get All Settings
```http
GET /api/settings
Authorization: Bearer {admin_token}
```
Response:
```json
{
"appName": "My App",
"appUrl": "http://localhost:8090",
"hideControls": false,
"pageDirection": "ltr",
"default.lang": "en",
"smtp": {
"enabled": false,
"host": "",
"port": 587,
"username": "",
"password": "",
"tls": true,
"fromEmail": "",
"fromName": ""
},
"cors": {
"enabled": true,
"allowedOrigins": ["http://localhost:3000"],
"allowedMethods": ["GET", "POST", "PUT", "PATCH", "DELETE"],
"allowedHeaders": ["Content-Type", "Authorization"]
},
"auth": {
"passwordMinLength": 8,
"passwordUppercase": false,
"passwordLowercase": false,
"passwordNumbers": false,
"passwordSymbols": false,
"requireEmailVerification": true,
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": false,
"onlyEmailDomains": [],
"exceptEmailDomains": [],
"manageAccounts": false
}
}
```
## Update Settings
```http
PATCH /api/settings
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"appName": "My App",
"appUrl": "https://myapp.com",
"cors": {
"allowedOrigins": ["https://myapp.com", "https://admin.myapp.com"]
},
"smtp": {
"enabled": true,
"host": "smtp.gmail.com",
"port": 587,
"username": "noreply@myapp.com",
"password": "password",
"tls": true,
"fromEmail": "noreply@myapp.com",
"fromName": "My App"
}
}
```
## Test SMTP Configuration
```http
POST /api/settings/test/smtp
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"to": "test@example.com",
"subject": "Test Email",
"html": "<p>This is a test email</p>"
}
```
---
**Note:** This is a placeholder file. See [core/going_to_production.md](../core/going_to_production.md) for configuration guidance.

View File

@@ -0,0 +1,396 @@
# PocketBase API Reference
Comprehensive guide to working with PocketBase APIs, SDKs, and common patterns.
## Table of Contents
1. [Installation & Setup](#installation--setup)
2. [JavaScript SDK](#javascript-sdk)
3. [REST API](#rest-api)
4. [Authentication](#authentication)
5. [CRUD Operations](#crud-operations)
6. [File Uploads](#file-uploads)
7. [Real-time Subscriptions](#real-time-subscriptions)
8. [Error Handling](#error-handling)
## Installation & Setup
### JavaScript SDK (Browser)
```html
<script src="https://unpkg.com/pocketbase@latest/dist/pocketbase.umd.js"></script>
<script>
const pb = new PocketBase('http://127.0.0.1:8090');
</script>
```
### JavaScript SDK (ESM)
```bash
npm install pocketbase
```
```javascript
import PocketBase from 'pocketbase'
const pb = new PocketBase('http://127.0.0.1:8090')
```
### Python SDK
```bash
pip install pocketbase
```
```python
from pocketbase import PocketBase
pb = PocketBase('http://127.0.0.1:8090')
```
## JavaScript SDK
### Initialize Client
```javascript
import PocketBase from 'pocketbase'
// Browser or ESM
const pb = new PocketBase('http://127.0.0.1:8090')
// Auto-cancel previous requests when new one is fired
pb.autoCancellation(false)
```
### Authentication
**Register User**
```javascript
const authData = await pb.collection('users').create({
email: 'test@example.com',
password: '123456789',
passwordConfirm: '123456789',
name: 'John Doe'
})
// Or with additional profile fields
const authData = await pb.collection('users').create({
email: 'test@example.com',
password: '123456789',
passwordConfirm: '123456789',
name: 'John Doe',
avatar: fileData // File instance
})
```
**Login**
```javascript
const authData = await pb.collection('users').authWithPassword(
'test@example.com',
'123456789'
)
// Access auth fields
console.log(authData.user.email)
console.log(authData.token) // JWT access token
```
**Login with OAuth2 (Google, GitHub, etc.)**
```javascript
const authData = await pb.collection('users').authWithOAuth2({
provider: 'google',
code: 'oa2-code-from-provider'
})
```
**Current Authenticated User**
```javascript
// Get current user
const user = pb.authStore.model
// Check if authenticated
if (pb.authStore.isValid) {
// User is authenticated
}
// Refresh current user
const user = await pb.collection('users').authRefresh()
// Logout
pb.authStore.clear()
```
### CRUD Operations
**Create Record**
```javascript
const record = await pb.collection('posts').create({
title: 'My First Post',
content: 'Hello world!',
author: pb.authStore.model.id // Link to current user
})
```
**Get Single Record**
```javascript
const record = await pb.collection('posts').getOne('RECORD_ID')
// With expand
const record = await pb.collection('posts').getOne('RECORD_ID', {
expand: 'author'
})
console.log(record.expand?.email) // If author is a relation
```
**Get Multiple Records (List)**
```javascript
// Basic list
const records = await pb.collection('posts').getList(1, 50)
// With filtering, sorting, and expansion
const records = await pb.collection('posts').getList(1, 50, {
filter: 'status = "published"',
sort: '-created',
expand: 'author,comments',
fields: 'id,title,author,created'
})
```
**Update Record**
```javascript
const updated = await pb.collection('posts').update('RECORD_ID', {
title: 'Updated Title'
})
```
**Delete Record**
```javascript
await pb.collection('posts').delete('RECORD_ID')
```
### Filtering & Querying
**Filter Examples**
```javascript
// Basic equality
const records = await pb.collection('posts').getList(1, 50, {
filter: 'status = "published"'
})
// Multiple conditions
const records = await pb.collection('posts').getList(1, 50, {
filter: 'status = "published" && created >= "2024-01-01"'
})
// Regex
const records = await pb.collection('posts').getList(1, 50, {
filter: 'title ~ "Hello"'
})
// In array
const records = await pb.collection('posts').getList(1, 50, {
filter: 'categoryId ~ ["tech", "coding"]'
})
// Null check
const records = await pb.collection('posts').getList(1, 50, {
filter: 'published != null'
})
```
**Sorting**
```javascript
// Sort by single field
const records = await pb.collection('posts').getList(1, 50, {
sort: 'created'
})
// Sort by multiple fields
const records = await pb.collection('posts').getList(1, 50, {
sort: 'status,-created'
})
```
### File Uploads
**Upload File**
```javascript
const formData = new FormData()
formData.append('avatar', fileInput.files[0])
const updated = await pb.collection('users').update('RECORD_ID', formData)
```
**Get File URL**
```javascript
const url = pb.files.getURL(record, record.avatar)
```
**Download File**
```javascript
const blob = await pb.files.download(record, record.fileField)
```
### Real-time Subscriptions
**Subscribe to Collection**
```javascript
// Listen to all record changes in a collection
pb.collection('posts').subscribe('*', function (e) {
console.log(e.action) // 'create', 'update', or 'delete'
console.log(e.record) // The changed record
})
```
**Subscribe to Specific Record**
```javascript
// Listen to changes for a specific record
pb.collection('posts').subscribe('RECORD_ID', function (e) {
console.log('Record changed:', e.record)
})
```
**Unsubscribe**
```javascript
// Unsubscribe from specific record
pb.collection('posts').unsubscribe('RECORD_ID')
// Unsubscribe from all posts collection
pb.collection('posts').unsubscribe()
// Unsubscribe from all collections
pb.collections.unsubscribe()
```
### Batch Operations
**Create Multiple Records**
```javascript
const promises = records.map(record => {
return pb.collection('posts').create({
title: record.title,
content: record.content
})
})
const results = await Promise.all(promises)
```
## REST API
### Direct HTTP Requests
**Get Records**
```bash
curl -X GET "http://127.0.0.1:8090/api/collections/posts/records?page=1&perPage=50" \
-H "Authorization: Bearer JWT_TOKEN"
```
**Create Record**
```bash
curl -X POST "http://127.0.0.1:8090/api/collections/posts/records" \
-H "Authorization: Bearer JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "My Post",
"content": "Content here"
}'
```
**Update Record**
```bash
curl -X PATCH "http://127.0.0.1:8090/api/collections/posts/records/RECORD_ID" \
-H "Authorization: Bearer JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title"}'
```
**Delete Record**
```bash
curl -X DELETE "http://127.0.0.1:8090/api/collections/posts/records/RECORD_ID" \
-H "Authorization: Bearer JWT_TOKEN"
```
### File Upload via REST API
```bash
curl -X POST "http://127.0.0.1:8090/api/collections/users/records/RECORD_ID" \
-H "Authorization: Bearer JWT_TOKEN" \
-F 'avatar=@/path/to/file.jpg'
```
## Error Handling
### SDK Error Handling
```javascript
try {
const record = await pb.collection('posts').getOne('INVALID_ID')
} catch (e) {
// 404: Record not found
if (e.status === 404) {
console.log('Record not found')
}
// 403: Permission denied
if (e.status === 403) {
console.log('You do not have permission to access this record')
}
// 400: Validation error
if (e.status === 400) {
console.log('Validation error:', e.data)
}
console.error('Error:', e.message)
}
```
### Common HTTP Status Codes
- **200/201**: Success
- **400**: Bad Request (validation error)
- **401**: Unauthorized (not logged in)
- **403**: Forbidden (permission denied)
- **404**: Not Found
- **500**: Internal Server Error
### Validation Errors
```javascript
try {
await pb.collection('users').create({
email: 'invalid-email', // Will fail validation
password: '123' // Too short
})
} catch (e) {
console.log(e.data) // { email: ['Invalid email'], password: ['Too short'] }
}
```
## Security Considerations
### CORS Configuration
Configure CORS in PocketBase settings to allow specific origins:
```javascript
// In admin UI > Settings > CORS
// Add your frontend origin (e.g., http://localhost:3000)
```
### Environment Variables
```javascript
// Production configuration
const pb = new PocketBase('https://your-production-url.com')
// Enable auto-cancellation in production
pb.autoCancellation(true)
```
### Rate Limiting
PocketBase includes built-in rate limiting. For custom rate limiting, add it in your application logic or use a reverse proxy.
## Best Practices
1. **Always use HTTPS in production**
2. **Validate data on both client and server**
3. **Use proper CORS configuration**
4. **Implement row-level security rules**
5. **Use pagination for large datasets**
6. **Cache frequent queries on the client**
7. **Unsubscribe from real-time events when no longer needed**
8. **Use file size and type validation**
9. **Implement proper error boundaries**
10. **Log security events and authentication failures**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
# go_collections - Go Extensions
## Overview
This file covers go_collections in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,85 @@
# go_console_commands - Go Extensions
## Overview
PocketBase exposes Cobra-based console commands that you can extend from either Go extensions or JavaScript `pb_hooks`. Use them for background jobs, data migrations, administrative helpers, or build-time automation. Review [`go_overview.md`](go_overview.md) for extension setup and wiring custom commands into the root CLI.
---
## Data Migration Commands
Import/export workflows benefit from running inside PocketBase where you can wrap operations in transactions, reuse the ORM, and uphold access rules. At a minimum:
1. Register a command on `$app.RootCmd()` (Go) or `$app.rootCmd` (JS hooks).
2. Accept flags for batch size, dry-run execution, and optional upsert keys.
3. Wrap writes in `app.RunInTransaction(...)` when consistency matters.
4. Use `app.FindCollectionByNameOrId` and `core.NewRecord` (Go) or `new Record(collection)` (JS) to construct records.
5. Stream large payloads in chunks to keep memory stable and close files promptly.
### Go skeleton
```go
package migrations
import (
"encoding/json"
"os"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/spf13/cobra"
)
func Register(app *pocketbase.PocketBase) {
var batchSize int
cmd := &cobra.Command{
Use: "data:import <collection> <file>",
Short: "Import records from a JSON file",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
collectionName, filePath := args[0], args[1]
payload, err := os.ReadFile(filePath)
if err != nil {
return err
}
var rows []map[string]any
if err := json.Unmarshal(payload, &rows); err != nil {
return err
}
return app.RunInTransaction(func(txApp core.App) error {
collection, err := txApp.FindCollectionByNameOrId(collectionName)
if err != nil {
return err
}
for idx, row := range rows {
record := core.NewRecord(collection)
record.Load(row)
if err := txApp.Save(record); err != nil {
return err
}
if batchSize > 0 && (idx+1)%batchSize == 0 {
cmd.Printf("Imported %d records\n", idx+1)
}
}
return nil
})
},
}
cmd.Flags().IntVar(&batchSize, "batch", 500, "Records per transaction chunk")
app.RootCmd().AddCommand(cmd)
}
```
Adapt the skeleton with streaming readers, upsert logic, or batch logging as needed. For a JavaScript equivalent, see the example in [Data Migration Workflows](../core/data_migration.md#option-2-custom-cli-commands).
---
## Automation Notes
- Prefer purpose-built commands (`data:import`, `data:export`) to avoid confusion with the built-in `migrate` schema command.
- Add validation flags (e.g., `--dry-run`, `--allow-save-no-validate`) to make commands safer in production.
- Consider registering complementary commands for exporting manifests, truncating collections, or rehydrating relation fields.
- If you need to expose the same logic via HTTP, see [`go_routing.md`](go_routing.md) for admin-only endpoints that invoke the same import/export routines under the hood.

View File

@@ -0,0 +1,130 @@
# Database Operations - Go Extensions
## Overview
PocketBase provides a powerful database API for Go extensions, allowing you to perform complex queries, transactions, and data operations.
## Basic Queries
### Find Records
```go
import "github.com/pocketbase/dbx"
// Find single record
record, err := app.FindRecordById("posts", "RECORD_ID")
// Find multiple records with filter
records, err := app.FindRecordsByFilter(
"posts",
"status = {:status}",
"-created",
50,
0,
dbx.Params{"status": "published"},
)
// Fetch all matching records without pagination
records, err = app.FindRecordsByFilter(
"posts",
"status = {:status}",
"-created",
0,
0,
dbx.Params{"status": "published"},
)
```
### Query Builder
```go
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
records := []*core.Record{}
err := app.RecordQuery("posts").
AndWhere(dbx.HashExp{"status": "published"}).
AndWhere(dbx.NewExp("created >= {:date}", dbx.Params{"date": "2024-01-01"})).
AndWhere(dbx.Or(
dbx.HashExp{"author": userId},
dbx.HashExp{"featured": true},
)).
OrderBy("created DESC").
Offset(0).
Limit(50).
All(&records)
```
### Transactions
```go
import "github.com/pocketbase/pocketbase/core"
err := app.RunInTransaction(func(txApp core.App) error {
collection, err := txApp.FindCollectionByNameOrId("posts")
if err != nil {
return err
}
post := core.NewRecord(collection)
post.Set("title", "New Post")
if err := txApp.Save(post); err != nil {
return err
}
commentsCol, err := txApp.FindCollectionByNameOrId("comments")
if err != nil {
return err
}
comment := core.NewRecord(commentsCol)
comment.Set("post", post.Id)
comment.Set("content", "First comment")
return txApp.Save(comment)
})
```
### Bulk import pattern
```go
import (
"encoding/json"
"os"
)
func importFile(app core.App, collectionName, path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
var rows []map[string]any
if err := json.Unmarshal(data, &rows); err != nil {
return err
}
return app.RunInTransaction(func(tx core.App) error {
col, err := tx.FindCollectionByNameOrId(collectionName)
if err != nil {
return err
}
for _, row := range rows {
rec := core.NewRecord(col)
rec.Load(row)
if err := tx.Save(rec); err != nil {
return err
}
}
return nil
})
}
```
- Split large imports into chunks to keep memory usage predictable.
- Prefer `RunInTransaction` for atomicity; if you intentionally bypass validation use `SaveNoValidate` after cleaning the data.
- Coordinate the schema setup with migrations—see [`go_migrations.md`](go_migrations.md) and [Data Migration Workflows](../core/data_migration.md).
---
**Note:** See [go_overview.md](go_overview.md) and the [official database guide](https://pocketbase.io/docs/go-database/) for comprehensive coverage.

View File

@@ -0,0 +1,104 @@
# Event Hooks - Go Extensions
## Overview
Event hooks allow you to execute custom logic when specific events occur in PocketBase, such as record creation, updates, authentication, or API requests.
## Hook Types
### Record Hooks
- `OnRecordCreate()` - Before/after record creation
- `OnRecordUpdate()` - Before/after record updates
- `OnRecordDelete()` - Before/after record deletion
- `OnRecordList()` - Before/after listing records
- `OnRecordView()` - Before/after viewing a record
### Auth Hooks
- `OnRecordAuth()` - After authentication
- `OnRecordAuthWithPassword()` - Password authentication
- `OnRecordAuthWithOAuth2()` - OAuth2 authentication
- `OnRecordRequestPasswordReset()` - Password reset request
- `OnRecordConfirmPasswordReset()` - Password reset confirmation
### Serve Hooks
- `OnServe()` - Customize the HTTP server before it starts serving requests
- `OnTerminate()` - Handle graceful shutdown logic
## Examples
### Auto-populate Fields
```go
app.OnRecordCreateRequest("posts").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Auth != nil {
e.Record.Set("author", e.Auth.Id)
}
title := e.Record.GetString("title")
slug := strings.ToLower(strings.ReplaceAll(title, " ", "-"))
e.Record.Set("slug", slug)
return e.Next()
})
```
### Validation
```go
import "github.com/pocketbase/dbx"
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
title := e.Record.GetString("title")
if len(title) < 5 {
return errors.New("title must be at least 5 characters")
}
if _, err := e.App.FindFirstRecordByFilter(
"posts",
"title = {:title}",
dbx.Params{"title": title},
); err == nil {
return errors.New("title already exists")
}
return e.Next()
})
```
### Cascading Updates
```go
import "github.com/pocketbase/dbx"
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
oldStatus := e.RecordOriginal.GetString("status")
newStatus := e.Record.GetString("status")
if oldStatus != "published" && newStatus == "published" {
comments, err := e.App.FindRecordsByFilter(
"comments",
"post = {:postId}",
"",
0,
0,
dbx.Params{"postId": e.Record.Id},
)
if err != nil {
return err
}
for _, comment := range comments {
comment.Set("status", "approved")
if err := e.App.Save(comment); err != nil {
return err
}
}
}
return e.Next()
})
```
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for detailed hook documentation.

View File

@@ -0,0 +1,9 @@
# go_filesystem - Go Extensions
## Overview
This file covers go_filesystem in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_jobs_scheduling - Go Extensions
## Overview
This file covers go_jobs_scheduling in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_logging - Go Extensions
## Overview
This file covers go_logging in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,16 @@
# go_migrations - Go Extensions
## Overview
PocketBase migrations capture schema changes and ensure collections look identical across environments. Generate them with `./pocketbase migrate collections`, edit them manually, or author Go-based migrations for more complex logic. See [go_overview.md](go_overview.md) for a complete walkthrough.
---
## Preparing for Data Imports
1. **Define collections and fields before importing data.** Either create them in the Admin UI and snapshot with `migrate collections`, or programmatically add them inside a migration using `app.FindCollectionByNameOrId` and the `schema` helpers.
2. **Record unique constraints in migrations.** Use indexes and validation rules so the import scripts can rely on deterministic upsert keys.
3. **Version relation changes.** If you add or rename relation fields, ship the migration alongside your import plan; run it first so the import sees the correct schema.
4. **Keep migrations idempotent.** Wrap schema mutations in guards (`if collection == nil { ... }`) when writing Go migrations to avoid panics on re-run.
For end-to-end import/export workflows, continue with [Data Migration Workflows](../core/data_migration.md).

View File

@@ -0,0 +1,9 @@
# go_miscellaneous - Go Extensions
## Overview
This file covers go_miscellaneous in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,787 @@
# Go Overview - PocketBase
## Overview
PocketBase can be extended using Go, allowing you to:
- Add custom API endpoints
- Implement event hooks
- Create custom database migrations
- Build scheduled jobs
- Add custom middleware
- Integrate external services
- Extend authentication
## Project Structure
```
myapp/
├── go.mod
├── pocketbase.go
├── migrations/
│ └── 1703123456_initial.go
├── hooks/
│ └── hooks.go
└── main.go
```
## Creating a Go Extension
### 1. Initialize Go Module
```bash
go mod init myapp
```
### 2. Install PocketBase SDK
```bash
go get github.com/pocketbase/pocketbase@latest
```
### 3. Basic PocketBase App
Create `main.go`:
```go
package main
import (
"log"
"net/http"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
)
func main() {
app := pocketbase.New()
// Add custom API endpoint
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
// Custom routes
se.Router.GET("/api/hello", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{
"message": "Hello from Go!",
})
})
return se.Next()
})
// Start the app
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
```
### 4. Run the Application
```bash
go run main.go pocketbase.go serve --http=0.0.0.0:8090
```
## Core Concepts
### Event Hooks
Execute code on specific events:
```go
// On record create
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
log.Println("Post created:", e.Record.GetString("title"))
return e.Next()
})
// On record update
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
log.Println("Post updated:", e.Record.GetString("title"))
return e.Next()
})
// On record delete
app.OnRecordDelete("posts").BindFunc(func(e *core.RecordDeleteEvent) error {
log.Println("Post deleted:", e.Record.GetString("title"))
return e.Next()
})
// On authentication
app.OnRecordAuth().BindFunc(func(e *core.RecordAuthEvent) error {
log.Println("User authenticated:", e.Record.GetString("email"))
return e.Next()
})
```
### Event Arguments
PocketBase exposes a different event struct for each hook (see the
[official event hooks reference](https://pocketbase.io/docs/go-event-hooks/)).
Common fields you will interact with include:
- `e.App` the running PocketBase instance (database access, configuration, cron, etc.).
- `e.Record` the record being created, updated, deleted, or authenticated.
- `e.RecordOriginal` the previous value during update hooks.
- `e.Next()` call to continue the handler chain after your logic.
Refer to the linked docs for the complete list of fields exposed by each event type.
## Custom API Endpoints
### Create GET Endpoint
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.GET("/api/stats", func(e *core.RequestEvent) error {
totalPosts, err := e.App.CountRecords("posts")
if err != nil {
return e.InternalServerError("failed to count posts", err)
}
totalUsers, err := e.App.CountRecords("users")
if err != nil {
return e.InternalServerError("failed to count users", err)
}
return e.JSON(200, map[string]any{
"total_posts": totalPosts,
"total_users": totalUsers,
"authenticated": e.Auth != nil,
})
})
return se.Next()
})
```
### Create POST Endpoint
```go
import "github.com/pocketbase/dbx"
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.POST("/api/search", func(e *core.RequestEvent) error {
// Parse request body
var req struct {
Query string `json:"query"`
}
if err := e.BindBody(&req); err != nil {
return e.BadRequestError("invalid body", err)
}
// Search posts with a safe filter
records, err := e.App.FindRecordsByFilter(
"posts",
"title ~ {:query}",
"-created",
50,
0,
dbx.Params{"query": req.Query},
)
if err != nil {
return e.InternalServerError("Search failed", err)
}
return e.JSON(200, map[string]any{
"results": records,
"count": len(records),
})
})
return se.Next()
})
```
### Custom Middleware
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.BindFunc(func(e *core.RequestEvent) error {
// Add CORS headers
e.Response.Header().Set("Access-Control-Allow-Origin", "*")
e.Response.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
e.Response.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if e.Request.Method == http.MethodOptions {
return e.NoContent(http.StatusOK)
}
return e.Next()
})
return se.Next()
})
```
## Database Operations
### Find Records
```go
import "github.com/pocketbase/dbx"
// Find single record
record, err := app.FindRecordById("posts", "RECORD_ID")
// Find multiple records with a filter and pagination
records, err := app.FindRecordsByFilter(
"posts",
"status = {:status}",
"-created",
50,
0,
dbx.Params{"status": "published"},
)
// Custom query with the record query builder
records := []*core.Record{}
err := app.RecordQuery("posts").
AndWhere(dbx.Like("title", "pocketbase")).
OrderBy("created DESC").
Limit(50).
All(&records)
// Find with relations
record, err = app.FindRecordById("posts", "id")
if err == nil {
if errs := app.ExpandRecord(record, []string{"author", "comments"}, nil); len(errs) > 0 {
// handle expand error(s)
}
}
```
### Create Records
```go
collection, err := app.FindCollectionByNameOrId("posts")
if err != nil {
return err
}
record := core.NewRecord(collection)
record.Set("title", "My Post")
record.Set("content", "Post content")
record.Set("author", "USER_ID")
if err := app.Save(record); err != nil {
return err
}
```
### Update Records
```go
record, err := app.FindRecordById("posts", "id")
if err != nil {
return err
}
record.Set("title", "Updated Title")
record.Set("content", "Updated content")
if err := app.Save(record); err != nil {
return err
}
```
### Delete Records
```go
record, err := app.FindRecordById("posts", "id")
if err != nil {
return err
}
if err := app.Delete(record); err != nil {
return err
}
```
### Query Builder
```go
records := []*core.Record{}
err := app.RecordQuery("posts").
AndWhere(dbx.HashExp{"status": "published"}).
AndWhere(dbx.NewExp("created >= {:date}", dbx.Params{"date": "2024-01-01"})).
OrderBy("created DESC").
Offset(0).
Limit(50).
All(&records)
// Combine conditions with OR
records = []*core.Record{}
err = app.RecordQuery("posts").
AndWhere(dbx.Or(
dbx.HashExp{"status": "published"},
dbx.HashExp{"author": userId},
)).
All(&records)
```
## Event Hooks Examples
### Auto-populate Fields
```go
// Auto-set author on post create
app.OnRecordCreateRequest("posts").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Auth != nil {
e.Record.Set("author", e.Auth.Id)
}
return e.Next()
})
// Auto-set slug from title
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
title := e.Record.GetString("title")
slug := strings.ToLower(strings.ReplaceAll(title, " ", "-"))
e.Record.Set("slug", slug)
return e.Next()
})
```
### Validation
```go
// Custom validation
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
title := e.Record.GetString("title")
if len(title) < 5 {
return errors.New("title must be at least 5 characters")
}
return e.Next()
})
// Check permissions
app.OnRecordCreateRequest("posts").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Auth == nil {
return e.ForbiddenError("authentication required", nil)
}
role := e.Auth.GetString("role")
if role != "admin" && role != "author" {
return e.ForbiddenError("insufficient permissions", nil)
}
return e.Next()
})
```
### Cascading Updates
```go
// When post is updated, update related comments
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
// Check if status changed
oldStatus := e.RecordOriginal.GetString("status")
newStatus := e.Record.GetString("status")
if oldStatus != newStatus && newStatus == "published" {
comments, err := e.App.FindRecordsByFilter(
"comments",
"post = {:postId}",
"",
0,
0,
dbx.Params{"postId": e.Record.Id},
)
if err != nil {
return err
}
for _, comment := range comments {
comment.Set("status", "approved")
if err := e.App.Save(comment); err != nil {
return err
}
}
}
return e.Next()
})
```
### Send Notifications
```go
// Send email when post is published
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
oldStatus := e.RecordOriginal.GetString("status")
newStatus := e.Record.GetString("status")
if oldStatus != "published" && newStatus == "published" {
author, err := e.App.FindRecordById("users", e.Record.GetString("author"))
if err == nil {
log.Println("Sending notification to:", author.GetString("email"))
}
}
return e.Next()
})
```
### Log Activities
```go
// Log all record changes using the builtin logger
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
e.App.Logger().Info("post created", "recordId", e.Record.Id)
return e.Next()
})
app.OnRecordUpdate("posts").BindFunc(func(e *core.RecordUpdateEvent) error {
e.App.Logger().Info(
"post updated",
"recordId", e.Record.Id,
"statusFrom", e.RecordOriginal.GetString("status"),
"statusTo", e.Record.GetString("status"),
)
return e.Next()
})
app.OnRecordDelete("posts").BindFunc(func(e *core.RecordDeleteEvent) error {
e.App.Logger().Info("post deleted", "recordId", e.Record.Id)
return e.Next()
})
```
## Scheduled Jobs
### Create Background Job
```go
// Register job
app.Cron().MustAdd("daily-backup", "0 2 * * *", func() {
log.Println("Running daily backup...")
backupDir := "./backups"
if err := os.MkdirAll(backupDir, 0o755); err != nil {
log.Println("Backup failed:", err)
return
}
// Your backup logic here
log.Println("Backup completed")
})
// Or add a job during serve
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.App.Cron().MustAdd("cleanup", "@every 5m", func() {
log.Println("Running cleanup task...")
// Cleanup logic
})
return se.Next()
})
```
## File Handling
### Custom File Upload
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.POST("/api/upload", func(e *core.RequestEvent) error {
files, err := e.FindUploadedFiles("file")
if err != nil {
return e.BadRequestError("no file uploaded", err)
}
collection, err := e.App.FindCollectionByNameOrId("uploads")
if err != nil {
return e.NotFoundError("uploads collection not found", err)
}
record := core.NewRecord(collection)
// Attach the uploaded file(s) to a file field
record.Set("document", files)
if err := e.App.Save(record, files...); err != nil {
return e.InternalServerError("failed to save file", err)
}
return e.JSON(http.StatusOK, record)
})
return se.Next()
})
```
## Custom Auth Provider
```go
// Custom OAuth provider
app.OnRecordAuthWithOAuth2().BindFunc(func(e *core.RecordAuthWithOAuth2Event) error {
if e.Provider != "custom" {
return e.Next()
}
// Fetch user info from custom provider
userInfo, err := fetchCustomUserInfo(e.OAuth2UserData)
if err != nil {
return err
}
// Find or create user
user, err := e.App.FindAuthRecordByData("users", "email", userInfo.Email)
if err != nil {
collection, err := e.App.FindCollectionByNameOrId("users")
if err != nil {
return err
}
user = core.NewRecord(collection)
user.Set("email", userInfo.Email)
user.Set("password", "") // OAuth users don't need password
user.Set("emailVisibility", false)
user.Set("verified", true)
user.Set("name", userInfo.Name)
if err := e.App.Save(user); err != nil {
return err
}
}
e.Record = user
return e.Next()
})
```
## Testing
### Unit Tests
```go
package main
import (
"testing"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
)
func TestCustomEndpoint(t *testing.T) {
app := pocketbase.NewWithConfig(config{})
// Add test endpoint
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.GET("/api/test", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{
"status": "ok",
})
})
return se.Next()
})
e := tests.NewRequestEvent(app, nil)
// Test endpoint
e.GET("/api/test").Expect(t).Status(200).JSON().Equal(map[string]interface{}{
"status": "ok",
})
}
```
### Integration Tests
```go
func TestRecordCreation(t *testing.T) {
app := pocketbase.New()
app.MustSeed()
client := tests.NewClient(app)
// Test authenticated request
auth := client.AuthRecord("users", "test@example.com", "password")
post := client.CreateRecord("posts", map[string]interface{}{
"title": "Test Post",
"content": "Test content",
}, auth.Token)
if post.GetString("title") != "Test Post" {
t.Errorf("Expected title 'Test Post', got %s", post.GetString("title"))
}
}
```
## Deployment
### Build and Run
```bash
# Build
go build -o myapp main.go
# Run
./myapp serve --http=0.0.0.0:8090
```
### Docker Deployment
```dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o myapp main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
COPY --from=builder /app/pocketbase ./
CMD ["./myapp", "serve", "--http=0.0.0.0:8090"]
```
## Best Practices
### 1. Error Handling
```go
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
if err := validatePost(e.Record); err != nil {
return err
}
return e.Next()
})
func validatePost(record *core.Record) error {
title := record.GetString("title")
if len(title) == 0 {
return errors.New("title is required")
}
if len(title) > 200 {
return errors.New("title too long")
}
return nil
}
```
### 2. Logging
```go
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
e.App.Logger().Info("Post created",
"id", e.Record.Id,
"title", e.Record.GetString("title"),
)
return e.Next()
})
```
### 3. Security
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
limiter := rate.NewLimiter(10, 20) // 10 req/sec, burst 20
se.Router.BindFunc(func(e *core.RequestEvent) error {
if !limiter.Allow() {
return e.TooManyRequestsError("rate limit exceeded", nil)
}
return e.Next()
})
return se.Next()
})
```
### 4. Configuration
```go
type Config struct {
ExternalAPIKey string
EmailFrom string
}
func (c Config) Name() string {
return "myapp"
}
func main() {
app := pocketbase.NewWithConfig(Config{
ExternalAPIKey: os.Getenv("API_KEY"),
EmailFrom: "noreply@example.com",
})
// Use config in hooks
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
cfg := e.App.Config().(*Config)
// Use cfg.ExternalAPIKey
return e.Next()
})
}
```
## Common Patterns
### 1. Soft Delete
```go
app.OnRecordDelete("posts").BindFunc(func(e *core.RecordDeleteEvent) error {
// Instead of deleting, mark as deleted
e.Record.Set("status", "deleted")
e.Record.Set("deleted_at", time.Now())
if err := e.App.Save(e.Record); err != nil {
return err
}
return e.Next()
})
```
### 2. Audit Trail
```go
app.OnRecordCreateRequest("").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Collection.Name == "posts" || e.Collection.Name == "comments" {
if e.Auth != nil {
e.Record.Set("created_by", e.Auth.Id)
}
if info, err := e.RequestInfo(); err == nil {
e.Record.Set("created_ip", info.RealIP)
}
}
return e.Next()
})
app.OnRecordUpdateRequest("").BindFunc(func(e *core.RecordRequestEvent) error {
if e.Collection.Name == "posts" || e.Collection.Name == "comments" {
if e.Auth != nil {
e.Record.Set("updated_by", e.Auth.Id)
}
e.Record.Set("updated_at", time.Now())
}
return e.Next()
})
```
### 3. Data Synchronization
```go
app.OnRecordCreate("posts").BindFunc(func(e *core.RecordCreateEvent) error {
// Sync with external service
if err := syncToExternalAPI(e.Record); err != nil {
e.App.Logger().Warn("sync failed", "error", err)
}
return e.Next()
})
func syncToExternalAPI(record *core.Record) error {
// Implement external API sync
return nil
}
```
## Related Topics
- [Event Hooks](go_event_hooks.md) - Detailed hook documentation
- [Database](go_database.md) - Database operations
- [Routing](go_routing.md) - Custom API endpoints
- [Migrations](go_migrations.md) - Database migrations
- [Testing](go_testing.md) - Testing strategies
- [Logging](go_logging.md) - Logging and monitoring

View File

@@ -0,0 +1,9 @@
# go_realtime - Go Extensions
## Overview
This file covers go_realtime in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_record_proxy - Go Extensions
## Overview
This file covers go_record_proxy in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_records - Go Extensions
## Overview
This file covers go_records in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_rendering_templates - Go Extensions
## Overview
This file covers go_rendering_templates in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,61 @@
# Routing - Go Extensions
## Overview
Create custom API endpoints and middleware using PocketBase's routing system.
## Custom Endpoints
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
// GET endpoint
se.Router.GET("/api/custom", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{"status": "ok"})
})
// POST endpoint
se.Router.POST("/api/custom", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{"message": "created"})
})
return se.Next()
})
```
### Admin-only import/export endpoints
Expose migration jobs over HTTP when you need a web dashboard trigger:
```go
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
se.Router.POST("/api/admin/data/import", func(e *core.RequestEvent) error {
if !apis.IsSuperUser(e.Auth) {
return apis.NewForbiddenError("admin token required", nil)
}
var payload struct {
Collection string `json:"collection"`
File string `json:"file"`
DryRun bool `json:"dryRun"`
}
if err := e.BindBody(&payload); err != nil {
return err
}
return e.App.RunInTransaction(func(txApp core.App) error {
// invoke shared import logic here
return nil
})
})
return se.Next()
})
```
- Require superuser tokens (or tighter auth) before touching data.
- For long-running operations, enqueue a job and return an ID the client can poll.
- Keep HTTP handlers thin—delegate to the same helpers used by the CLI commands described in [Data Migration Workflows](../core/data_migration.md).
---
**Note:** See [go_overview.md](go_overview.md) for detailed routing documentation.

View File

@@ -0,0 +1,9 @@
# go_sdk - Go Extensions
## Overview
This file covers go_sdk in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_sending_emails - Go Extensions
## Overview
This file covers go_sending_emails in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

View File

@@ -0,0 +1,9 @@
# go_testing - Go Extensions
## Overview
This file covers go_testing in PocketBase Go extensions.
---
**Note:** This is a placeholder file. See [go_overview.md](go_overview.md) for comprehensive documentation.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
# Dart SDK
## Overview
This file covers dart_sdk in PocketBase Go extensions.
---
**Note:** This is a placeholder file.

View File

@@ -0,0 +1,203 @@
# JavaScript SDK
## Overview
The JavaScript SDK is the primary way to interact with PocketBase from frontend applications. It's available via CDN or npm package.
## Installation
### Via CDN
```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');
</script>
```
### Via npm
```bash
npm install pocketbase
```
```javascript
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
```
## Initialization
```javascript
const pb = new PocketBase('http://127.0.0.1:8090');
```
For advanced configuration (custom auth store, language, fetch implementation, etc.), refer to the [official JS SDK README](https://github.com/pocketbase/js-sdk).
## Core Features
### Authentication
- User registration and login
- OAuth2 integration
- Auth state management
- JWT token handling
### Data Operations
- CRUD operations on collections
- Filtering, sorting, pagination
- Relation expansion
- Batch operations
### Realtime
- WebSocket subscriptions
- Live updates
- Event handling
### File Management
- File uploads
- File URL generation
- Thumbnail access
## Common Use Cases
### React Integration
```javascript
import { useEffect, useState } from 'react';
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
function useAuth() {
const [user, setUser] = useState(pb.authStore.model);
useEffect(() => {
const unsub = pb.authStore.onChange(() => {
setUser(pb.authStore.model);
});
return () => unsub();
}, []);
return { user };
}
function PostsList() {
const [posts, setPosts] = useState([]);
const { user } = useAuth();
useEffect(() => {
loadPosts();
// Subscribe to realtime 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);
}
async function createPost(data) {
await pb.collection('posts').create(data);
}
return (
<div>
{user && (
<button onClick={() => createPost({ title: 'New Post' })}>
Create Post
</button>
)}
{posts.map(post => <div key={post.id}>{post.title}</div>)}
</div>
);
}
```
### Vue.js Integration
```javascript
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
export default {
data() {
return {
posts: [],
user: pb.authStore.model
};
},
mounted() {
this.loadPosts();
// Subscribe to auth changes
pb.authStore.onChange(() => {
this.user = pb.authStore.model;
});
// Subscribe to realtime
pb.collection('posts').subscribe('*', () => {
this.loadPosts();
});
},
beforeUnmount() {
pb.collection('posts').unsubscribe();
},
methods: {
async loadPosts() {
const records = await pb.collection('posts').getList(1, 50);
this.posts = records.items;
},
async login(email, password) {
await pb.collection('users').authWithPassword(email, password);
this.user = pb.authStore.model;
}
}
};
```
### Vanilla JavaScript
```javascript
const pb = new PocketBase('http://127.0.0.1:8090');
async function loadPosts() {
const response = await pb.collection('posts').getList(1, 50);
renderPosts(response.items);
}
function renderPosts(posts) {
const container = document.getElementById('posts');
container.innerHTML = posts.map(post => `
<div class="post">
<h3>${post.title}</h3>
<p>${post.content}</p>
</div>
`).join('');
}
async function createPost(title, content) {
await pb.collection('posts').create({
title,
content
});
await loadPosts();
}
// Subscribe to realtime
pb.collection('posts').subscribe('*', () => {
loadPosts();
});
// Initialize
loadPosts();
```
---
**Note:** This is a placeholder file. See [core/getting_started.md](../core/getting_started.md) for detailed SDK usage examples.

View File

@@ -0,0 +1,467 @@
# PocketBase Security Rules
Comprehensive guide to implementing security and access control in PocketBase collections.
## Table of Contents
1. [Understanding Security Rules](#understanding-security-rules)
2. [Rule Types](#rule-types)
3. [Common Patterns](#common-patterns)
4. [Role-Based Access Control](#role-based-access-control)
5. [Field-Level Security](#field-level-security)
6. [File Security](#file-security)
7. [Examples by Use Case](#examples-by-use-case)
8. [Testing Rules](#testing-rules)
## Understanding Security Rules
PocketBase uses four types of security rules per collection:
1. **listRule** - Who can view the list of records
2. **viewRule** - Who can view individual records
3. **createRule** - Who can create new records
4. **updateRule** - Who can update existing records
5. **deleteRule** - Who can delete records
### Rule Context Variables
- `@request.auth.id` - ID of the authenticated user making the request
- `@request.auth` - The full authenticated user record
- `@request.method` - HTTP method (GET, POST, PATCH, DELETE)
- `id` - ID of the current record being accessed
### Common Comparison Operators
- `=` - Equals
- `!=` - Not equals
- `<`, `<=`, `>`, `>=` - Numeric comparisons
- `~` - Contains/in (for arrays and relations)
- `!~` - Not contains
- `~` with regex - Pattern matching (e.g., `name ~ "test"`)
## Rule Types
### Public Access (No Authentication)
```javascript
// Anyone can read, only authenticated can write
listRule: ""
viewRule: ""
createRule: "@request.auth.id != ''"
```
### Authenticated Users Only
```javascript
// Only authenticated users can access
listRule: "@request.auth.id != ''"
viewRule: "@request.auth.id != ''"
createRule: "@request.auth.id != ''"
updateRule: "@request.auth.id != ''"
deleteRule: "@request.auth.id != ''"
```
### Owner-Based Access Control
```javascript
// Only the record owner can modify
createRule: "@request.auth.id != ''"
updateRule: "user_id = @request.auth.id"
deleteRule: "user_id = @request.auth.id"
```
### Admin-Only Access
```javascript
// Only admins can access (requires user role field)
listRule: "@request.auth.role = 'admin'"
viewRule: "@request.auth.role = 'admin'"
createRule: "@request.auth.role = 'admin'"
updateRule: "@request.auth.role = 'admin'"
deleteRule: "@request.auth.role = 'admin'"
```
### Read Public, Write Owner
```javascript
// Public can read, only owner can write
listRule: ""
viewRule: ""
createRule: "@request.auth.id != ''"
updateRule: "author = @request.auth.id"
deleteRule: "author = @request.auth.id"
```
## Common Patterns
### Pattern 1: User Profile (User Can Only Modify Their Own)
```javascript
listRule: "@request.auth.id = user_id"
viewRule: "@request.auth.id = user_id"
createRule: "@request.auth.id = user_id"
updateRule: "@request.auth.id = user_id"
deleteRule: "@request.auth.id = user_id"
// Where user_id is a field that stores the record owner's ID
```
### Pattern 2: Posts/Articles (Public Read, Owner Write)
```javascript
listRule: "status = 'published'"
viewRule: "status = 'published'"
createRule: "@request.auth.id != ''"
updateRule: "author = @request.auth.id"
deleteRule: "author = @request.auth.id"
```
### Pattern 3: Comments (Nested Under Parent)
```javascript
listRule: "post.status = 'published'"
viewRule: "post.status = 'published'"
createRule: "@request.auth.id != ''"
updateRule: "author = @request.auth.id"
deleteRule: "author = @request.auth.id"
// Assuming 'post' is a relation field
```
### Pattern 4: Team Projects (Team Members Only)
```javascript
listRule: "@request.auth.id ~ members"
viewRule: "@request.auth.id ~ members"
createRule: "@request.auth.id ~ members"
updateRule: "creator = @request.auth.id || @request.auth.id ~ members"
deleteRule: "creator = @request.auth.id"
// Where 'members' is an array of user IDs
```
### Pattern 5: E-commerce Orders
```javascript
// Customers can see their own orders
listRule: "customer = @request.auth.id"
viewRule: "customer = @request.auth.id"
createRule: "customer = @request.auth.id"
updateRule: "@request.auth.id != ''" // Only admins/staff can update
deleteRule: "@request.auth.id != ''" // Only admins can delete
```
## Role-Based Access Control
### Basic RBAC with User Roles
First, add a role field to your users collection:
```json
{
"id": "role",
"name": "role",
"type": "select",
"required": true,
"options": {
"values": ["user", "moderator", "admin"]
}
}
```
Now use the role in security rules:
**Regular Collection (User/Moderator/Admin)**
```javascript
listRule: "@request.auth.id != ''"
viewRule: "@request.auth.id != ''"
createRule: "@request.auth.id != ''"
updateRule: "user_id = @request.auth.id || @request.auth.role = 'moderator' || @request.auth.role = 'admin'"
deleteRule: "@request.auth.role = 'moderator' || @request.auth.role = 'admin'"
```
**Admin-Only Collection**
```javascript
listRule: "@request.auth.role = 'admin'"
viewRule: "@request.auth.role = 'admin'"
createRule: "@request.auth.role = 'admin'"
updateRule: "@request.auth.role = 'admin'"
deleteRule: "@request.auth.role = 'admin'"
```
**Moderator+ Collection**
```javascript
listRule: "@request.auth.id != ''"
viewRule: "@request.auth.id != ''"
createRule: "@request.auth.id != ''"
updateRule: "user_id = @request.auth.id || @request.auth.role != 'user'"
deleteRule: "@request.auth.role != 'user'"
```
### Advanced RBAC: Permission Matrix
```javascript
// Roles: user, author, editor, admin
// Permissions: read, write, delete, publish
// For 'posts' collection:
createRule: "@request.auth.role = 'author' || @request.auth.role = 'editor' || @request.auth.role = 'admin'"
updateRule: "author = @request.auth.id || @request.auth.role = 'editor' || @request.auth.role = 'admin'"
deleteRule: "author = @request.auth.id || @request.auth.role = 'admin'"
updateRule: "status = 'draft' && (author = @request.auth.id || @request.auth.role = 'editor' || @request.auth.role = 'admin')"
// Only editors and admins can publish
updateRule: "if(status != 'published'){ author = @request.auth.id } else { @request.auth.role = 'editor' || @request.auth.role = 'admin' }"
```
## Field-Level Security
Restrict access to specific fields using the `options` parameter in the schema.
### Read-Only Fields
```json
{
"id": "created_by",
"name": "created_by",
"type": "relation",
"required": true,
"options": {
"collectionId": "users",
"cascadeDelete": false,
"maxSelect": 1
},
"presentable": false // Don't show in public APIs
}
```
### Admin-Only Fields
```json
{
"id": "internal_notes",
"name": "internal_notes",
"type": "text",
"options": {},
"onlyAllow": ["@request.auth.role = 'admin'"] // Only admins can set
}
```
### User-Owned Fields
```json
{
"id": "private_data",
"name": "private_data",
"type": "json",
"options": {},
"onlyAllow": ["user_id = @request.auth.id || @request.auth.role = 'admin'"]
}
```
## File Security
Control who can upload, view, and delete files.
### Private Files (Owner Only)
```javascript
// User avatars - only owner can upload
createRule: "@request.auth.id != ''"
updateRule: "@request.auth.id = user_id"
// File access rule (in file field options):
// This controls who can access the file URL
"options": {
"maxSelect": 1,
"maxSize": 5242880,
"thumbs": ["100x100", "300x300"],
"filterSelect": "user_id = @request.auth.id"
}
```
### Public Files (Viewable by All)
```javascript
// Blog post images - authenticated users can upload
createRule: "@request.auth.id != ''"
updateRule: "author = @request.auth.id"
// File is publicly viewable
```
### Members-Only Files
```javascript
// Team documents - only team members can access
createRule: "@request.auth.id ~ team_members"
updateRule: "@request.auth.id ~ team_members"
// File access filter
"filterSelect": "@request.auth.id ~ team_members"
```
## Examples by Use Case
### Blog Platform
```javascript
// Posts
listRule: "status = 'published'"
viewRule: "status = 'published'"
createRule: "@request.auth.id != ''"
updateRule: "author = @request.auth.id"
deleteRule: "author = @request.auth.id"
// Comments (must be authenticated)
listRule: "is_approved = true"
viewRule: "is_approved = true"
createRule: "@request.auth.id != ''"
updateRule: "author = @request.auth.id"
deleteRule: "author = @request.auth.id"
```
### Social Network
```javascript
// Posts (public)
listRule: ""
viewRule: ""
createRule: "@request.auth.id != ''"
updateRule: "author = @request.auth.id"
deleteRule: "author = @request.auth.id"
// Private Messages
listRule: "sender = @request.auth.id || receiver = @request.auth.id"
viewRule: "sender = @request.auth.id || receiver = @request.auth.id"
createRule: "@request.auth.id != ''"
updateRule: "@request.auth.id != ''" // Only mark as read
deleteRule: "sender = @request.auth.id || receiver = @request.auth.id"
```
### SaaS Application
```javascript
// Workspaces
listRule: "@request.auth.id ~ members"
viewRule: "@request.auth.id ~ members"
createRule: "@request.auth.id ~ members"
updateRule: "@request.auth.id ~ owners"
deleteRule: "@request.auth.id ~ owners"
// Workspace Records
listRule: "workspace.members.id ?= @request.auth.id"
viewRule: "workspace.members.id ?= @request.auth.id"
createRule: "workspace.members.id ?= @request.auth.id"
updateRule: "workspace.members.id ?= @request.auth.id"
deleteRule: "workspace.owners.id ?= @request.auth.id"
```
### E-commerce
```javascript
// Products
listRule: "is_active = true"
viewRule: "is_active = true"
createRule: "@request.auth.id != ''" // Staff only in real app
updateRule: "@request.auth.id != ''" // Staff only
deleteRule: "@request.auth.id != ''" // Staff only
// Orders
listRule: "customer = @request.auth.id"
viewRule: "customer = @request.auth.id"
createRule: "customer = @request.auth.id"
updateRule: "@request.auth.id != ''" // Staff can update status
deleteRule: "@request.auth.id != ''" // Staff only
```
### Project Management
```javascript
// Projects
listRule: "@request.auth.id ~ members || @request.auth.id = owner"
viewRule: "@request.auth.id ~ members || @request.auth.id = owner"
createRule: "@request.auth.id != ''"
updateRule: "@request.auth.id = owner || @request.auth.id ~ managers"
deleteRule: "@request.auth.id = owner"
// Tasks
listRule: "project.members.id ?= @request.auth.id"
viewRule: "project.members.id ?= @request.auth.id"
createRule: "project.members.id ?= @request.auth.id"
updateRule: "assignee = @request.auth.id || @request.auth.id ~ project.managers"
deleteRule: "@request.auth.id ~ project.managers"
```
## Testing Rules
### Manual Testing
1. Create a test user account
2. Create records with different ownership
3. Test each rule (list, view, create, update, delete)
4. Test with different user roles
5. Test edge cases (null values, missing relations, etc.)
### Programmatic Testing
```javascript
// Test with authenticated user
const pb = new PocketBase('http://127.0.0.1:8090')
// Login
await pb.collection('users').authWithPassword('test@example.com', 'password')
try {
// Try to create record
const record = await pb.collection('posts').create({
title: 'Test',
content: 'Content'
})
console.log('✓ Create successful')
} catch (e) {
console.log('✗ Create failed:', e.data)
}
try {
// Try to get list
const records = await pb.collection('posts').getList(1, 50)
console.log('✓ List successful:', records.items.length, 'records')
} catch (e) {
console.log('✗ List failed:', e.message)
}
```
### Common Pitfalls
1. **Forgetting `!= ''` check**
```javascript
// Wrong - allows anonymous users
createRule: "user_id = user_id"
// Correct - requires authentication
createRule: "@request.auth.id != '' && user_id = @request.auth.id"
```
2. **Incorrect relation syntax**
```javascript
// Wrong
listRule: "user.id = @request.auth.id"
// Correct
listRule: "user = @request.auth.id"
```
3. **Not handling null values**
```javascript
// If field can be null, add explicit check
listRule: "status != null && status = 'published'"
```
4. **Over-restrictive rules**
```javascript
// This prevents admins from accessing
updateRule: "author = @request.auth.id"
// Better - allows admin override
updateRule: "author = @request.auth.id || @request.auth.role = 'admin'"
```
## Best Practices
1. **Use the principle of least privilege** - Start restrictive, add permissions as needed
2. **Test with multiple user roles** - Don't just test with admin users
3. **Document your rules** - Add comments explaining complex rules
4. **Use consistent naming** - Name fields clearly (e.g., `author` instead of `user`)
5. **Validate on the client** - Don't rely solely on server-side validation
6. **Use indexes** - Add database indexes for fields used in rules
7. **Monitor access** - Log security events and failed attempts
8. **Regular audits** - Review rules periodically for security issues
## Security Checklist
- [ ] All sensitive collections require authentication
- [ ] Users can only access their own data
- [ ] Admin-only collections are properly protected
- [ ] File uploads have size and type restrictions
- [ ] Delete operations are properly restricted
- [ ] Rules handle edge cases (null values, empty arrays)
- [ ] Public data is explicitly marked as public
- [ ] Internal data is never exposed via rules
- [ ] Rules are tested with multiple user types
- [ ] Complex rules are documented and reviewed

View File

@@ -0,0 +1,219 @@
#!/usr/bin/env python3
#!/usr/bin/env python3
"""PocketBase data export helper with admin auth, pagination, filters, and NDJSON support."""
import argparse
import json
from getpass import getpass
from pathlib import Path
from typing import Dict, Iterable, List, Optional
import requests
DEFAULT_BATCH_SIZE = 200
REQUEST_TIMEOUT = 30
def authenticate(base_url: str, email: Optional[str], password: Optional[str]) -> Dict[str, str]:
if not email:
return {}
if not password:
password = getpass(prompt="Admin password: ")
response = requests.post(
f"{base_url}/api/admins/auth-with-password",
json={"identity": email, "password": password},
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
token = response.json().get("token")
if not token:
raise RuntimeError("Authentication response missing token")
return {"Authorization": f"Bearer {token}"}
def list_collections(base_url: str, headers: Dict[str, str]) -> List[Dict]:
collections: List[Dict] = []
page = 1
while True:
response = requests.get(
f"{base_url}/api/collections",
params={"page": page, "perPage": 200},
headers=headers,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
payload = response.json()
items = payload.get("items", [])
collections.extend(items)
total = payload.get("totalItems", len(collections))
if page * 200 >= total or not items:
break
page += 1
return collections
def filter_collections(
collections: Iterable[Dict],
include: Optional[List[str]],
exclude: Optional[List[str]],
include_system: bool,
) -> List[Dict]:
include_set = {name.strip() for name in include or [] if name.strip()}
exclude_set = {name.strip() for name in exclude or [] if name.strip()}
filtered: List[Dict] = []
for collection in collections:
name = collection.get("name")
if not name:
continue
if include_set and name not in include_set:
continue
if name in exclude_set:
continue
if not include_system and collection.get("system"):
continue
filtered.append(collection)
filtered.sort(key=lambda c: c.get("name", ""))
return filtered
def export_collection(
base_url: str,
collection: Dict,
headers: Dict[str, str],
output_dir: Path,
batch_size: int,
fmt: str,
) -> int:
name = collection["name"]
output_dir.mkdir(parents=True, exist_ok=True)
total_written = 0
file_ext = "ndjson" if fmt == "ndjson" else "json"
output_path = output_dir / f"{name}.{file_ext}"
records_url = f"{base_url}/api/collections/{name}/records"
with output_path.open("w", encoding="utf-8") as handle:
page = 1
aggregated: List[Dict] = []
while True:
response = requests.get(
records_url,
params={"page": page, "perPage": batch_size},
headers=headers,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
payload = response.json()
items = payload.get("items", [])
if not items:
break
if fmt == "ndjson":
for item in items:
handle.write(json.dumps(item, ensure_ascii=False))
handle.write("\n")
else:
aggregated.extend(items)
total_written += len(items)
total_items = payload.get("totalItems")
if total_items and total_written >= total_items:
break
page += 1
if fmt == "json":
json.dump(
{
"collection": name,
"exportedAt": collection.get("updated", ""),
"items": aggregated,
},
handle,
ensure_ascii=False,
indent=2,
)
return total_written
def build_manifest(output_dir: Path, manifest: List[Dict]):
if not manifest:
return
(output_dir / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Export PocketBase collections")
parser.add_argument("base_url", help="PocketBase base URL, e.g. http://127.0.0.1:8090")
parser.add_argument(
"output_dir",
nargs="?",
default="pocketbase_export",
help="Directory to write exported files",
)
parser.add_argument("--email", help="Admin email for authentication")
parser.add_argument("--password", help="Admin password (omit to prompt)")
parser.add_argument(
"--collections",
help="Comma-separated collection names to export",
)
parser.add_argument(
"--exclude",
help="Comma-separated collection names to skip",
)
parser.add_argument(
"--include-system",
action="store_true",
help="Include system collections (default: skip)",
)
parser.add_argument(
"--batch-size",
type=int,
default=DEFAULT_BATCH_SIZE,
help="Records per request (default: 200)",
)
parser.add_argument(
"--format",
choices=["json", "ndjson"],
default="json",
help="Output format per collection",
)
return parser.parse_args()
def main():
args = parse_args()
base_url = args.base_url.rstrip("/")
output_dir = Path(args.output_dir)
headers = authenticate(base_url, args.email, args.password)
collections = list_collections(base_url, headers)
include = args.collections.split(",") if args.collections else None
exclude = args.exclude.split(",") if args.exclude else None
filtered = filter_collections(collections, include, exclude, args.include_system)
if not filtered:
raise RuntimeError("No collections selected for export")
manifest: List[Dict] = []
for collection in filtered:
name = collection["name"]
count = export_collection(
base_url,
collection,
headers,
output_dir,
max(args.batch_size, 1),
args.format,
)
manifest.append({"collection": name, "records": count})
print(f"Exported {name}: {count} records")
build_manifest(output_dir, manifest)
print(f"Completed export to {output_dir.resolve()}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,339 @@
#!/usr/bin/env python3
#!/usr/bin/env python3
"""PocketBase data import helper with admin auth, batching, optional upsert, and dry-run."""
import argparse
import json
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from getpass import getpass
from pathlib import Path
from typing import Dict, Iterable, Iterator, List, Optional, Tuple
import requests
REQUEST_TIMEOUT = 30
DEFAULT_BATCH_SIZE = 100
DROP_KEYS = {"id", "created", "updated", "@collectionId", "@collectionName", "@expand"}
def authenticate(base_url: str, email: Optional[str], password: Optional[str]) -> Dict[str, str]:
if not email:
return {}
if not password:
password = getpass(prompt="Admin password: ")
response = requests.post(
f"{base_url}/api/admins/auth-with-password",
json={"identity": email, "password": password},
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
token = response.json().get("token")
if not token:
raise RuntimeError("Authentication response missing token")
return {"Authorization": f"Bearer {token}"}
def list_collections(base_url: str, headers: Dict[str, str]) -> Dict[str, Dict]:
collections: Dict[str, Dict] = {}
page = 1
while True:
response = requests.get(
f"{base_url}/api/collections",
params={"page": page, "perPage": 200},
headers=headers,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
payload = response.json()
items = payload.get("items", [])
for item in items:
if item.get("name"):
collections[item["name"]] = item
total = payload.get("totalItems", len(collections))
if page * 200 >= total or not items:
break
page += 1
return collections
def chunked(iterable: Iterable[Dict], size: int) -> Iterator[List[Dict]]:
chunk: List[Dict] = []
for item in iterable:
chunk.append(item)
if len(chunk) >= size:
yield chunk
chunk = []
if chunk:
yield chunk
def iter_ndjson(file_path: Path) -> Iterator[Dict]:
with file_path.open("r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line:
continue
yield json.loads(line)
def load_json_records(file_path: Path) -> Tuple[List[Dict], Optional[str]]:
with file_path.open("r", encoding="utf-8") as handle:
payload = json.load(handle)
if isinstance(payload, dict):
return payload.get("items", []), payload.get("collection")
if isinstance(payload, list):
return payload, None
raise ValueError(f"Unsupported JSON structure in {file_path}")
def clean_record(record: Dict) -> Dict:
return {k: v for k, v in record.items() if k not in DROP_KEYS}
def prepend_items(items: Iterable[Dict], iterator: Iterator[Dict]) -> Iterator[Dict]:
for item in items:
yield item
for item in iterator:
yield item
def build_filter(field: str, value) -> str:
if value is None:
return f"{field} = null"
if isinstance(value, bool):
return f"{field} = {str(value).lower()}"
if isinstance(value, (int, float)):
return f"{field} = {value}"
escaped = str(value).replace("\"", r"\"")
return f'{field} = "{escaped}"'
def request_with_retry(session: requests.Session, method: str, url: str, *, retries: int = 3, backoff: float = 1.0, **kwargs) -> requests.Response:
last_response: Optional[requests.Response] = None
for attempt in range(retries):
response = session.request(method, url, timeout=REQUEST_TIMEOUT, **kwargs)
status = response.status_code
if status in {429, 503} and attempt < retries - 1:
time.sleep(backoff)
backoff = min(backoff * 2, 8)
last_response = response
continue
if status >= 400:
response.raise_for_status()
return response
assert last_response is not None
last_response.raise_for_status()
def find_existing(
base_url: str,
collection: str,
field: str,
value,
headers: Dict[str, str],
) -> Optional[Dict]:
session = requests.Session()
try:
response = request_with_retry(
session,
"get",
f"{base_url}/api/collections/{collection}/records",
headers=headers,
params={
"page": 1,
"perPage": 1,
"filter": build_filter(field, value),
"skipTotal": 1,
},
)
items = response.json().get("items", [])
if items:
return items[0]
return None
finally:
session.close()
def process_record(
base_url: str,
collection: str,
record: Dict,
headers: Dict[str, str],
upsert_field: Optional[str],
dry_run: bool,
) -> Tuple[bool, Optional[str]]:
data = clean_record(record)
if dry_run:
return True, None
session = requests.Session()
try:
url = f"{base_url}/api/collections/{collection}/records"
if upsert_field and upsert_field in record:
existing = find_existing(base_url, collection, upsert_field, record.get(upsert_field), headers)
if existing:
record_id = existing.get("id")
if record_id:
response = request_with_retry(
session,
"patch",
f"{url}/{record_id}",
headers=headers,
json=data,
)
return response.ok, None
response = request_with_retry(
session,
"post",
url,
headers=headers,
json=data,
)
return response.status_code in {200, 201}, None
except requests.HTTPError as exc:
return False, f"HTTP {exc.response.status_code}: {exc.response.text[:200]}"
except Exception as exc: # noqa: BLE001
return False, str(exc)
finally:
session.close()
def parse_upsert(args: argparse.Namespace) -> Dict[str, str]:
mapping: Dict[str, str] = {}
for item in args.upsert or []:
if "=" not in item:
raise ValueError(f"Invalid upsert mapping '{item}'. Use collection=field or *=field")
collection, field = item.split("=", 1)
mapping[collection.strip()] = field.strip()
return mapping
def infer_collection(file_path: Path, first_record: Optional[Dict]) -> str:
if first_record and first_record.get("@collectionName"):
return first_record["@collectionName"]
return file_path.stem
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Import PocketBase data dumps")
parser.add_argument("base_url", help="PocketBase base URL, e.g. http://127.0.0.1:8090")
parser.add_argument("input_path", help="Directory or file with export data")
parser.add_argument("--email", help="Admin email for authentication")
parser.add_argument("--password", help="Admin password (omit to prompt)")
parser.add_argument("--collections", help="Comma-separated collections to include")
parser.add_argument("--exclude", help="Comma-separated collections to skip")
parser.add_argument("--upsert", action="append", help="collection=field mapping (use *=field for default)")
parser.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE, help="Records per batch")
parser.add_argument("--concurrency", type=int, default=4, help="Concurrent workers per batch")
parser.add_argument("--throttle", type=float, default=0.0, help="Seconds to sleep between batches")
parser.add_argument("--dry-run", action="store_true", help="Parse files without writing to PocketBase")
parser.add_argument("--skip-missing", action="store_true", help="Skip files whose collections do not exist")
return parser.parse_args()
def main():
args = parse_args()
base_url = args.base_url.rstrip("/")
input_path = Path(args.input_path)
if not input_path.exists():
raise SystemExit(f"Input path {input_path} does not exist")
headers = authenticate(base_url, args.email, args.password)
collections = list_collections(base_url, headers)
include = {c.strip() for c in args.collections.split(",")} if args.collections else None
exclude = {c.strip() for c in args.exclude.split(",")} if args.exclude else set()
upsert_map = parse_upsert(args)
if input_path.is_file():
files = [input_path]
else:
files = sorted(
p for p in input_path.iterdir() if p.is_file() and p.suffix.lower() in {".json", ".ndjson"}
)
if not files:
raise SystemExit("No data files found")
for file_path in files:
if file_path.stem == "manifest":
continue
if file_path.suffix.lower() == ".ndjson":
iterator = iter_ndjson(file_path)
peeked: List[Dict] = []
try:
first_record = next(iterator)
peeked.append(first_record)
except StopIteration:
print(f"Skipping {file_path.name}: no records")
continue
source_iter = prepend_items(peeked, iterator)
meta_collection = None
else:
records, meta_collection = load_json_records(file_path)
if not records:
print(f"Skipping {file_path.name}: no records")
continue
first_record = records[0]
source_iter = iter(records)
collection = meta_collection or infer_collection(file_path, first_record)
if include and collection not in include:
continue
if collection in exclude:
continue
if collection not in collections:
if args.skip_missing:
print(f"Skipping {file_path.name}: collection '{collection}' not found")
continue
raise SystemExit(f"Collection '{collection}' not found in PocketBase")
print(f"Importing {file_path.name} -> {collection}")
total = success = 0
failures: List[str] = []
field = upsert_map.get(collection, upsert_map.get("*"))
source_iter = prepend_items(peeked, iterator)
for batch in chunked(source_iter, max(args.batch_size, 1)):
workers = max(args.concurrency, 1)
if workers == 1:
for record in batch:
ok, error = process_record(base_url, collection, record, headers, field, args.dry_run)
total += 1
success += int(ok)
if not ok and error:
failures.append(error)
else:
with ThreadPoolExecutor(max_workers=workers) as executor:
futures = {
executor.submit(
process_record,
base_url,
collection,
record,
headers,
field,
args.dry_run,
): record
for record in batch
}
for future in as_completed(futures):
ok, error = future.result()
total += 1
success += int(ok)
if not ok and error:
failures.append(error)
if args.throttle > 0:
time.sleep(args.throttle)
print(f" {success}/{total} records processed")
if failures:
print(f" {len(failures)} failures (showing up to 3):")
for message in failures[:3]:
print(f" - {message}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,60 @@
#!/bin/bash
# PocketBase Docker Setup Script
# Quickly spin up a PocketBase instance with Docker
set -e
echo "🚀 Setting up PocketBase with Docker..."
echo "========================================"
# Configuration
CONTAINER_NAME="pocketbase"
PORT="8090"
DATA_DIR="./pb_data"
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
echo "❌ Docker is not installed. Please install Docker first."
echo " Visit: https://docs.docker.com/get-docker/"
exit 1
fi
# Stop and remove existing container if it exists
if docker ps -a | grep -q "$CONTAINER_NAME"; then
echo "⚠️ Stopping existing PocketBase container..."
docker stop "$CONTAINER_NAME" > /dev/null 2>&1
docker rm "$CONTAINER_NAME" > /dev/null 2>&1
fi
# Create data directory
echo "📁 Creating data directory: $DATA_DIR"
mkdir -p "$DATA_DIR"
# Start new container
echo "🐳 Starting PocketBase container..."
docker run -d \
--name "$CONTAINER_NAME" \
-p "$PORT:8090" \
-v "$DATA_DIR:/pb/pb_data" \
ghcr.io/pocketbase/pocketbase:latest serve --http=0.0.0.0:8090
echo "========================================"
echo "✅ PocketBase is starting up!"
echo ""
echo "🌐 Admin UI: http://localhost:$PORT/_/"
echo "📖 API Docs: http://localhost:$PORT/api/docs"
echo "📁 Data directory: $DATA_DIR"
echo ""
echo "To view logs: docker logs -f $CONTAINER_NAME"
echo "To stop: docker stop $CONTAINER_NAME"
echo ""
echo "⏳ Waiting for PocketBase to be ready..."
sleep 3
# Check if container is running
if docker ps | grep -q "$CONTAINER_NAME"; then
echo "✅ PocketBase is running successfully!"
else
echo "❌ Something went wrong. Check logs with: docker logs $CONTAINER_NAME"
exit 1
fi