Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
|
||||||
410
agents/pocketbase-docs-researcher.md
Normal file
410
agents/pocketbase-docs-researcher.md
Normal 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
241
plugin.lock.json
Normal 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
359
skills/pocketbase/SKILL.md
Normal 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.
|
||||||
27
skills/pocketbase/assets/Caddyfile
Normal file
27
skills/pocketbase/assets/Caddyfile
Normal 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
|
||||||
|
}
|
||||||
104
skills/pocketbase/assets/collection-schema-template.json
Normal file
104
skills/pocketbase/assets/collection-schema-template.json
Normal 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"
|
||||||
|
}
|
||||||
35
skills/pocketbase/assets/docker-compose.yml
Normal file
35
skills/pocketbase/assets/docker-compose.yml
Normal 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:
|
||||||
275
skills/pocketbase/assets/frontend-template.html
Normal file
275
skills/pocketbase/assets/frontend-template.html
Normal 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>
|
||||||
56
skills/pocketbase/references/api/api_backups.md
Normal file
56
skills/pocketbase/references/api/api_backups.md
Normal 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.
|
||||||
158
skills/pocketbase/references/api/api_collections.md
Normal file
158
skills/pocketbase/references/api/api_collections.md
Normal 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.
|
||||||
57
skills/pocketbase/references/api/api_crons.md
Normal file
57
skills/pocketbase/references/api/api_crons.md
Normal 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.
|
||||||
58
skills/pocketbase/references/api/api_files.md
Normal file
58
skills/pocketbase/references/api/api_files.md
Normal 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.
|
||||||
68
skills/pocketbase/references/api/api_health.md
Normal file
68
skills/pocketbase/references/api/api_health.md
Normal 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.
|
||||||
53
skills/pocketbase/references/api/api_logs.md
Normal file
53
skills/pocketbase/references/api/api_logs.md
Normal 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.
|
||||||
648
skills/pocketbase/references/api/api_realtime.md
Normal file
648
skills/pocketbase/references/api/api_realtime.md
Normal 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
|
||||||
637
skills/pocketbase/references/api/api_records.md
Normal file
637
skills/pocketbase/references/api/api_records.md
Normal 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
|
||||||
97
skills/pocketbase/references/api/api_settings.md
Normal file
97
skills/pocketbase/references/api/api_settings.md
Normal 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.
|
||||||
396
skills/pocketbase/references/api_reference.md
Normal file
396
skills/pocketbase/references/api_reference.md
Normal 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**
|
||||||
664
skills/pocketbase/references/core/api_rules_filters.md
Normal file
664
skills/pocketbase/references/core/api_rules_filters.md
Normal file
@@ -0,0 +1,664 @@
|
|||||||
|
# API Rules and Filters
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
PocketBase uses rule expressions to control access to collections and records. Rules are evaluated server-side and determine who can create, read, update, or delete data.
|
||||||
|
|
||||||
|
## Rule Types
|
||||||
|
|
||||||
|
There are four main rule types for collections:
|
||||||
|
|
||||||
|
1. **List Rule** - Controls who can list/query multiple records
|
||||||
|
2. **View Rule** - Controls who can view individual records
|
||||||
|
3. **Create Rule** - Controls who can create new records
|
||||||
|
4. **Update Rule** - Controls who can update records
|
||||||
|
5. **Delete Rule** - Controls who can delete records
|
||||||
|
|
||||||
|
## Rule Syntax
|
||||||
|
|
||||||
|
### Basic Comparison Operators
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Equality
|
||||||
|
field = value
|
||||||
|
field != value
|
||||||
|
|
||||||
|
// String matching
|
||||||
|
field ~ "substring"
|
||||||
|
field !~ "substring"
|
||||||
|
field = "exact match"
|
||||||
|
|
||||||
|
// Numeric comparison
|
||||||
|
count > 10
|
||||||
|
age >= 18
|
||||||
|
price < 100
|
||||||
|
quantity != 0
|
||||||
|
|
||||||
|
// Date comparison
|
||||||
|
created >= "2024-01-01"
|
||||||
|
updated <= "2024-12-31"
|
||||||
|
published_date != null
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logical Operators
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// AND
|
||||||
|
condition1 && condition2
|
||||||
|
condition1 && condition2 && condition3
|
||||||
|
|
||||||
|
// OR
|
||||||
|
condition1 || condition2
|
||||||
|
|
||||||
|
// NOT
|
||||||
|
!(condition)
|
||||||
|
status != "draft"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special Variables
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
@request.auth // Current authenticated user object
|
||||||
|
@request.auth.id // Current user ID
|
||||||
|
@request.auth.email // User email
|
||||||
|
@request.auth.role // User role (admin, authenticated)
|
||||||
|
@request.auth.verified // Email verification status
|
||||||
|
@request.timestamp // Current server timestamp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field References
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Reference own field
|
||||||
|
user_id = @request.auth.id
|
||||||
|
|
||||||
|
// Reference nested field (for JSON fields)
|
||||||
|
settings.theme = "dark"
|
||||||
|
|
||||||
|
// Reference array field
|
||||||
|
tags ~ ["javascript", "react"]
|
||||||
|
|
||||||
|
// Reference all elements in array
|
||||||
|
categoryId ~ ["tech", "programming", "web"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Array Operations
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check if array contains value
|
||||||
|
tags ~ "javascript"
|
||||||
|
|
||||||
|
// Check if any array element matches condition
|
||||||
|
categories.id ~ ["cat1", "cat2"]
|
||||||
|
|
||||||
|
// Check if array is not empty
|
||||||
|
images != []
|
||||||
|
|
||||||
|
// Check if array is empty
|
||||||
|
images = []
|
||||||
|
```
|
||||||
|
|
||||||
|
### String Operations
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Pattern matching with wildcards
|
||||||
|
title ~ "Hello*"
|
||||||
|
|
||||||
|
// Case-sensitive regex
|
||||||
|
content ~ /pattern/i
|
||||||
|
|
||||||
|
// Starts with
|
||||||
|
title ~ "^Getting started"
|
||||||
|
|
||||||
|
// Contains
|
||||||
|
description ~ "important"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Rule Patterns
|
||||||
|
|
||||||
|
### Owner-Based Access Control
|
||||||
|
|
||||||
|
**Users can only access their own records**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// List Rule - show only user's records in lists
|
||||||
|
user_id = @request.auth.id
|
||||||
|
|
||||||
|
// View Rule - can only view own records
|
||||||
|
user_id = @request.auth.id
|
||||||
|
|
||||||
|
// Create Rule - only authenticated users can create
|
||||||
|
@request.auth.id != ""
|
||||||
|
|
||||||
|
// Update Rule - only owner can update
|
||||||
|
user_id = @request.auth.id
|
||||||
|
|
||||||
|
// Delete Rule - only owner can delete
|
||||||
|
user_id = @request.auth.id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Public Read, Authenticated Write
|
||||||
|
|
||||||
|
**Anyone can read, only authenticated users can create/modify**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// List Rule - public can read
|
||||||
|
status = "published"
|
||||||
|
|
||||||
|
// View Rule - public can view published items
|
||||||
|
status = "published"
|
||||||
|
|
||||||
|
// Create Rule - authenticated users only
|
||||||
|
@request.auth.id != ""
|
||||||
|
|
||||||
|
// Update Rule - author or admin can update
|
||||||
|
author_id = @request.auth.id || @request.auth.role = "admin"
|
||||||
|
|
||||||
|
// Delete Rule - author or admin can delete
|
||||||
|
author_id = @request.auth.id || @request.auth.role = "admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Role-Based Access
|
||||||
|
|
||||||
|
**Different permissions based on user role**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Admins can do everything
|
||||||
|
List Rule: true
|
||||||
|
View Rule: true
|
||||||
|
Create Rule: @request.auth.role = "admin"
|
||||||
|
Update Rule: @request.auth.role = "admin"
|
||||||
|
Delete Rule: @request.auth.role = "admin"
|
||||||
|
|
||||||
|
// Moderators can manage non-admin content
|
||||||
|
List Rule: true
|
||||||
|
View Rule: true
|
||||||
|
Create Rule: @request.auth.role = "moderator" || @request.auth.role = "admin"
|
||||||
|
Update Rule: @request.auth.role = "moderator" || @request.auth.role = "admin"
|
||||||
|
Delete Rule: @request.auth.role = "admin"
|
||||||
|
|
||||||
|
// Regular users have limited access
|
||||||
|
List Rule: @request.auth.role != ""
|
||||||
|
View Rule: @request.auth.role != ""
|
||||||
|
Create Rule: @request.auth.role = "authenticated"
|
||||||
|
Update Rule: false
|
||||||
|
Delete Rule: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status-Based Access
|
||||||
|
|
||||||
|
**Access based on record status**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Only show published content publicly
|
||||||
|
List Rule: status = "published"
|
||||||
|
|
||||||
|
// Drafts visible to authors
|
||||||
|
List Rule: status = "published" || author_id = @request.auth.id
|
||||||
|
|
||||||
|
// Published items visible to all
|
||||||
|
View Rule: status = "published"
|
||||||
|
|
||||||
|
// Authors can edit their own
|
||||||
|
Update Rule: author_id = @request.auth.id
|
||||||
|
|
||||||
|
// Deletion only for drafts
|
||||||
|
Delete Rule: status = "draft"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verified User Only
|
||||||
|
|
||||||
|
**Only verified users can interact**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Only verified users
|
||||||
|
Create Rule: @request.auth.verified = true
|
||||||
|
Update Rule: @request.auth.verified = true
|
||||||
|
Delete Rule: @request.auth.verified = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Time-Based Access
|
||||||
|
|
||||||
|
**Access based on time constraints**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Only future events
|
||||||
|
start_date > @request.timestamp
|
||||||
|
|
||||||
|
// Only published items or drafts for authors
|
||||||
|
status = "published" || (status = "draft" && author_id = @request.auth.id)
|
||||||
|
|
||||||
|
// Only items from last 30 days
|
||||||
|
created >= dateSubtract(@request.timestamp, 30, "days")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex Multi-Condition Rules
|
||||||
|
|
||||||
|
**E-commerce order access**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Customers can view their own orders
|
||||||
|
List Rule: user_id = @request.auth.id
|
||||||
|
View Rule: user_id = @request.auth.id
|
||||||
|
|
||||||
|
// Staff can view all orders
|
||||||
|
View Rule: @request.auth.role = "staff" || user_id = @request.auth.id
|
||||||
|
|
||||||
|
// Only staff can create orders for customers
|
||||||
|
Create Rule: @request.auth.role = "staff"
|
||||||
|
|
||||||
|
// Customers can update their orders only if pending
|
||||||
|
Update Rule: (user_id = @request.auth.id && status = "pending") || @request.auth.role = "staff"
|
||||||
|
|
||||||
|
// Only staff can cancel orders
|
||||||
|
Delete Rule: @request.auth.role = "staff"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering in Queries
|
||||||
|
|
||||||
|
Rules control access, but you can also filter data in queries.
|
||||||
|
|
||||||
|
### Basic Filters
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Equality
|
||||||
|
const records = await pb.collection('posts').getList(1, 50, {
|
||||||
|
filter: 'status = "published"'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Not equal
|
||||||
|
filter: 'category != "draft"'
|
||||||
|
|
||||||
|
// Multiple conditions
|
||||||
|
filter: 'status = "published" && created >= "2024-01-01"'
|
||||||
|
|
||||||
|
// OR condition
|
||||||
|
filter: 'category = "tech" || category = "programming"'
|
||||||
|
```
|
||||||
|
|
||||||
|
### String Filters
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Contains substring
|
||||||
|
filter: 'title ~ "PocketBase"'
|
||||||
|
|
||||||
|
// Not contains
|
||||||
|
filter: 'content !~ "spam"'
|
||||||
|
|
||||||
|
// Pattern matching
|
||||||
|
filter: 'title ~ "Getting started*"'
|
||||||
|
|
||||||
|
// Regex (case insensitive)
|
||||||
|
filter: 'content ~ /important/i'
|
||||||
|
|
||||||
|
// Starts with
|
||||||
|
filter: 'email ~ "^admin@"'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Numeric Filters
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Greater than
|
||||||
|
filter: 'price > 100'
|
||||||
|
|
||||||
|
// Greater than or equal
|
||||||
|
filter: 'age >= 18'
|
||||||
|
|
||||||
|
// Less than
|
||||||
|
filter: 'stock < 10'
|
||||||
|
|
||||||
|
// Less than or equal
|
||||||
|
filter: 'price <= 50'
|
||||||
|
|
||||||
|
// Between (inclusive)
|
||||||
|
filter: 'price >= 10 && price <= 100'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Date Filters
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// After date
|
||||||
|
filter: 'created >= "2024-01-01"'
|
||||||
|
|
||||||
|
// Before date
|
||||||
|
filter: 'event_date <= "2024-12-31"'
|
||||||
|
|
||||||
|
// Date range
|
||||||
|
filter: 'created >= "2024-01-01" && created <= "2024-12-31"'
|
||||||
|
|
||||||
|
// Last 30 days
|
||||||
|
filter: 'created >= dateSubtract(@request.timestamp, 30, "days")'
|
||||||
|
|
||||||
|
// Next 7 days
|
||||||
|
filter: 'event_date <= dateAdd(@request.timestamp, 7, "days")'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Array Filters
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Array contains value
|
||||||
|
filter: 'tags ~ "javascript"'
|
||||||
|
|
||||||
|
// Array contains any of multiple values
|
||||||
|
filter: 'tags ~ ["javascript", "react", "vue"]'
|
||||||
|
|
||||||
|
// Array does not contain value
|
||||||
|
filter: 'categories !~ "private"'
|
||||||
|
|
||||||
|
// Check if array is not empty
|
||||||
|
filter: 'images != []'
|
||||||
|
|
||||||
|
// Check if array is empty
|
||||||
|
filter: 'comments = []'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relation Filters
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Filter by related record field
|
||||||
|
filter: 'author.email = "user@example.com"'
|
||||||
|
|
||||||
|
// Expand and filter
|
||||||
|
filter: 'expand.author.role = "admin"'
|
||||||
|
|
||||||
|
// Multiple relation levels
|
||||||
|
filter: 'expand.post.expand.author.role = "moderator"'
|
||||||
|
```
|
||||||
|
|
||||||
|
### NULL Checks
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Field is not null
|
||||||
|
filter: 'published_date != null'
|
||||||
|
|
||||||
|
// Field is null
|
||||||
|
filter: 'archived_date = null'
|
||||||
|
|
||||||
|
// Field exists (not null or empty string)
|
||||||
|
filter: 'deleted != ""'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sorting
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Sort by single field
|
||||||
|
sort: 'created'
|
||||||
|
|
||||||
|
// Sort by field descending
|
||||||
|
sort: '-created'
|
||||||
|
|
||||||
|
// Sort by multiple fields
|
||||||
|
sort: 'status,-created'
|
||||||
|
|
||||||
|
// Sort by numeric field
|
||||||
|
sort: 'price'
|
||||||
|
|
||||||
|
// Sort by string field (alphabetical)
|
||||||
|
sort: 'title'
|
||||||
|
|
||||||
|
// Sort by relation field
|
||||||
|
sort: 'expand.author.name'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Selection
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Select specific fields
|
||||||
|
fields: 'id,title,author,created'
|
||||||
|
|
||||||
|
// Exclude large fields
|
||||||
|
fields: 'id,title,author,-content'
|
||||||
|
|
||||||
|
// Select all fields
|
||||||
|
fields: '*'
|
||||||
|
|
||||||
|
// Select with relations
|
||||||
|
fields: 'id,title,expand.author.name'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get page 1 with 50 items per page
|
||||||
|
const page1 = await pb.collection('posts').getList(1, 50)
|
||||||
|
|
||||||
|
// Get page 2
|
||||||
|
const page2 = await pb.collection('posts').getList(2, 50)
|
||||||
|
|
||||||
|
// Get all (use carefully - can be slow)
|
||||||
|
const all = await pb.collection('posts').getFullList(200)
|
||||||
|
|
||||||
|
// Get with cursor-based pagination (PocketBase 0.20+)
|
||||||
|
const records = await pb.collection('posts').getList(1, 50, {
|
||||||
|
filter: 'created >= "2024-01-01"',
|
||||||
|
sort: 'created'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relation Expansion
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Expand single relation
|
||||||
|
expand: 'author'
|
||||||
|
|
||||||
|
// Expand multiple relations
|
||||||
|
expand: 'author,comments'
|
||||||
|
|
||||||
|
// Expand nested relations
|
||||||
|
expand: 'author,comments.author'
|
||||||
|
|
||||||
|
// Access expanded data
|
||||||
|
const post = await pb.collection('posts').getOne('POST_ID', {
|
||||||
|
expand: 'author'
|
||||||
|
});
|
||||||
|
console.log(post.expand.author.email);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Filter Functions
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Date arithmetic
|
||||||
|
filter: 'created >= dateSubtract(@request.timestamp, 7, "days")'
|
||||||
|
|
||||||
|
// String length
|
||||||
|
filter: 'length(title) > 10'
|
||||||
|
|
||||||
|
// Count array elements
|
||||||
|
filter: 'count(tags) > 0'
|
||||||
|
|
||||||
|
// Case-insensitive matching
|
||||||
|
filter: 'lower(name) = lower("JOHN")'
|
||||||
|
|
||||||
|
// Extract JSON field
|
||||||
|
filter: 'settings->theme = "dark"'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Indexing for Filters
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create indexes for commonly filtered fields
|
||||||
|
CREATE INDEX idx_posts_status ON posts(status);
|
||||||
|
CREATE INDEX idx_posts_created ON posts(created);
|
||||||
|
CREATE INDEX idx_posts_author ON posts(author_id);
|
||||||
|
CREATE INDEX idx_posts_status_created ON posts(status, created);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Efficient Rules
|
||||||
|
|
||||||
|
**Good:**
|
||||||
|
```javascript
|
||||||
|
// Simple, indexed field comparison
|
||||||
|
user_id = @request.auth.id
|
||||||
|
status = "published"
|
||||||
|
created >= "2024-01-01"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Avoid (can be slow):**
|
||||||
|
```javascript
|
||||||
|
// Complex string matching
|
||||||
|
title ~ /javascript.*framework/i
|
||||||
|
// Use equals or prefix matching instead
|
||||||
|
title = "JavaScript Framework"
|
||||||
|
|
||||||
|
// Nested relation checks
|
||||||
|
expand.post.expand.author.role = "admin"
|
||||||
|
// Pre-compute or use views
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pagination Best Practices
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Always paginate large datasets
|
||||||
|
const records = await pb.collection('posts').getList(1, 50, {
|
||||||
|
filter: 'status = "published"',
|
||||||
|
sort: '-created',
|
||||||
|
fields: 'id,title,author,created' // Select only needed fields
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use cursor-based pagination for infinite scroll
|
||||||
|
let cursor = null;
|
||||||
|
const batch1 = await pb.collection('posts').getList(1, 50, {
|
||||||
|
sort: 'created'
|
||||||
|
});
|
||||||
|
cursor = batch1.items[batch1.items.length - 1].created;
|
||||||
|
|
||||||
|
const batch2 = await pb.collection('posts').getList(1, 50, {
|
||||||
|
filter: `created < "${cursor}"`,
|
||||||
|
sort: 'created'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-time and Rules
|
||||||
|
|
||||||
|
Real-time subscriptions respect the same rules:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Subscribe to changes
|
||||||
|
pb.collection('posts').subscribe('*', function(e) {
|
||||||
|
console.log(e.action); // 'create', 'update', 'delete'
|
||||||
|
console.log(e.record); // Changed record
|
||||||
|
});
|
||||||
|
|
||||||
|
// User will only receive events for records they have access to
|
||||||
|
// based on their current rules
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Rules
|
||||||
|
|
||||||
|
### Test as Different Users
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Test public access
|
||||||
|
const publicPosts = await pb.collection('posts').getList(1, 50);
|
||||||
|
// Should respect public rules
|
||||||
|
|
||||||
|
// Test authenticated access
|
||||||
|
pb.collection('users').authWithPassword('user@example.com', 'password');
|
||||||
|
const userPosts = await pb.collection('posts').getList(1, 50);
|
||||||
|
// Should show more based on rules
|
||||||
|
|
||||||
|
// Test admin access
|
||||||
|
pb.admins.authWithPassword('admin@example.com', 'password');
|
||||||
|
const adminPosts = await pb.collection('posts').getList(1, 50);
|
||||||
|
// Should show everything
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rule Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Public users see appropriate data
|
||||||
|
- [ ] Authenticated users see correct data
|
||||||
|
- [ ] Users can't access others' private data
|
||||||
|
- [ ] Admins have full access
|
||||||
|
- [ ] Create rules work for authorized users
|
||||||
|
- [ ] Create rules block unauthorized users
|
||||||
|
- [ ] Update rules work correctly
|
||||||
|
- [ ] Delete rules work correctly
|
||||||
|
- [ ] Real-time updates respect rules
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### 1. Forgetting List vs View Rules
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// WRONG - Both rules same
|
||||||
|
List Rule: user_id = @request.auth.id
|
||||||
|
View Rule: user_id = @request.auth.id
|
||||||
|
|
||||||
|
// RIGHT - Public can view, private in lists
|
||||||
|
List Rule: status = "published"
|
||||||
|
View Rule: status = "published" || user_id = @request.auth.id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Using Wrong Comparison
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// WRONG - string comparison for numbers
|
||||||
|
price > "100"
|
||||||
|
|
||||||
|
// RIGHT - numeric comparison
|
||||||
|
price > 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Not Indexing Filtered Fields
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// If filtering by 'status', ensure index exists
|
||||||
|
CREATE INDEX idx_posts_status ON posts(status);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Over-restrictive Rules
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Too restrictive - breaks functionality
|
||||||
|
List Rule: false // No one can see anything
|
||||||
|
|
||||||
|
// Better - allow authenticated users to read
|
||||||
|
List Rule: @request.auth.id != ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Forgetting to Handle NULL
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// May not work if published_date is null
|
||||||
|
filter: 'published_date >= "2024-01-01"'
|
||||||
|
|
||||||
|
// Better - handle nulls explicitly
|
||||||
|
filter: 'published_date != null && published_date >= "2024-01-01"'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Start with restrictive rules**
|
||||||
|
```javascript
|
||||||
|
// Default to no access
|
||||||
|
Create Rule: @request.auth.role = "admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test rules thoroughly**
|
||||||
|
- Test as different user types
|
||||||
|
- Verify data isolation
|
||||||
|
- Check edge cases
|
||||||
|
|
||||||
|
3. **Log and monitor**
|
||||||
|
- Check for unauthorized access attempts
|
||||||
|
- Monitor rule performance
|
||||||
|
- Track slow queries
|
||||||
|
|
||||||
|
4. **Use views for complex access logic**
|
||||||
|
- Pre-compute expensive checks
|
||||||
|
- Simplify rule logic
|
||||||
|
|
||||||
|
5. **Regular security audits**
|
||||||
|
- Review rules periodically
|
||||||
|
- Check for privilege escalation
|
||||||
|
- Verify data isolation
|
||||||
|
|
||||||
|
## Related Topics
|
||||||
|
|
||||||
|
- [Collections](collections.md) - Collection configuration
|
||||||
|
- [Authentication](authentication.md) - User management
|
||||||
|
- [Working with Relations](working_with_relations.md) - Relationship patterns
|
||||||
|
- [Security Rules](../security_rules.md) - Comprehensive security patterns
|
||||||
|
- [API Records](../api_records.md) - Record CRUD operations
|
||||||
583
skills/pocketbase/references/core/authentication.md
Normal file
583
skills/pocketbase/references/core/authentication.md
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
# Authentication in PocketBase
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
PocketBase provides comprehensive authentication features including:
|
||||||
|
- Email/password authentication
|
||||||
|
- OAuth2 integration (Google, GitHub, etc.)
|
||||||
|
- Magic link authentication
|
||||||
|
- Email verification
|
||||||
|
- Password reset
|
||||||
|
- JWT token management
|
||||||
|
- Role-based access control
|
||||||
|
|
||||||
|
## Auth Collections
|
||||||
|
|
||||||
|
User accounts are managed through **Auth Collections**. Unlike base collections, auth collections:
|
||||||
|
- Have built-in authentication fields
|
||||||
|
- Support OAuth2 providers
|
||||||
|
- Provide password management
|
||||||
|
- Include email verification
|
||||||
|
- Generate JWT tokens
|
||||||
|
|
||||||
|
### Built-in Auth Fields
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string (unique)",
|
||||||
|
"email": "string (required, unique)",
|
||||||
|
"password": "string (hashed)",
|
||||||
|
"passwordConfirm": "string (validation only)",
|
||||||
|
"emailVisibility": "boolean (default: true)",
|
||||||
|
"verified": "boolean (default: false)",
|
||||||
|
"created": "datetime (autodate)",
|
||||||
|
"updated": "datetime (autodate)",
|
||||||
|
"lastResetSentAt": "datetime",
|
||||||
|
"verificationToken": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Password fields are never returned in API responses for security.
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
### Email/Password
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Register new user
|
||||||
|
const authData = await pb.collection('users').create({
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
passwordConfirm: 'password123',
|
||||||
|
name: 'John Doe' // custom field
|
||||||
|
});
|
||||||
|
|
||||||
|
// Returns:
|
||||||
|
// {
|
||||||
|
// token: "JWT_TOKEN",
|
||||||
|
// user: { ...user data... }
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Automatic password hashing
|
||||||
|
- Email uniqueness validation
|
||||||
|
- Email verification (if enabled)
|
||||||
|
- Custom fields supported
|
||||||
|
|
||||||
|
### OAuth2 Registration/Login
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// With OAuth2 code (from provider redirect)
|
||||||
|
const authData = await pb.collection('users').authWithOAuth2({
|
||||||
|
provider: 'google',
|
||||||
|
code: 'OAUTH2_CODE_FROM_GOOGLE'
|
||||||
|
});
|
||||||
|
|
||||||
|
// With existing access token
|
||||||
|
const authData = await pb.collection('users').authWithOAuth2({
|
||||||
|
provider: 'github',
|
||||||
|
accessToken: 'USER_ACCESS_TOKEN'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Supported Providers:**
|
||||||
|
- Google
|
||||||
|
- GitHub
|
||||||
|
- GitLab
|
||||||
|
- Discord
|
||||||
|
- Facebook
|
||||||
|
- Microsoft
|
||||||
|
- Spotify
|
||||||
|
- Twitch
|
||||||
|
- Discord
|
||||||
|
- Twitter/X
|
||||||
|
|
||||||
|
**Custom OAuth2 Configuration:**
|
||||||
|
1. Go to Auth Collections → OAuth2 providers
|
||||||
|
2. Add provider with client ID/secret
|
||||||
|
3. Configure redirect URL
|
||||||
|
4. Enable provider
|
||||||
|
|
||||||
|
### Magic Link Authentication
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Send magic link
|
||||||
|
await pb.collection('users').requestPasswordReset('user@example.com');
|
||||||
|
|
||||||
|
// User clicks link (URL contains token)
|
||||||
|
// Reset password (returns 204 on success)
|
||||||
|
await pb.collection('users').confirmPasswordReset(
|
||||||
|
'RESET_TOKEN',
|
||||||
|
'newPassword123',
|
||||||
|
'newPassword123'
|
||||||
|
);
|
||||||
|
|
||||||
|
// After confirming, prompt the user to sign in again with the new password.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Login
|
||||||
|
|
||||||
|
### Standard Login
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Email and password
|
||||||
|
const authData = await pb.collection('users').authWithPassword(
|
||||||
|
'user@example.com',
|
||||||
|
'password123'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Access token and user data
|
||||||
|
console.log(authData.token); // JWT token
|
||||||
|
console.log(authData.user); // User record
|
||||||
|
|
||||||
|
// Token is automatically stored
|
||||||
|
console.log(pb.authStore.token); // Access stored token
|
||||||
|
```
|
||||||
|
|
||||||
|
### OAuth2 Login
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Same as registration - creates user if doesn't exist
|
||||||
|
const authData = await pb.collection('users').authWithOAuth2({
|
||||||
|
provider: 'google',
|
||||||
|
code: 'OAUTH2_CODE'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Magic Link Login
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Request magic link
|
||||||
|
await pb.collection('users').requestVerification('user@example.com');
|
||||||
|
|
||||||
|
// User clicks verification link (returns 204 on success)
|
||||||
|
await pb.collection('users').confirmVerification('VERIFICATION_TOKEN');
|
||||||
|
|
||||||
|
// Verification does not log the user in automatically; call authWithPassword or another auth method.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auth State Management
|
||||||
|
|
||||||
|
### Checking Auth Status
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check if user is authenticated
|
||||||
|
if (pb.authStore.isValid) {
|
||||||
|
const user = pb.authStore.model;
|
||||||
|
console.log('User is logged in:', user.email);
|
||||||
|
} else {
|
||||||
|
console.log('User is not logged in');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
const user = pb.authStore.model;
|
||||||
|
|
||||||
|
// Refresh auth state
|
||||||
|
await pb.collection('users').authRefresh();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Store Persistence
|
||||||
|
|
||||||
|
The default auth store persists tokens in `localStorage` when available and falls back to an in-memory store otherwise. Call `pb.authStore.clear()` to invalidate the current session. For custom storage implementations, extend the SDK `BaseAuthStore` as described in the [official JS SDK README](https://github.com/pocketbase/js-sdk#auth-store).
|
||||||
|
|
||||||
|
### React Auth Hook
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
function useAuth(pb) {
|
||||||
|
const [user, setUser] = useState(pb.authStore.model);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = pb.authStore.onChange((token, model) => {
|
||||||
|
setUser(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsub();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { user };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
function App() {
|
||||||
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||||
|
const { user } = useAuth(pb);
|
||||||
|
|
||||||
|
return user ? (
|
||||||
|
<div>Welcome, {user.email}!</div>
|
||||||
|
) : (
|
||||||
|
<div>Please log in</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logout
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Clear auth state
|
||||||
|
pb.authStore.clear();
|
||||||
|
|
||||||
|
// After logout, authStore.model will be null
|
||||||
|
console.log(pb.authStore.model); // null
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protected Routes (Frontend)
|
||||||
|
|
||||||
|
### React Router Protection
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
function ProtectedRoute({ children }) {
|
||||||
|
const { user } = useAuth(pb);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Navigate to="/login" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Route
|
||||||
|
path="/dashboard"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Dashboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vanilla JS Protection
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check before API call
|
||||||
|
function requireAuth() {
|
||||||
|
if (!pb.authStore.isValid) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
if (requireAuth()) {
|
||||||
|
const posts = await pb.collection('posts').getList(1, 50);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Profile Management
|
||||||
|
|
||||||
|
### Update User Data
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Update user profile
|
||||||
|
const updated = await pb.collection('users').update(user.id, {
|
||||||
|
name: 'Jane Doe',
|
||||||
|
bio: 'Updated bio',
|
||||||
|
avatar: fileInput.files[0] // File upload
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only authenticated user can update their own profile
|
||||||
|
// unless using admin API
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change Password
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Change password (requires current password)
|
||||||
|
const updated = await pb.collection('users').update(user.id, {
|
||||||
|
oldPassword: 'currentPassword123',
|
||||||
|
password: 'newPassword123',
|
||||||
|
passwordConfirm: 'newPassword123'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Email
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Update email (triggers verification if enabled)
|
||||||
|
const updated = await pb.collection('users').update(user.id, {
|
||||||
|
email: 'newemail@example.com'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Verification
|
||||||
|
|
||||||
|
### Enable Email Verification
|
||||||
|
|
||||||
|
1. Go to Auth Collections → Options
|
||||||
|
2. Enable "Email verification"
|
||||||
|
3. Customize verification page
|
||||||
|
4. Save
|
||||||
|
|
||||||
|
### Manual Verification
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Request verification email
|
||||||
|
await pb.collection('users').requestVerification('user@example.com');
|
||||||
|
|
||||||
|
// User clicks link with token
|
||||||
|
const authData = await pb.collection('users').confirmVerification('TOKEN_FROM_URL');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Verify on OAuth
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// OAuth users can be auto-verified
|
||||||
|
// Configure in Auth Collections → OAuth2 providers
|
||||||
|
// Check "Auto-verification"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Reset
|
||||||
|
|
||||||
|
### Request Reset
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Send password reset email
|
||||||
|
await pb.collection('users').requestPasswordReset('user@example.com');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Confirm Reset
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// User clicks link from email
|
||||||
|
// Reset with token from URL
|
||||||
|
const authData = await pb.collection('users').confirmPasswordReset(
|
||||||
|
'TOKEN_FROM_URL',
|
||||||
|
'newPassword123',
|
||||||
|
'newPassword123'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## OAuth2 Configuration
|
||||||
|
|
||||||
|
### Google OAuth2 Setup
|
||||||
|
|
||||||
|
1. **Google Cloud Console**
|
||||||
|
- Create new project or select existing
|
||||||
|
- Enable Google+ API
|
||||||
|
- Create OAuth 2.0 credentials
|
||||||
|
- Add authorized redirect URIs:
|
||||||
|
- `http://localhost:8090/api/oauth2/google/callback`
|
||||||
|
- `https://yourdomain.com/api/oauth2/google/callback`
|
||||||
|
|
||||||
|
2. **PocketBase Admin UI**
|
||||||
|
- Go to Auth Collections → OAuth2 providers
|
||||||
|
- Click "Google"
|
||||||
|
- Enter Client ID and Client Secret
|
||||||
|
- Save
|
||||||
|
|
||||||
|
### GitHub OAuth2 Setup
|
||||||
|
|
||||||
|
1. **GitHub Developer Settings**
|
||||||
|
- New OAuth App
|
||||||
|
- Authorization callback URL:
|
||||||
|
- `http://localhost:8090/api/oauth2/github/callback`
|
||||||
|
- Get Client ID and Secret
|
||||||
|
|
||||||
|
2. **PocketBase Admin UI**
|
||||||
|
- Configure GitHub provider
|
||||||
|
- Enter credentials
|
||||||
|
|
||||||
|
### Custom OAuth2 Provider
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Most providers follow similar pattern:
|
||||||
|
// 1. Redirect to provider auth page
|
||||||
|
// 2. Provider redirects back with code
|
||||||
|
// 3. Exchange code for access token
|
||||||
|
// 4. Use access token with PocketBase
|
||||||
|
|
||||||
|
// Example: Discord
|
||||||
|
window.location.href =
|
||||||
|
`https://discord.com/api/oauth2/authorize?client_id=CLIENT_ID&redirect_uri=${encodeURIComponent('http://localhost:8090/_/')}&response_type=code&scope=identify%20email`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## JWT Token Details
|
||||||
|
|
||||||
|
### Token Structure
|
||||||
|
|
||||||
|
JWT tokens consist of three parts:
|
||||||
|
- **Header** - Algorithm and token type
|
||||||
|
- **Payload** - User data and claims
|
||||||
|
- **Signature** - HMAC validation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Payload includes (fields may vary depending on the auth collection):
|
||||||
|
{
|
||||||
|
"id": "USER_ID",
|
||||||
|
"collectionId": "COLLECTION_ID",
|
||||||
|
"collectionName": "users",
|
||||||
|
"exp": 1234567890, // expires at
|
||||||
|
"iat": 1234567890 // issued at
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Expiration
|
||||||
|
|
||||||
|
- Default expiration: 7 days
|
||||||
|
- Can be customized in Auth Collections → Options
|
||||||
|
- Tokens remain valid until `exp`; call `pb.collection('users').authRefresh()` to refresh.
|
||||||
|
|
||||||
|
### Manual Token Validation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check if token is still valid
|
||||||
|
if (pb.authStore.isValid) {
|
||||||
|
// Token is valid
|
||||||
|
const user = pb.authStore.model;
|
||||||
|
} else {
|
||||||
|
// Token expired or invalid
|
||||||
|
// Redirect to login
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### Password Security
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Configure in Auth Collections → Options
|
||||||
|
{
|
||||||
|
"minPasswordLength": 8,
|
||||||
|
"requirePasswordUppercase": true,
|
||||||
|
"requirePasswordLowercase": true,
|
||||||
|
"requirePasswordNumbers": true,
|
||||||
|
"requirePasswordSymbols": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Account Security
|
||||||
|
|
||||||
|
1. **Enable Email Verification**
|
||||||
|
- Prevent fake accounts
|
||||||
|
- Verify user email ownership
|
||||||
|
|
||||||
|
2. **Implement Rate Limiting**
|
||||||
|
- Prevent brute force attacks
|
||||||
|
- Configure at reverse proxy level
|
||||||
|
|
||||||
|
3. **Use HTTPS in Production**
|
||||||
|
- Encrypt data in transit
|
||||||
|
- Required for OAuth2
|
||||||
|
|
||||||
|
4. **Set Appropriate Token Expiration**
|
||||||
|
- Balance security and UX
|
||||||
|
- Consider refresh tokens
|
||||||
|
|
||||||
|
5. **Validate OAuth State**
|
||||||
|
- Prevent CSRF attacks
|
||||||
|
- Implement proper state parameter
|
||||||
|
|
||||||
|
### Common Auth Rules
|
||||||
|
|
||||||
|
**Users can only access their own data:**
|
||||||
|
```
|
||||||
|
user_id = @request.auth.id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verified users only:**
|
||||||
|
```
|
||||||
|
@request.auth.verified = true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Admins only:**
|
||||||
|
```
|
||||||
|
@request.auth.role = 'admin'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Role-based access:**
|
||||||
|
```
|
||||||
|
@request.auth.role = 'moderator' || @request.auth.role = 'admin'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Tenant Authentication
|
||||||
|
|
||||||
|
### Workspace/Team Model
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// collections:
|
||||||
|
// - users (auth) - email, password
|
||||||
|
// - workspaces (base) - name, owner_id
|
||||||
|
// - workspace_members (base) - workspace_id, user_id, role
|
||||||
|
|
||||||
|
// Users can access workspaces they're members of:
|
||||||
|
List Rule: "id != '' && (@request.auth.id != '')"
|
||||||
|
View Rule: "members.user_id ?~ @request.auth.id"
|
||||||
|
|
||||||
|
// On login, filter workspace by user membership
|
||||||
|
async function getUserWorkspaces() {
|
||||||
|
const memberships = await pb.collection('workspace_members').getList(1, 100, {
|
||||||
|
filter: `user_id = "${pb.authStore.model.id}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceIds = memberships.items.map(m => m.workspace_id);
|
||||||
|
return workspaceIds;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auth API Reference
|
||||||
|
|
||||||
|
### User Methods
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Auth collection methods
|
||||||
|
pb.collection('users').create() // Register
|
||||||
|
pb.collection('users').authWithPassword() // Login
|
||||||
|
pb.collection('users).authWithOAuth2() // OAuth2
|
||||||
|
pb.collection('users).authRefresh() // Refresh
|
||||||
|
pb.collection('users).requestVerification() // Send verification
|
||||||
|
pb.collection('users).confirmVerification() // Verify
|
||||||
|
pb.collection('users).requestPasswordReset() // Reset request
|
||||||
|
pb.collection('users).confirmPasswordReset() // Confirm reset
|
||||||
|
|
||||||
|
// Admin methods
|
||||||
|
pb.collection('users').getOne(id) // Get user
|
||||||
|
pb.collection('users).update(id, data) // Update user
|
||||||
|
pb.collection('users).delete(id) // Delete user
|
||||||
|
pb.collection('users').listAuthMethods() // List allowed auth methods and OAuth providers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Login not working**
|
||||||
|
- Check email/password correctness
|
||||||
|
- Verify user exists
|
||||||
|
- Check if account is verified (if verification required)
|
||||||
|
- Check auth rules don't block access
|
||||||
|
|
||||||
|
**OAuth2 redirect errors**
|
||||||
|
- Verify redirect URI matches exactly
|
||||||
|
- Check provider configuration
|
||||||
|
- Ensure HTTPS in production
|
||||||
|
- Check CORS settings
|
||||||
|
|
||||||
|
**Token expired**
|
||||||
|
- Use authRefresh() to get new token
|
||||||
|
- Check token expiration time
|
||||||
|
- Implement auto-refresh logic
|
||||||
|
|
||||||
|
**Password reset not working**
|
||||||
|
- Check if email exists
|
||||||
|
- Verify reset link wasn't used
|
||||||
|
- Check spam folder
|
||||||
|
- Verify email sending configuration
|
||||||
|
|
||||||
|
**Can't access protected data**
|
||||||
|
- Check auth rules
|
||||||
|
- Verify user is authenticated
|
||||||
|
- Check user permissions
|
||||||
|
- Verify collection rules
|
||||||
|
|
||||||
|
## Related Topics
|
||||||
|
|
||||||
|
- [Collections](collections.md) - Auth collection details
|
||||||
|
- [API Rules & Filters](api_rules_filters.md) - Security rules
|
||||||
|
- [Files Handling](files_handling.md) - File uploads
|
||||||
|
- [Security Rules](../security_rules.md) - Comprehensive access control
|
||||||
|
- [Going to Production](going_to_production.md) - Production security
|
||||||
514
skills/pocketbase/references/core/cli_commands.md
Normal file
514
skills/pocketbase/references/core/cli_commands.md
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
# PocketBase CLI Commands
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The PocketBase Command Line Interface (CLI) provides powerful tools for managing your PocketBase instances, including server operations, database migrations, user management, and system administration. The CLI is essential for development workflows, production deployments, and automation tasks.
|
||||||
|
|
||||||
|
## Installation & Setup
|
||||||
|
|
||||||
|
The PocketBase CLI is built into the main PocketBase executable. After downloading PocketBase, the CLI is available via the `./pocketbase` command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make sure the executable has proper permissions
|
||||||
|
chmod +x pocketbase
|
||||||
|
|
||||||
|
# Verify CLI is working
|
||||||
|
./pocketbase --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Flags
|
||||||
|
|
||||||
|
These flags can be used with any PocketBase CLI command:
|
||||||
|
|
||||||
|
| Flag | Description | Default |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `--automigrate` | Enable/disable auto migrations | `true` |
|
||||||
|
| `--dev` | Enable dev mode (prints logs and SQL statements to console) | `false` |
|
||||||
|
| `--dir string` | The PocketBase data directory | `"pb_data"` |
|
||||||
|
| `--encryptionEnv string` | Env variable with 32-char value for app settings encryption | `none` |
|
||||||
|
| `--hooksDir string` | Directory with JS app hooks | |
|
||||||
|
| `--hooksPool int` | Total prewarm goja.Runtime instances for JS hooks execution | `15` |
|
||||||
|
| `--hooksWatch` | Auto restart on pb_hooks file change (no effect on Windows) | `true` |
|
||||||
|
| `--indexFallback` | Fallback to index.html on missing static path (for SPA pretty URLs) | `true` |
|
||||||
|
| `--migrationsDir string` | Directory with user-defined migrations | |
|
||||||
|
| `--publicDir string` | Directory to serve static files | `"pb_public"` |
|
||||||
|
| `--queryTimeout int` | Default SELECT queries timeout in seconds | `30` |
|
||||||
|
| `-h, --help` | Show help information | |
|
||||||
|
| `-v, --version` | Show version information | |
|
||||||
|
|
||||||
|
## Core Commands
|
||||||
|
|
||||||
|
### serve
|
||||||
|
|
||||||
|
Starts the PocketBase web server. This is the most commonly used command for running your application.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage (default: 127.0.0.1:8090)
|
||||||
|
./pocketbase serve
|
||||||
|
|
||||||
|
# Specify custom host and port
|
||||||
|
./pocketbase serve --http=0.0.0.0:8090
|
||||||
|
|
||||||
|
# Serve with specific domain(s)
|
||||||
|
./pocketbase serve example.com www.example.com
|
||||||
|
|
||||||
|
# Enable HTTPS with automatic HTTP to HTTPS redirect
|
||||||
|
./pocketbase serve --https=0.0.0.0:443
|
||||||
|
|
||||||
|
# Development mode with verbose logging
|
||||||
|
./pocketbase serve --dev
|
||||||
|
|
||||||
|
# Production with custom directories
|
||||||
|
./pocketbase serve --dir=/data/pocketbase --publicDir=/var/www/html
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Serve-Specific Flags
|
||||||
|
|
||||||
|
| Flag | Description | Default |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `--http string` | TCP address for HTTP server | Domain mode: `0.0.0.0:80`<br>No domain: `127.0.0.1:8090` |
|
||||||
|
| `--https string` | TCP address for HTTPS server | Domain mode: `0.0.0.0:443`<br>No domain: empty (no TLS) |
|
||||||
|
| `--origins strings` | CORS allowed domain origins list | `[*]` |
|
||||||
|
|
||||||
|
#### Common Usage Patterns
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development server with all origins allowed
|
||||||
|
./pocketbase serve --dev --origins=http://localhost:3000,http://localhost:8080
|
||||||
|
|
||||||
|
# Production server with specific origins
|
||||||
|
./pocketbase serve --http=0.0.0.0:8090 --origins=https://app.example.com
|
||||||
|
|
||||||
|
# Behind reverse proxy (HTTPS handled by proxy)
|
||||||
|
./pocketbase serve --http=127.0.0.1:8090
|
||||||
|
```
|
||||||
|
|
||||||
|
### migrate
|
||||||
|
|
||||||
|
Manages database schema migrations. Essential for version-controlling your database structure and deploying schema changes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all available migrations
|
||||||
|
./pocketbase migrate up
|
||||||
|
|
||||||
|
# Revert the last applied migration
|
||||||
|
./pocketbase migrate down
|
||||||
|
|
||||||
|
# Revert the last 3 migrations
|
||||||
|
./pocketbase migrate down 3
|
||||||
|
|
||||||
|
# Create new blank migration template
|
||||||
|
./pocketbase migrate create add_user_profile_fields
|
||||||
|
|
||||||
|
# Create migration from current collections configuration
|
||||||
|
./pocketbase migrate collections
|
||||||
|
|
||||||
|
# Clean up migration history (remove references to deleted files)
|
||||||
|
./pocketbase migrate history-sync
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Migration Arguments
|
||||||
|
|
||||||
|
| Argument | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `up` | Runs all available migrations |
|
||||||
|
| `down [number]` | Reverts the last `[number]` applied migrations (default: 1) |
|
||||||
|
| `create name` | Creates new blank migration template file |
|
||||||
|
| `collections` | Creates migration file with snapshot of local collections configuration |
|
||||||
|
| `history-sync` | Ensures `_migrations` history table doesn't reference deleted migration files |
|
||||||
|
|
||||||
|
#### Migration Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Make schema changes via Admin UI or API
|
||||||
|
# 2. Create migration to capture changes
|
||||||
|
./pocketbase migrate collections
|
||||||
|
|
||||||
|
# 3. The new migration file appears in migrations directory
|
||||||
|
# 4. Commit migration file to version control
|
||||||
|
|
||||||
|
# Deploy to production:
|
||||||
|
./pocketbase migrate up
|
||||||
|
```
|
||||||
|
|
||||||
|
> Need to move historical data between environments? Pair schema migrations with the import/export options documented in [Data Migration Workflows](data_migration.md). Keep the schema in sync first, then run the data tools.
|
||||||
|
|
||||||
|
### superuser
|
||||||
|
|
||||||
|
Manages administrator (superuser) accounts for accessing the PocketBase admin dashboard.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create new superuser interactively
|
||||||
|
./pocketbase superuser create
|
||||||
|
|
||||||
|
# Create superuser with email and password
|
||||||
|
./pocketbase superuser create admin@example.com password123
|
||||||
|
|
||||||
|
# Update existing superuser password
|
||||||
|
./pocketbase superuser update admin@example.com newpassword123
|
||||||
|
|
||||||
|
# Delete superuser
|
||||||
|
./pocketbase superuser delete admin@example.com
|
||||||
|
|
||||||
|
# Create or update (idempotent)
|
||||||
|
./pocketbase superuser upsert admin@example.com password123
|
||||||
|
|
||||||
|
# Generate one-time password for existing superuser
|
||||||
|
./pocketbase superuser otp admin@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Superuser Sub-Commands
|
||||||
|
|
||||||
|
| Sub-command | Description | Example |
|
||||||
|
|-------------|-------------|---------|
|
||||||
|
| `create` | Creates a new superuser | `superuser create email@domain.com password` |
|
||||||
|
| `update` | Changes password of existing superuser | `superuser update email@domain.com newpassword` |
|
||||||
|
| `delete` | Deletes an existing superuser | `superuser delete email@domain.com` |
|
||||||
|
| `upsert` | Creates or updates if email exists | `superuser upsert email@domain.com password` |
|
||||||
|
| `otp` | Creates one-time password for superuser | `superuser otp email@domain.com` |
|
||||||
|
|
||||||
|
### update
|
||||||
|
|
||||||
|
Automatically updates PocketBase to the latest available version.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for and apply latest update
|
||||||
|
./pocketbase update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Setting Up a New Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create project directory
|
||||||
|
mkdir my-pocketbase-app
|
||||||
|
cd my-pocketbase-app
|
||||||
|
|
||||||
|
# 2. Download PocketBase
|
||||||
|
wget https://github.com/pocketbase/pocketbase/releases/latest/download/pocketbase_0.20.0_linux_amd64.zip
|
||||||
|
unzip pocketbase_0.20.0_linux_amd64.zip
|
||||||
|
chmod +x pocketbase
|
||||||
|
|
||||||
|
# 3. Create initial superuser
|
||||||
|
./pocketbase superuser create admin@example.com password123
|
||||||
|
|
||||||
|
# 4. Start development server
|
||||||
|
./pocketbase serve --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daily Development Cycle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start server in development mode
|
||||||
|
./pocketbase serve --dev
|
||||||
|
|
||||||
|
# In another terminal, make schema changes via Admin UI
|
||||||
|
# Then create migration to capture changes
|
||||||
|
./pocketbase migrate collections
|
||||||
|
|
||||||
|
# Test your application
|
||||||
|
# When ready, commit migration file to version control
|
||||||
|
```
|
||||||
|
|
||||||
|
### Team Collaboration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest changes from version control
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Run any new migrations
|
||||||
|
./pocketbase migrate up
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
./pocketbase serve --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Production Server Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Extract PocketBase to production directory
|
||||||
|
mkdir -p /opt/pocketbase
|
||||||
|
cp pocketbase /opt/pocketbase/
|
||||||
|
cd /opt/pocketbase
|
||||||
|
|
||||||
|
# 2. Set up proper permissions
|
||||||
|
chmod +x pocketbase
|
||||||
|
mkdir -p pb_data pb_public
|
||||||
|
|
||||||
|
# 3. Create superuser if not exists
|
||||||
|
./pocketbase superuser upsert admin@example.com securepassword123
|
||||||
|
|
||||||
|
# 4. Run production server
|
||||||
|
./pocketbase serve --http=0.0.0.0:8090
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using with Systemd
|
||||||
|
|
||||||
|
For service setup and production hardening guidance, see [Going to Production](going_to_production.md).
|
||||||
|
|
||||||
|
### Environment-Specific Configurations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
./pocketbase serve --dev --dir=./dev_data
|
||||||
|
|
||||||
|
# Staging
|
||||||
|
./pocketbase serve --http=0.0.0.0:8090 --dir=./staging_data
|
||||||
|
|
||||||
|
# Production
|
||||||
|
./pocketbase serve --http=0.0.0.0:8090 --dir=/data/pocketbase
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Custom Directories
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Custom data and public directories
|
||||||
|
./pocketbase serve --dir=/var/lib/pocketbase --publicDir=/var/www/pocketbase
|
||||||
|
|
||||||
|
# Custom migrations directory
|
||||||
|
./pocketbase migrate --migrationsDir=/opt/pocketbase/migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable encryption for app settings
|
||||||
|
export PB_ENCRYPTION_KEY="your-32-character-encryption-key"
|
||||||
|
./pocketbase serve --encryptionEnv=PB_ENCRYPTION_KEY
|
||||||
|
|
||||||
|
# Restrict CORS origins in production
|
||||||
|
./pocketbase serve --origins=https://app.example.com,https://admin.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript Hooks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable JavaScript hooks with custom directory
|
||||||
|
./pocketbase serve --hooksDir=./pb_hooks --hooksWatch
|
||||||
|
|
||||||
|
# Configure hook pool size for performance
|
||||||
|
./pocketbase serve --hooksPool=25
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Timeout Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set longer query timeout for complex operations
|
||||||
|
./pocketbase serve --queryTimeout=120
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Permission Denied
|
||||||
|
```bash
|
||||||
|
# Make executable
|
||||||
|
chmod +x pocketbase
|
||||||
|
|
||||||
|
# Check file ownership
|
||||||
|
ls -la pocketbase
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Port Already in Use
|
||||||
|
```bash
|
||||||
|
# Check what's using the port
|
||||||
|
sudo lsof -i :8090
|
||||||
|
|
||||||
|
# Use different port
|
||||||
|
./pocketbase serve --http=127.0.0.1:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Migration Conflicts
|
||||||
|
```bash
|
||||||
|
# Check migration status
|
||||||
|
./pocketbase migrate history-sync
|
||||||
|
|
||||||
|
# Re-run migrations if needed
|
||||||
|
./pocketbase migrate down
|
||||||
|
./pocketbase migrate up
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Data Directory Issues
|
||||||
|
```bash
|
||||||
|
# Ensure data directory exists and is writable
|
||||||
|
mkdir -p pb_data
|
||||||
|
chmod 755 pb_data
|
||||||
|
|
||||||
|
# Check directory permissions
|
||||||
|
ls -la pb_data/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable development mode for verbose logging
|
||||||
|
./pocketbase serve --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime logs print to stdout; when running under systemd, inspect them with `journalctl -u pocketbase -f`.
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Increase query timeout for slow queries
|
||||||
|
./pocketbase serve --queryTimeout=60
|
||||||
|
|
||||||
|
# Increase hooks pool for better concurrency
|
||||||
|
./pocketbase serve --hooksPool=50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Development
|
||||||
|
1. **Always use `--dev` flag** during development for detailed logging
|
||||||
|
2. **Create migrations** after making schema changes via Admin UI
|
||||||
|
3. **Commit migration files** to version control
|
||||||
|
4. **Use different data directories** for different environments
|
||||||
|
5. **Test migrations** on staging before production
|
||||||
|
|
||||||
|
### Production
|
||||||
|
1. **Never use `--dev` flag** in production
|
||||||
|
2. **Set up proper user permissions** for the PocketBase process
|
||||||
|
3. **Configure reverse proxy** (nginx/Caddy) for HTTPS
|
||||||
|
4. **Set up proper logging** and monitoring
|
||||||
|
5. **Regular backups** using the backup API
|
||||||
|
6. **Restrict CORS origins** to specific domains
|
||||||
|
7. **Use encryption** for sensitive app settings
|
||||||
|
|
||||||
|
### Security
|
||||||
|
1. **Use strong passwords** for superuser accounts
|
||||||
|
2. **Restrict origins** in production environments
|
||||||
|
3. **Enable encryption** for app settings
|
||||||
|
4. **Run as non-root user** whenever possible
|
||||||
|
5. **Keep PocketBase updated** using the update command
|
||||||
|
|
||||||
|
## CLI Scripting Examples
|
||||||
|
|
||||||
|
### Automated Setup Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# setup-pocketbase.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
ADMIN_EMAIL="admin@example.com"
|
||||||
|
ADMIN_PASSWORD="securepassword123"
|
||||||
|
DATA_DIR="./pb_data"
|
||||||
|
|
||||||
|
echo "🚀 Setting up PocketBase..."
|
||||||
|
|
||||||
|
# Create data directory
|
||||||
|
mkdir -p "$DATA_DIR"
|
||||||
|
|
||||||
|
# Create superuser
|
||||||
|
./pocketbase superuser upsert "$ADMIN_EMAIL" "$ADMIN_PASSWORD"
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
echo "✅ PocketBase setup complete!"
|
||||||
|
echo "🌐 Admin UI: http://127.0.0.1:8090/_/"
|
||||||
|
./pocketbase serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# migrate.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔄 Running PocketBase migrations..."
|
||||||
|
|
||||||
|
# Run all pending migrations
|
||||||
|
./pocketbase migrate up
|
||||||
|
|
||||||
|
echo "✅ Migrations complete!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Deployment Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# deploy-production.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Stop existing service
|
||||||
|
sudo systemctl stop pocketbase
|
||||||
|
|
||||||
|
# Backup current data
|
||||||
|
cp -r /opt/pocketbase/pb_data /opt/pocketbase/pb_data.backup.$(date +%Y%m%d)
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
/opt/pocketbase/pocketbase migrate up
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
sudo systemctl start pocketbase
|
||||||
|
|
||||||
|
echo "✅ PocketBase deployed successfully!"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Other Tools
|
||||||
|
|
||||||
|
### Docker Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build Docker image that includes custom migrations
|
||||||
|
FROM ghcr.io/pocketbase/pocketbase:latest
|
||||||
|
|
||||||
|
COPY ./migrations /pb/migrations
|
||||||
|
COPY ./pb_hooks /pb/pb_hooks
|
||||||
|
|
||||||
|
# Run migrations on startup
|
||||||
|
CMD ["sh", "-c", "./pocketbase migrate up && ./pocketbase serve --http=0.0.0.0:8090"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD Pipeline
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/deploy.yml
|
||||||
|
- name: Deploy PocketBase
|
||||||
|
run: |
|
||||||
|
./pocketbase migrate up
|
||||||
|
./pocketbase superuser upsert ${{ secrets.ADMIN_EMAIL }} ${{ secrets.ADMIN_PASSWORD }}
|
||||||
|
systemctl restart pocketbase
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Backups API](../api/api_backups.md) for backup automation techniques.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Essential Commands
|
||||||
|
```bash
|
||||||
|
./pocketbase serve --dev # Development server
|
||||||
|
./pocketbase migrate up # Run migrations
|
||||||
|
./pocketbase superuser create email pass # Create admin
|
||||||
|
./pocketbase update # Update PocketBase
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Flags
|
||||||
|
```bash
|
||||||
|
--dev # Development mode
|
||||||
|
--http=0.0.0.0:8090 # Custom host/port
|
||||||
|
--dir=custom_data # Custom data directory
|
||||||
|
--origins=https://domain.com # CORS restrictions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Checklist
|
||||||
|
- [ ] Remove `--dev` flag
|
||||||
|
- [ ] Set proper file permissions
|
||||||
|
- [ ] Configure reverse proxy for HTTPS
|
||||||
|
- [ ] Restrict CORS origins
|
||||||
|
- [ ] Set up monitoring and backups
|
||||||
|
- [ ] Create systemd service
|
||||||
|
- [ ] Test migration workflow
|
||||||
544
skills/pocketbase/references/core/collections.md
Normal file
544
skills/pocketbase/references/core/collections.md
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
# Collections in PocketBase
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Collections are the fundamental data structures in PocketBase, similar to tables in a relational database. They define the schema and behavior of your data.
|
||||||
|
|
||||||
|
## Collection Types
|
||||||
|
|
||||||
|
### 1. Base Collection
|
||||||
|
Flexible collection with custom schema. Used for:
|
||||||
|
- Posts, articles, products
|
||||||
|
- Comments, messages
|
||||||
|
- Any application-specific data
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- No built-in authentication
|
||||||
|
- Custom fields only
|
||||||
|
- Full CRUD operations
|
||||||
|
- Can be accessed via REST API
|
||||||
|
|
||||||
|
### 2. Auth Collection
|
||||||
|
Special collection for user accounts. Used for:
|
||||||
|
- User registration and login
|
||||||
|
- User profiles and settings
|
||||||
|
- Authentication workflows
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Built-in auth fields (`email`, `password`, `emailVisibility`, `verified`)
|
||||||
|
- Automatic user ID tracking on creation
|
||||||
|
- OAuth2 support
|
||||||
|
- Password management
|
||||||
|
- Email verification
|
||||||
|
- Password reset functionality
|
||||||
|
|
||||||
|
### 3. View Collection
|
||||||
|
Read-only collection based on SQL views. Used for:
|
||||||
|
- Complex joins and aggregations
|
||||||
|
- Denormalized data for performance
|
||||||
|
- Reporting and analytics
|
||||||
|
- Dashboard metrics
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Read-only (no create, update, delete)
|
||||||
|
- Defined via SQL query
|
||||||
|
- Auto-updates when source data changes
|
||||||
|
- Useful for performance optimization
|
||||||
|
|
||||||
|
## Creating Collections
|
||||||
|
|
||||||
|
### Via Admin UI
|
||||||
|
1. Navigate to Collections
|
||||||
|
2. Click "New Collection"
|
||||||
|
3. Choose collection type
|
||||||
|
4. Configure name and schema
|
||||||
|
5. Save
|
||||||
|
|
||||||
|
### Via API
|
||||||
|
```javascript
|
||||||
|
const collection = await pb.collections.create({
|
||||||
|
name: 'products',
|
||||||
|
type: 'base',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'price',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Field Types
|
||||||
|
|
||||||
|
### Text
|
||||||
|
Short to medium text strings.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `min` - Minimum character length
|
||||||
|
- `max` - Maximum character length
|
||||||
|
- `pattern` - Regex pattern for validation
|
||||||
|
|
||||||
|
### Number
|
||||||
|
Integer or decimal numbers.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "price",
|
||||||
|
"type": "number",
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"noDecimal": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `min` - Minimum value
|
||||||
|
- `max` - Maximum value
|
||||||
|
- `noDecimal` - Allow only integers
|
||||||
|
|
||||||
|
### Email
|
||||||
|
Email addresses with validation.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "contact_email",
|
||||||
|
"type": "email"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL
|
||||||
|
URLs with validation.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "website",
|
||||||
|
"type": "url"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Date
|
||||||
|
Date and time values.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "published_date",
|
||||||
|
"type": "date",
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Boolean
|
||||||
|
True/false values.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "is_published",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON
|
||||||
|
Arbitrary JSON data.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "metadata",
|
||||||
|
"type": "json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relation
|
||||||
|
Links to records in other collections.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "author",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "AUTH_COLLECTION_ID",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `collectionId` - Target collection ID
|
||||||
|
- `cascadeDelete` - Delete related records when this is deleted
|
||||||
|
- `maxSelect` - Maximum number of related records (1 or null for unlimited)
|
||||||
|
- `displayFields` - Fields to display when showing the relation
|
||||||
|
|
||||||
|
### File
|
||||||
|
File uploads and storage.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "file",
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"mimeTypes": ["image/*"],
|
||||||
|
"thumbs": ["100x100", "300x300"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `maxSelect` - Maximum number of files
|
||||||
|
- `maxSize` - Maximum file size in bytes
|
||||||
|
- `mimeTypes` - Allowed MIME types (array or ["*"] for all)
|
||||||
|
- `thumbs` - Auto-generate image thumbnails at specified sizes
|
||||||
|
|
||||||
|
### Select
|
||||||
|
Dropdown with predefined options.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "select",
|
||||||
|
"options": {
|
||||||
|
"values": ["draft", "published", "archived"],
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `values` - Array of allowed values
|
||||||
|
- `maxSelect` - Maximum selections (1 for single select, null for multi-select)
|
||||||
|
|
||||||
|
### Autodate
|
||||||
|
Automatically populated dates.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "created",
|
||||||
|
"type": "autodate",
|
||||||
|
"options": {
|
||||||
|
"onCreate": true,
|
||||||
|
"onUpdate": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `onCreate` - Set on record creation
|
||||||
|
- `onUpdate` - Update on record modification
|
||||||
|
|
||||||
|
### Username
|
||||||
|
Unique usernames (valid only for auth collections).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"type": "username",
|
||||||
|
"options": {
|
||||||
|
"min": 3,
|
||||||
|
"max": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Collection Rules
|
||||||
|
|
||||||
|
Rules control who can access, create, update, and delete records.
|
||||||
|
|
||||||
|
### Types of Rules
|
||||||
|
|
||||||
|
1. **List Rule** - Who can list/view multiple records
|
||||||
|
2. **View Rule** - Who can view individual records
|
||||||
|
3. **Create Rule** - Who can create new records
|
||||||
|
4. **Update Rule** - Who can modify records
|
||||||
|
5. **Delete Rule** - Who can delete records
|
||||||
|
|
||||||
|
### Rule Syntax
|
||||||
|
|
||||||
|
**Authenticated Users Only**
|
||||||
|
```
|
||||||
|
@request.auth.id != ""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Owner-Based Access**
|
||||||
|
```
|
||||||
|
user_id = @request.auth.id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Role-Based Access**
|
||||||
|
```
|
||||||
|
@request.auth.role = 'admin'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conditional Access**
|
||||||
|
```
|
||||||
|
status = 'published' || @request.auth.id = author_id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Complex Conditions**
|
||||||
|
```
|
||||||
|
@request.auth.role = 'moderator' && @request.auth.verified = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special Variables
|
||||||
|
|
||||||
|
- `@request.auth` - Current authenticated user
|
||||||
|
- `@request.auth.id` - User ID
|
||||||
|
- `@request.auth.email` - User email
|
||||||
|
- `@request.auth.role` - User role
|
||||||
|
- `@request.auth.verified` - Email verification status
|
||||||
|
|
||||||
|
### Rule Examples
|
||||||
|
|
||||||
|
**Public Blog Posts**
|
||||||
|
```
|
||||||
|
List Rule: status = 'published'
|
||||||
|
View Rule: status = 'published'
|
||||||
|
Create Rule: @request.auth.id != ''
|
||||||
|
Update Rule: author_id = @request.auth.id
|
||||||
|
Delete Rule: author_id = @request.auth.id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Private User Data**
|
||||||
|
```
|
||||||
|
List Rule: user_id = @request.auth.id
|
||||||
|
View Rule: user_id = @request.auth.id
|
||||||
|
Create Rule: @request.auth.id != ''
|
||||||
|
Update Rule: user_id = @request.auth.id
|
||||||
|
Delete Rule: user_id = @request.auth.id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Admin-Only Content**
|
||||||
|
```
|
||||||
|
List Rule: @request.auth.role = 'admin'
|
||||||
|
View Rule: @request.auth.role = 'admin'
|
||||||
|
Create Rule: @request.auth.role = 'admin'
|
||||||
|
Update Rule: @request.auth.role = 'admin'
|
||||||
|
Delete Rule: @request.auth.role = 'admin'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Moderated Comments**
|
||||||
|
```
|
||||||
|
List Rule: status = 'approved' || author_id = @request.auth.id
|
||||||
|
View Rule: status = 'approved' || author_id = @request.auth.id
|
||||||
|
Create Rule: @request.auth.id != ''
|
||||||
|
Update Rule: author_id = @request.auth.id
|
||||||
|
Delete Rule: author_id = @request.auth.id || @request.auth.role = 'moderator'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Collection Indexes
|
||||||
|
|
||||||
|
Indexes improve query performance on frequently searched or sorted fields.
|
||||||
|
|
||||||
|
### Creating Indexes
|
||||||
|
|
||||||
|
**Via Admin UI**
|
||||||
|
1. Go to collection settings
|
||||||
|
2. Click "Indexes" tab
|
||||||
|
3. Click "New Index"
|
||||||
|
4. Select fields to index
|
||||||
|
5. Save
|
||||||
|
|
||||||
|
**Via API**
|
||||||
|
```javascript
|
||||||
|
await pb.collections.update('COLLECTION_ID', {
|
||||||
|
indexes: [
|
||||||
|
'CREATE INDEX idx_posts_status ON posts(status)',
|
||||||
|
'CREATE INDEX idx_posts_author ON posts(author_id)',
|
||||||
|
'CREATE INDEX idx_posts_created ON posts(created)'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Index Best Practices
|
||||||
|
|
||||||
|
1. **Index fields used in filters**
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_posts_status ON posts(status)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Index fields used in sorts**
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_posts_created ON posts(created)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Index foreign keys (relations)**
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_comments_post ON comments(post_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Composite indexes for multi-field queries**
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_posts_status_created ON posts(status, created)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Don't over-index** - Each index adds overhead to writes
|
||||||
|
|
||||||
|
## Collection Options
|
||||||
|
|
||||||
|
### General Options
|
||||||
|
|
||||||
|
- **Name** - Collection identifier (used in API endpoints)
|
||||||
|
- **Type** - base, auth, or view
|
||||||
|
- **System collection** - Built-in collections (users, _pb_users_auth_)
|
||||||
|
- **List encryption** - Encrypt data in list views
|
||||||
|
|
||||||
|
### API Options
|
||||||
|
|
||||||
|
- **API keys** - Manage read/write API keys
|
||||||
|
- **CRUD endpoints** - Enable/disable specific endpoints
|
||||||
|
- **File access** - Configure public/private file access
|
||||||
|
|
||||||
|
### Auth Collection Options
|
||||||
|
|
||||||
|
- **Min password length** - Minimum password requirements
|
||||||
|
- **Password constraints** - Require uppercase, numbers, symbols
|
||||||
|
- **Email verification** - Require email confirmation
|
||||||
|
- **OAuth2 providers** - Configure social login
|
||||||
|
|
||||||
|
## Managing Collections
|
||||||
|
|
||||||
|
### List Collections
|
||||||
|
```javascript
|
||||||
|
const collections = await pb.collections.getList(1, 50);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Collection
|
||||||
|
```javascript
|
||||||
|
const collection = await pb.collections.getOne('COLLECTION_ID');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Collection
|
||||||
|
```javascript
|
||||||
|
const updated = await pb.collections.update('COLLECTION_ID', {
|
||||||
|
name: 'new_name',
|
||||||
|
schema: [
|
||||||
|
// updated schema
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Collection
|
||||||
|
```javascript
|
||||||
|
await pb.collections.delete('COLLECTION_ID');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Collection Schema
|
||||||
|
```javascript
|
||||||
|
const collection = await pb.collections.getOne('COLLECTION_ID');
|
||||||
|
const schemaJSON = JSON.stringify(collection.schema, null, 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Plan Schema Carefully**
|
||||||
|
- Design before implementing
|
||||||
|
- Consider future needs
|
||||||
|
- Use appropriate field types
|
||||||
|
|
||||||
|
2. **Use Relations Wisely**
|
||||||
|
- Normalize data appropriately
|
||||||
|
- Set cascadeDelete when appropriate
|
||||||
|
- Consider performance impact
|
||||||
|
|
||||||
|
3. **Set Rules Early**
|
||||||
|
- Security from the start
|
||||||
|
- Test rules thoroughly
|
||||||
|
- Document rule logic
|
||||||
|
|
||||||
|
4. **Index Strategically**
|
||||||
|
- Profile slow queries
|
||||||
|
- Index commonly filtered fields
|
||||||
|
- Avoid over-indexing
|
||||||
|
|
||||||
|
5. **Use Auth Collections for Users**
|
||||||
|
- Built-in auth features
|
||||||
|
- OAuth2 support
|
||||||
|
- Password management
|
||||||
|
|
||||||
|
6. **Use Views for Complex Queries**
|
||||||
|
- Improve performance
|
||||||
|
- Simplify frontend code
|
||||||
|
- Pre-compute expensive joins
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Blog/Post System
|
||||||
|
```
|
||||||
|
Collections:
|
||||||
|
- posts (base) - title, content, author, status, published_date
|
||||||
|
- categories (base) - name, slug, description
|
||||||
|
- tags (base) - name, slug
|
||||||
|
- posts_tags (base) - post_id, tag_id (relation join)
|
||||||
|
```
|
||||||
|
|
||||||
|
### E-commerce
|
||||||
|
```
|
||||||
|
Collections:
|
||||||
|
- products (base) - name, price, description, category, stock
|
||||||
|
- orders (base) - user, items, total, status
|
||||||
|
- order_items (base) - order, product, quantity, price
|
||||||
|
- categories (base) - name, parent (self-relation)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Social Network
|
||||||
|
```
|
||||||
|
Collections:
|
||||||
|
- posts (base) - author, content, media, created, visibility
|
||||||
|
- likes (base) - post, user (unique constraint)
|
||||||
|
- follows (base) - follower, following (unique constraint)
|
||||||
|
- users (auth) - built-in auth + profile fields
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Collection not showing data**
|
||||||
|
- Check listRule
|
||||||
|
- Verify user permissions
|
||||||
|
- Check if view collection is properly configured
|
||||||
|
|
||||||
|
**Slow queries**
|
||||||
|
- Add database indexes
|
||||||
|
- Optimize rule conditions
|
||||||
|
- Use views for complex joins
|
||||||
|
|
||||||
|
**Can't create records**
|
||||||
|
- Check createRule
|
||||||
|
- Verify required fields
|
||||||
|
- Ensure user is authenticated
|
||||||
|
|
||||||
|
**File uploads failing**
|
||||||
|
- Check maxSize and mimeTypes
|
||||||
|
- Verify file field options
|
||||||
|
- Check user has create permissions
|
||||||
|
|
||||||
|
## Related Topics
|
||||||
|
|
||||||
|
- [Authentication](authentication.md) - User management
|
||||||
|
- [API Rules & Filters](api_rules_filters.md) - Security rules syntax
|
||||||
|
- [Working with Relations](working_with_relations.md) - Field relationships
|
||||||
|
- [Files Handling](files_handling.md) - File uploads and storage
|
||||||
|
- [Schema Templates](../templates/schema_templates.md) - Pre-built schemas
|
||||||
184
skills/pocketbase/references/core/data_migration.md
Normal file
184
skills/pocketbase/references/core/data_migration.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# Data Migration Workflows
|
||||||
|
|
||||||
|
PocketBase does not ship with a one-click import/export pipeline, but the core project maintainers outline several supported patterns in [GitHub discussion #6287](https://github.com/pocketbase/pocketbase/discussions/6287). This guide explains how to choose the right workflow, hardens the existing helper scripts, and points to extension patterns you can adapt for larger migrations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision Guide
|
||||||
|
|
||||||
|
| Scenario | Recommended Path | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Small/medium data sets (< 100k records) and you just need JSON dumps | [Web API scripts](#option-1-web-api-scripts) | Works everywhere; slower but simplest to automate |
|
||||||
|
| You want transactions, schema automation, or better performance | [Custom CLI commands](#option-2-custom-cli-commands) | Implement in JS `pb_hooks` or native Go extensions |
|
||||||
|
| You must transform data from another live database | [Mini Go program bridging databases](#option-3-mini-go-bridge) | Connect to PocketBase `pb_data` alongside the legacy DB |
|
||||||
|
| You already have CSV or SQLite dumps | [External tooling](#option-4-external-import-tools) | sqlite3 `.import`, community tools like `pocketbase-import` |
|
||||||
|
| You need full control and understand PB internals | [Raw SQLite scripts](#option-5-raw-sqlite-scripts) | Only if you know how PB stores complex field types |
|
||||||
|
|
||||||
|
> **Tip:** If you are migrating an application that already works and you do not plan on extending it, consider whether the migration effort is worth it—the PocketBase author recommends staying on the stable stack unless you need PB-specific capabilities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-flight Checklist
|
||||||
|
|
||||||
|
1. **Back up `pb_data/` first.** Use `sqlite3` or the Backups API before experimenting.
|
||||||
|
2. **Create collections and fields up-front.** Use the Admin UI, migrations (`./pocketbase migrate collections`), or extension code so relations, file fields, and validation rules exist before import.
|
||||||
|
3. **Map unique keys per collection.** Decide which field(s) you will use for upserts (e.g., `email` on `users`).
|
||||||
|
4. **Audit data types.** PocketBase stores multi-selects and relation sets as JSON arrays, and file fields expect PocketBase-managed file IDs.
|
||||||
|
5. **Plan authentication.** Admin endpoints require a superuser token; scripts now prompt for credentials.
|
||||||
|
6. **Run a dry run.** Use the script `--dry-run` flag or custom command to validate payloads before writing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 1: Web API Scripts
|
||||||
|
|
||||||
|
Use the hardened Python helpers in `scripts/` when you need a portable solution without custom builds.
|
||||||
|
|
||||||
|
### Export
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/export_data.py \
|
||||||
|
http://127.0.0.1:8090 \
|
||||||
|
pb_export \
|
||||||
|
--email admin@example.com \
|
||||||
|
--batch-size 500 \
|
||||||
|
--format ndjson \
|
||||||
|
--exclude _pb_users,_migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
- Authenticates as an admin (password prompt if omitted).
|
||||||
|
- Enumerates collections dynamically; filter with `--collections` or `--exclude`.
|
||||||
|
- Streams records page-by-page and writes per-collection `.json` or `.ndjson` files plus a `manifest.json` summary.
|
||||||
|
- Use NDJSON for large exports where you want to stream line-by-line elsewhere.
|
||||||
|
|
||||||
|
### Import
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/import_data.py \
|
||||||
|
http://127.0.0.1:8090 \
|
||||||
|
pb_export \
|
||||||
|
--email admin@example.com \
|
||||||
|
--upsert users=email --upsert orders=orderNumber \
|
||||||
|
--concurrency 4 \
|
||||||
|
--batch-size 200 \
|
||||||
|
--dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
- Supports `.json` and `.ndjson` dumps.
|
||||||
|
- Cleans system fields (`id`, `created`, `updated`, `@expand`).
|
||||||
|
- Optional per-collection upserts via `--upsert collection=field` (use `*=field` as a fallback).
|
||||||
|
- Batches and runs limited concurrency to reduce HTTP latency, with optional throttling between batches.
|
||||||
|
- `--dry-run` validates payloads without writing to the database. When satisfied, re-run without the flag.
|
||||||
|
- Fails fast if a collection is missing unless `--skip-missing` is set.
|
||||||
|
|
||||||
|
This approach is intentionally simple and aligns with the "v1" recommendation from the PocketBase maintainer. Expect higher runtimes for large datasets but minimal setup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 2: Custom CLI Commands
|
||||||
|
|
||||||
|
Register commands inside `pb_hooks/` or a Go extension to bypass the REST layer and operate inside a database transaction.
|
||||||
|
|
||||||
|
### JS `pb_hooks` example
|
||||||
|
|
||||||
|
```js
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
const { Command } = require("commander");
|
||||||
|
|
||||||
|
$app.rootCmd.addCommand(new Command({
|
||||||
|
use: "data:import <file> <collection>",
|
||||||
|
run: (cmd, args) => {
|
||||||
|
const rows = require(args[0]);
|
||||||
|
const collection = $app.findCollectionByNameOrId(args[1]);
|
||||||
|
$app.runInTransaction((tx) => {
|
||||||
|
for (const row of rows) {
|
||||||
|
const record = new Record(collection);
|
||||||
|
record.load(row);
|
||||||
|
tx.save(record);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
$app.rootCmd.addCommand(new Command({
|
||||||
|
use: "data:export <collection> <file>",
|
||||||
|
run: (cmd, args) => {
|
||||||
|
const records = $app.findAllRecords(args[0], cmd.getOptionValue("batch") || 1000);
|
||||||
|
$os.writeFile(args[1], JSON.stringify(records, null, 2), 0o644);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
- Invoke with `./pocketbase data:import ./users.json users`.
|
||||||
|
- Wrap heavy operations in `runInTransaction` and consider `saveNoValidate` only after cleaning data.
|
||||||
|
- Extend with chunks, progress logs, or schema checks per your needs.
|
||||||
|
|
||||||
|
See also: [`references/go/go_console_commands.md`](../go/go_console_commands.md) for Go equivalents and CLI wiring tips.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 3: Mini Go Bridge
|
||||||
|
|
||||||
|
For zero-downtime migrations or complex transformations, create a Go program that embeds PocketBase and connects to your legacy database driver (`database/sql`, `pgx`, etc.).
|
||||||
|
|
||||||
|
High-level steps:
|
||||||
|
|
||||||
|
1. Import `github.com/pocketbase/pocketbase` as a module and boot the app in headless mode.
|
||||||
|
2. Connect to the legacy database, stream rows, and normalize data types.
|
||||||
|
3. Use `app.RunInTransaction` plus `app.FindCollectionByNameOrId` to create records directly.
|
||||||
|
4. Batch writes to avoid exhausting memory; reuse prepared statements for speed.
|
||||||
|
|
||||||
|
Refer to [`references/go/go_database.md`](../go/go_database.md) and [`references/go/go_migrations.md`](../go/go_migrations.md) for transaction helpers and schema management patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 4: External Import Tools
|
||||||
|
|
||||||
|
- **sqlite3 CLI** (`.import`, `.dump`, `.excel`): usable when the source data already matches the PocketBase schema. Ensure collections/fields exist first.
|
||||||
|
- **Community tool [`michal-kapala/pocketbase-import`](https://github.com/michal-kapala/pocketbase-import)**: handles CSV and flat JSON, creates text fields dynamically, and wraps operations in a transaction.
|
||||||
|
- **Custom CSV pipelines**: parse CSV with your preferred language, then leverage the REST scripts or CLI commands above.
|
||||||
|
|
||||||
|
Always inspect the generated SQLite tables after import to confirm multi-value fields and relation columns are stored as expected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 5: Raw SQLite Scripts
|
||||||
|
|
||||||
|
This path edits `pb_data/data.db` directly. Only attempt it if you fully understand PocketBase’s internal schema conventions:
|
||||||
|
|
||||||
|
1. Snapshot the database before touching it.
|
||||||
|
2. Insert `_collections` metadata before writing to collection tables so the Admin UI and APIs recognize the data.
|
||||||
|
3. Convert non-SQLite dumps (PostgreSQL/MySQL) to SQLite-compatible syntax.
|
||||||
|
4. Manually serialize multiselects, relation lists, and JSON fields.
|
||||||
|
|
||||||
|
Treat this as a last resort when other methods are impractical.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation & Rollback
|
||||||
|
|
||||||
|
1. Compare counts between source and target collections (`records/count` endpoint or SQL).
|
||||||
|
2. Spot-check a few complex records (relations, files, arrays).
|
||||||
|
3. Run application-level smoke tests or automation scripts.
|
||||||
|
4. If issues appear, restore the pre-flight backup and iterate.
|
||||||
|
5. Document the exact command set you used for future recoveries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related References
|
||||||
|
|
||||||
|
- [`scripts/export_data.py`](../../scripts/export_data.py) – authenticated export script with filters, pagination, and NDJSON support.
|
||||||
|
- [`scripts/import_data.py`](../../scripts/import_data.py) – authenticated import script with upsert, batching, and dry-run.
|
||||||
|
- [`references/go/go_console_commands.md`](../go/go_console_commands.md) – extend PocketBase with custom CLI commands.
|
||||||
|
- [`references/go/go_routing.md`](../go/go_routing.md) – expose admin-only import/export endpoints if you prefer HTTP jobs.
|
||||||
|
- [`references/api/api_records.md`](../api/api_records.md) – record filtering syntax used by the scripts.
|
||||||
|
- [`references/api/api_backups.md`](../api/api_backups.md) – full database backup/restore (different from selective migrations).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Checklist
|
||||||
|
|
||||||
|
- [ ] Pick a workflow that matches the data volume and complexity.
|
||||||
|
- [ ] Prepare schema and unique constraints before importing.
|
||||||
|
- [ ] Run exports with authentication and pagination.
|
||||||
|
- [ ] Test imports with `--dry-run`, then run again without it.
|
||||||
|
- [ ] Validate data counts and integrity, keep a rollback plan handy.
|
||||||
768
skills/pocketbase/references/core/files_handling.md
Normal file
768
skills/pocketbase/references/core/files_handling.md
Normal file
@@ -0,0 +1,768 @@
|
|||||||
|
# File Handling in PocketBase
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
PocketBase provides comprehensive file handling capabilities:
|
||||||
|
- Single and multi-file uploads
|
||||||
|
- Automatic image thumbnail generation
|
||||||
|
- File type restrictions
|
||||||
|
- Size limits
|
||||||
|
- Public and private file access
|
||||||
|
- CDN integration support
|
||||||
|
- Image resizing and optimization
|
||||||
|
|
||||||
|
## File Fields
|
||||||
|
|
||||||
|
Add file fields to collections via the Admin UI or API:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "file",
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 10485760,
|
||||||
|
"mimeTypes": ["image/*"],
|
||||||
|
"thumbs": ["100x100", "300x300"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Field Options
|
||||||
|
|
||||||
|
#### maxSelect
|
||||||
|
Maximum number of files allowed:
|
||||||
|
- `1` - Single file upload
|
||||||
|
- `null` or `2+` - Multiple files
|
||||||
|
|
||||||
|
```json
|
||||||
|
"maxSelect": 5 // Allow up to 5 files
|
||||||
|
```
|
||||||
|
|
||||||
|
#### maxSize
|
||||||
|
Maximum file size in bytes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"maxSize": 10485760 // 10MB
|
||||||
|
|
||||||
|
// Common sizes:
|
||||||
|
5MB = 5242880
|
||||||
|
10MB = 10485760
|
||||||
|
50MB = 52428800
|
||||||
|
100MB = 104857600
|
||||||
|
```
|
||||||
|
|
||||||
|
#### mimeTypes
|
||||||
|
Allowed MIME types (array):
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Images only
|
||||||
|
"mimeTypes": ["image/jpeg", "image/png", "image/gif"]
|
||||||
|
|
||||||
|
// Images and videos
|
||||||
|
"mimeTypes": ["image/*", "video/*"]
|
||||||
|
|
||||||
|
// Any file type
|
||||||
|
"mimeTypes": ["*"]
|
||||||
|
|
||||||
|
// Specific types
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"application/pdf",
|
||||||
|
"text/csv"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### thumbs
|
||||||
|
Auto-generate image thumbnails:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"thumbs": [
|
||||||
|
"100x100", // Small square
|
||||||
|
"300x300", // Medium square
|
||||||
|
"800x600", // Large thumbnail
|
||||||
|
"1200x800" // Extra large
|
||||||
|
]
|
||||||
|
|
||||||
|
// Formats:
|
||||||
|
// WIDTHxHEIGHT - exact size, may crop
|
||||||
|
// WIDTHx - width only, maintain aspect ratio
|
||||||
|
// xHEIGHT - height only, maintain aspect ratio
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uploading Files
|
||||||
|
|
||||||
|
### Single File Upload
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', fileInput.files[0]);
|
||||||
|
|
||||||
|
const user = await pb.collection('users').update('USER_ID', formData);
|
||||||
|
|
||||||
|
// Access file URL
|
||||||
|
const avatarUrl = pb.files.getURL(user, user.avatar);
|
||||||
|
console.log(avatarUrl);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple File Upload
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Add multiple files
|
||||||
|
formData.append('images', fileInput.files[0]);
|
||||||
|
formData.append('images', fileInput.files[1]);
|
||||||
|
formData.append('images', fileInput.files[2]);
|
||||||
|
|
||||||
|
const post = await pb.collection('posts').update('POST_ID', formData);
|
||||||
|
|
||||||
|
// Access all files
|
||||||
|
post.images.forEach(image => {
|
||||||
|
const url = pb.files.getURL(post, image);
|
||||||
|
console.log(url);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload with Metadata
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('document', fileInput.files[0], {
|
||||||
|
filename: 'custom-name.pdf', // Custom filename
|
||||||
|
type: 'application/pdf',
|
||||||
|
lastModified: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
const record = await pb.collection('documents').update('DOC_ID', formData);
|
||||||
|
```
|
||||||
|
|
||||||
|
## File URLs
|
||||||
|
|
||||||
|
### Get File URL
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Basic URL
|
||||||
|
const url = pb.files.getURL(record, record.avatar);
|
||||||
|
|
||||||
|
// With thumbnail
|
||||||
|
const thumbnailUrl = pb.files.getURL(
|
||||||
|
record,
|
||||||
|
record.avatar,
|
||||||
|
{ thumb: '300x300' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom options
|
||||||
|
const url = pb.files.getURL(
|
||||||
|
record,
|
||||||
|
record.avatar,
|
||||||
|
{
|
||||||
|
thumb: '100x100',
|
||||||
|
expires: 3600 // URL expires in 1 hour (for private files)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL Parameters
|
||||||
|
|
||||||
|
**For public files:**
|
||||||
|
```javascript
|
||||||
|
// Direct access (public files only)
|
||||||
|
const url = pb.files.getURL(record, record.avatar);
|
||||||
|
// Returns: http://localhost:8090/api/files/COLLECTION_ID/RECORD_ID/filename.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
**For private files:**
|
||||||
|
```javascript
|
||||||
|
// Temporary signed URL (1 hour expiry)
|
||||||
|
const url = pb.files.getURL(record, record.avatar, { expires: 3600 });
|
||||||
|
// Returns: http://localhost:8090/api/files/COLLECTION_ID/RECORD_ID/filename.jpg?token=SIGNED_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
**Thumbnail URLs:**
|
||||||
|
```javascript
|
||||||
|
// Automatic thumbnail
|
||||||
|
const thumbUrl = pb.files.getURL(record, record.avatar, {
|
||||||
|
thumb: '300x300'
|
||||||
|
});
|
||||||
|
// Returns: thumbnail if available
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Access Control
|
||||||
|
|
||||||
|
### Public Files
|
||||||
|
Default behavior - anyone with URL can access:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// File is publicly accessible
|
||||||
|
const url = pb.files.getURL(record, record.avatar);
|
||||||
|
// Can be shared and accessed by anyone
|
||||||
|
```
|
||||||
|
|
||||||
|
### Private Files
|
||||||
|
Restrict access to authenticated users:
|
||||||
|
|
||||||
|
**1. Configure in Admin UI**
|
||||||
|
- Go to Collection → File field options
|
||||||
|
- Enable "Private files"
|
||||||
|
- Set file rules (e.g., `user_id = @request.auth.id`)
|
||||||
|
|
||||||
|
**2. Use signed URLs**
|
||||||
|
```javascript
|
||||||
|
// Generate signed URL (expires)
|
||||||
|
const signedUrl = pb.files.getURL(record, record.avatar, {
|
||||||
|
expires: 3600 // Expires in 1 hour
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use signed URL in frontend
|
||||||
|
<img src={signedUrl} alt="Avatar" />
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Access files with auth token**
|
||||||
|
```javascript
|
||||||
|
// Include auth token in requests
|
||||||
|
const response = await fetch(signedUrl, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${pb.authStore.token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Rules
|
||||||
|
|
||||||
|
Control who can upload/view/delete files:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Owner can only access their files
|
||||||
|
File Rule: user_id = @request.auth.id
|
||||||
|
|
||||||
|
// Public read, authenticated write
|
||||||
|
List Rule: true
|
||||||
|
View Rule: true
|
||||||
|
Create Rule: @request.auth.id != ""
|
||||||
|
Update Rule: user_id = @request.auth.id
|
||||||
|
Delete Rule: user_id = @request.auth.id
|
||||||
|
|
||||||
|
// Admins only
|
||||||
|
Create Rule: @request.auth.role = "admin"
|
||||||
|
Update Rule: @request.auth.role = "admin"
|
||||||
|
Delete Rule: @request.auth.role = "admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Download Files
|
||||||
|
|
||||||
|
### Browser Download
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Download via browser
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = pb.files.getURL(record, record.document);
|
||||||
|
link.download = record.document;
|
||||||
|
link.click();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Download
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Fetch file as blob
|
||||||
|
const blob = await pb.files.download(record, record.document);
|
||||||
|
|
||||||
|
// Or with fetch
|
||||||
|
const response = await fetch(pb.files.getURL(record, record.document));
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
// Save file
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = record.document;
|
||||||
|
a.click();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deleting Files
|
||||||
|
|
||||||
|
### Delete Single File
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Remove file from record
|
||||||
|
const updated = await pb.collection('users').update('USER_ID', {
|
||||||
|
avatar: null // Remove avatar
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Multiple Files
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Remove specific files from array
|
||||||
|
const updated = await pb.collection('posts').update('POST_ID', {
|
||||||
|
images: record.images.filter(img => img !== imageToRemove)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete File on Record Delete
|
||||||
|
|
||||||
|
Files are automatically deleted when record is deleted:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await pb.collection('posts').delete('POST_ID');
|
||||||
|
// All associated files are removed automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image Thumbnails
|
||||||
|
|
||||||
|
### Automatic Thumbnails
|
||||||
|
|
||||||
|
Define in file field options:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "file",
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 10,
|
||||||
|
"maxSize": 10485760,
|
||||||
|
"mimeTypes": ["image/*"],
|
||||||
|
"thumbs": ["100x100", "300x300", "800x600"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access Thumbnails
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get specific thumbnail size
|
||||||
|
const smallThumb = pb.files.getURL(post, post.images[0], {
|
||||||
|
thumb: '100x100'
|
||||||
|
});
|
||||||
|
|
||||||
|
const mediumThumb = pb.files.getURL(post, post.images[0], {
|
||||||
|
thumb: '300x300'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-select best thumbnail
|
||||||
|
const thumb = pb.files.getURL(post, post.images[0], {
|
||||||
|
thumb: '300x300' // Returns thumbnail or original if not available
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Thumbnail Formats
|
||||||
|
|
||||||
|
- `WxH` - Crop to exact dimensions
|
||||||
|
- `Wx` - Width only, maintain aspect ratio
|
||||||
|
- `xH` - Height only, maintain aspect ratio
|
||||||
|
- `Wx0` - Width, no height limit
|
||||||
|
- `0xH` - Height, no width limit
|
||||||
|
|
||||||
|
## Frontend Integration
|
||||||
|
|
||||||
|
### React Image Component
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
function ImageUpload() {
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [uploadedUrl, setUploadedUrl] = useState('');
|
||||||
|
|
||||||
|
const handleUpload = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', file);
|
||||||
|
|
||||||
|
const updated = await pb.collection('users').update('USER_ID', formData);
|
||||||
|
setUploadedUrl(pb.files.getURL(updated, updated.avatar));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input type="file" onChange={handleUpload} />
|
||||||
|
{uploadedUrl && <img src={uploadedUrl} alt="Avatar" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vue.js File Upload
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<input type="file" @change="handleUpload" />
|
||||||
|
<img v-if="uploadedUrl" :src="uploadedUrl" alt="Avatar" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
uploadedUrl: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleUpload(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', file);
|
||||||
|
|
||||||
|
const updated = await pb.collection('users').update('USER_ID', formData);
|
||||||
|
this.uploadedUrl = pb.files.getURL(updated, updated.avatar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vanilla JavaScript
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input type="file" id="fileInput" />
|
||||||
|
<img id="preview" />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
const preview = document.getElementById('preview');
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', file);
|
||||||
|
|
||||||
|
const updated = await pb.collection('users').update('USER_ID', formData);
|
||||||
|
const avatarUrl = pb.files.getURL(updated, updated.avatar);
|
||||||
|
|
||||||
|
preview.src = avatarUrl;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Validation
|
||||||
|
|
||||||
|
### Client-Side Validation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function validateFile(file) {
|
||||||
|
const maxSize = 10485760; // 10MB
|
||||||
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||||
|
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
alert('File too large. Max size is 10MB.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
alert('Invalid file type. Only images allowed.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (validateFile(file)) {
|
||||||
|
// Proceed with upload
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Side Validation
|
||||||
|
|
||||||
|
Configure in file field options:
|
||||||
|
- Max file size
|
||||||
|
- Allowed MIME types
|
||||||
|
- File access rules
|
||||||
|
|
||||||
|
## CDN Integration
|
||||||
|
|
||||||
|
### Using External CDN
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// PocketBase behind CDN
|
||||||
|
const pb = new PocketBase('https://cdn.yoursite.com');
|
||||||
|
|
||||||
|
// Or proxy files through CDN
|
||||||
|
const cdnUrl = `https://cdn.yoursite.com${pb.files.getURL(record, record.avatar)}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloudflare R2 / AWS S3
|
||||||
|
|
||||||
|
PocketBase can work with S3-compatible storage:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In production config
|
||||||
|
export default {
|
||||||
|
dataDir: '/path/to/data',
|
||||||
|
// S3 configuration
|
||||||
|
s3: {
|
||||||
|
endpoint: 'https://s3.amazonaws.com',
|
||||||
|
bucket: 'your-bucket',
|
||||||
|
region: 'us-east-1',
|
||||||
|
accessKey: 'YOUR_KEY',
|
||||||
|
secretKey: 'YOUR_SECRET'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Storage Locations
|
||||||
|
|
||||||
|
### Local Storage
|
||||||
|
|
||||||
|
Default - files stored in `pb_data/db/files/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pb_data/
|
||||||
|
db/
|
||||||
|
files/
|
||||||
|
collection_id/
|
||||||
|
record_id/
|
||||||
|
filename1.jpg
|
||||||
|
filename2.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloud Storage
|
||||||
|
|
||||||
|
Configure in `pocketbase.js` config:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
const pb = new PocketBase('http://127.0.0.1:8090', {
|
||||||
|
files: {
|
||||||
|
// S3 or S3-compatible
|
||||||
|
endpoint: 'https://your-s3-endpoint',
|
||||||
|
bucket: 'your-bucket',
|
||||||
|
region: 'your-region',
|
||||||
|
accessKey: 'your-access-key',
|
||||||
|
secretKey: 'your-secret-key'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Metadata
|
||||||
|
|
||||||
|
### Access File Information
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const post = await pb.collection('posts').getOne('POST_ID');
|
||||||
|
|
||||||
|
// File objects contain:
|
||||||
|
{
|
||||||
|
"@collectionId": "...",
|
||||||
|
"@collectionName": "...",
|
||||||
|
"id": "file-id",
|
||||||
|
"name": "filename.jpg",
|
||||||
|
"title": "Original filename",
|
||||||
|
"size": 1048576, // File size in bytes
|
||||||
|
"type": "image/jpeg", // MIME type
|
||||||
|
"width": 1920, // Image width (if image)
|
||||||
|
"height": 1080, // Image height (if image)
|
||||||
|
"created": "2024-01-01T00:00:00.000Z",
|
||||||
|
"updated": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom File Metadata
|
||||||
|
|
||||||
|
Store additional file information:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// When uploading
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('document', file);
|
||||||
|
formData.append('description', 'My document'); // Custom field
|
||||||
|
|
||||||
|
const record = await pb.collection('documents').create(formData);
|
||||||
|
|
||||||
|
// Access later
|
||||||
|
console.log(record.description);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progress Tracking
|
||||||
|
|
||||||
|
### Upload with Progress
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function uploadWithProgress(file, onProgress) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percentComplete = (e.loaded / e.total) * 100;
|
||||||
|
onProgress(percentComplete);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', async () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const response = JSON.parse(xhr.responseText);
|
||||||
|
// Handle success
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', file);
|
||||||
|
|
||||||
|
xhr.open('PATCH', `${pb.baseUrl}/api/collections/users/records/USER_ID`);
|
||||||
|
xhr.send(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
uploadWithProgress(file, (progress) => {
|
||||||
|
console.log(`Upload progress: ${progress}%`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### 1. Set File Size Limits
|
||||||
|
```json
|
||||||
|
"maxSize": 10485760 // 10MB
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Restrict MIME Types
|
||||||
|
```json
|
||||||
|
"mimeTypes": ["image/jpeg", "image/png"] // Specific types only
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use Private Files for Sensitive Data
|
||||||
|
- Enable "Private files" option
|
||||||
|
- Use signed URLs with expiration
|
||||||
|
- Implement proper file rules
|
||||||
|
|
||||||
|
### 4. Validate File Content
|
||||||
|
```javascript
|
||||||
|
// Check file type
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
throw new Error('Only images allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file extension
|
||||||
|
const validExtensions = ['.jpg', '.jpeg', '.png', '.gif'];
|
||||||
|
if (!validExtensions.some(ext => file.name.toLowerCase().endsWith(ext))) {
|
||||||
|
throw new Error('Invalid file extension');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Sanitize Filenames
|
||||||
|
```javascript
|
||||||
|
// Remove special characters
|
||||||
|
const sanitizedName = file.name.replace(/[^a-zA-Z0-9.]/g, '_');
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
const uniqueName = `${Date.now()}_${sanitizedName}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Implement File Rules
|
||||||
|
```javascript
|
||||||
|
// Only owners can upload
|
||||||
|
File Rule: user_id = @request.auth.id
|
||||||
|
|
||||||
|
// Public read, authenticated write
|
||||||
|
File Rule: @request.auth.id != ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Monitor File Usage
|
||||||
|
- Track storage usage
|
||||||
|
- Monitor for abuse
|
||||||
|
- Set up alerts for unusual activity
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### User Avatars
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "file",
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"mimeTypes": ["image/*"],
|
||||||
|
"thumbs": ["100x100", "300x300"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Document Storage
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "documents",
|
||||||
|
"type": "file",
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 10,
|
||||||
|
"maxSize": 52428800,
|
||||||
|
"mimeTypes": ["application/pdf", "text/*", "application/msword"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Product Images
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "file",
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 10,
|
||||||
|
"maxSize": 10485760,
|
||||||
|
"mimeTypes": ["image/*"],
|
||||||
|
"thumbs": ["300x300", "800x800"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Media Gallery
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "media",
|
||||||
|
"type": "file",
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 50,
|
||||||
|
"maxSize": 104857600,
|
||||||
|
"mimeTypes": ["image/*", "video/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Upload fails with 413 (Payload Too Large)**
|
||||||
|
- File exceeds maxSize limit
|
||||||
|
- Increase maxSize in field options
|
||||||
|
- Or split large file into smaller chunks
|
||||||
|
|
||||||
|
**File type rejected**
|
||||||
|
- Check mimeTypes in field options
|
||||||
|
- Verify actual file type (not just extension)
|
||||||
|
- Update allowed types
|
||||||
|
|
||||||
|
**Private file returns 403**
|
||||||
|
- Ensure user is authenticated
|
||||||
|
- Use signed URL with expiration
|
||||||
|
- Check file rules allow access
|
||||||
|
|
||||||
|
**Thumbnail not generating**
|
||||||
|
- Verify file is an image
|
||||||
|
- Check thumbs array in field options
|
||||||
|
- Ensure PocketBase has GD/ImageMagick extension
|
||||||
|
|
||||||
|
**Slow file uploads**
|
||||||
|
- Check network connection
|
||||||
|
- Reduce file size
|
||||||
|
- Use CDN for large files
|
||||||
|
- Enable compression
|
||||||
|
|
||||||
|
## Related Topics
|
||||||
|
|
||||||
|
- [Collections](collections.md) - File field configuration
|
||||||
|
- [Authentication](authentication.md) - User file access
|
||||||
|
- [API Files](../api_files.md) - File API endpoints
|
||||||
|
- [Security Rules](../security_rules.md) - File access control
|
||||||
|
- [Going to Production](going_to_production.md) - Production file storage
|
||||||
246
skills/pocketbase/references/core/getting_started.md
Normal file
246
skills/pocketbase/references/core/getting_started.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# Getting Started with PocketBase
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
PocketBase is an open-source backend consisting of:
|
||||||
|
- **SQLite database** with real-time subscriptions
|
||||||
|
- **Built-in Admin Dashboard UI** (single-page application)
|
||||||
|
- **Authentication** (email/password, OAuth2, magic link)
|
||||||
|
- **File storage** with automatic image resizing
|
||||||
|
- **RESTful APIs** with CORS support
|
||||||
|
- **WebSocket** for real-time updates
|
||||||
|
- **Admin dashboard** for data management
|
||||||
|
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
### Option 1: Download Binary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download latest release
|
||||||
|
wget https://github.com/pocketbase/pocketbase/releases/latest/download/pocketbase_0.20.0_linux_amd64.zip
|
||||||
|
|
||||||
|
# Unzip
|
||||||
|
unzip pocketbase_0.20.0_linux_amd64.zip
|
||||||
|
|
||||||
|
# Serve on port 8090
|
||||||
|
./pocketbase serve --http=0.0.0.0:8090
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit http://127.0.0.1:8090/_/ to access the admin dashboard.
|
||||||
|
|
||||||
|
💡 **Want to master the PocketBase CLI?** See the comprehensive [CLI Commands Guide](cli_commands.md) for detailed information on `serve`, `migrate`, `superuser`, and all CLI commands.
|
||||||
|
|
||||||
|
### Option 2: Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-v pb_data:/pb_data \
|
||||||
|
-p 8090:8090 \
|
||||||
|
--name pocketbase \
|
||||||
|
ghcr.io/pocketbase/pocketbase:latest serve --http=0.0.0.0:8090
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Docker Compose
|
||||||
|
|
||||||
|
Create `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
pocketbase:
|
||||||
|
image: ghcr.io/pocketbase/pocketbase:latest
|
||||||
|
command: serve --http=0.0.0.0:8090
|
||||||
|
volumes:
|
||||||
|
- ./pb_data:/pb_data
|
||||||
|
ports:
|
||||||
|
- "8090:8090"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## First Steps in Admin Dashboard
|
||||||
|
|
||||||
|
1. **Create Admin Account**
|
||||||
|
- Navigate to http://localhost:8090/_/
|
||||||
|
- Enter email and password
|
||||||
|
- Click "Create and Login"
|
||||||
|
|
||||||
|
2. **Configure Settings**
|
||||||
|
- Go to Settings → CORS
|
||||||
|
- Add your frontend domain (e.g., `http://localhost:3000`)
|
||||||
|
- Click "Save"
|
||||||
|
|
||||||
|
3. **Create Your First Collection**
|
||||||
|
- Go to Collections → New Collection
|
||||||
|
- Choose between:
|
||||||
|
- **Base collection** - flexible schema
|
||||||
|
- **Auth collection** - for user management
|
||||||
|
- **View collection** - read-only computed data
|
||||||
|
|
||||||
|
## Basic Concepts
|
||||||
|
|
||||||
|
### Collections
|
||||||
|
Collections are like tables in a traditional database. Each collection has:
|
||||||
|
- **Schema** - fields and their types
|
||||||
|
- **Rules** - access control (read, write, delete)
|
||||||
|
- **Indexes** - performance optimization
|
||||||
|
- **Options** - additional settings
|
||||||
|
|
||||||
|
### Records
|
||||||
|
Records are individual entries in a collection, similar to rows in a table. Each record:
|
||||||
|
- Has a unique `id`
|
||||||
|
- Contains data based on collection schema
|
||||||
|
- Has built-in fields: `id`, `created`, `updated`
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
User accounts can be created through:
|
||||||
|
- Email/Password registration
|
||||||
|
- OAuth2 providers (Google, GitHub, etc.)
|
||||||
|
- Magic link authentication
|
||||||
|
|
||||||
|
### Files
|
||||||
|
File fields allow:
|
||||||
|
- Single or multiple file uploads
|
||||||
|
- Automatic thumbnail generation
|
||||||
|
- MIME type restrictions
|
||||||
|
- Size limits
|
||||||
|
|
||||||
|
## Frontend Integration
|
||||||
|
|
||||||
|
### JavaScript SDK
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/pocketbase@latest/dist/pocketbase.umd.js"></script>
|
||||||
|
<script>
|
||||||
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||||
|
|
||||||
|
// Example: Register user
|
||||||
|
const authData = await pb.collection('users').create({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
passwordConfirm: 'password123'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Example: Login
|
||||||
|
const authData = await pb.collection('users').authWithPassword(
|
||||||
|
'test@example.com',
|
||||||
|
'password123'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Example: Create record
|
||||||
|
const record = await pb.collection('posts').create({
|
||||||
|
title: 'My First Post',
|
||||||
|
content: 'Hello world!'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### React Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install pocketbase
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||||
|
|
||||||
|
// React hook for auth state
|
||||||
|
function useAuth() {
|
||||||
|
const [user, setUser] = React.useState(pb.authStore.model);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const unsub = pb.authStore.onChange(() => {
|
||||||
|
setUser(pb.authStore.model);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsub();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { user };
|
||||||
|
}
|
||||||
|
|
||||||
|
// React component
|
||||||
|
function Posts() {
|
||||||
|
const [posts, setPosts] = React.useState([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
loadPosts();
|
||||||
|
|
||||||
|
// Subscribe to real-time updates
|
||||||
|
pb.collection('posts').subscribe('*', () => {
|
||||||
|
loadPosts();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => pb.collection('posts').unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function loadPosts() {
|
||||||
|
const records = await pb.collection('posts').getList(1, 50);
|
||||||
|
setPosts(records.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts.map(post => <div key={post.id}>{post.title}</div>);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **Schema Design** - Define your data structure (see `collections.md`)
|
||||||
|
- **Authentication** - Set up user management (see `authentication.md`)
|
||||||
|
- **Security Rules** - Control data access (see `security_rules.md`)
|
||||||
|
- **API Integration** - Build your frontend (see `api_records.md`)
|
||||||
|
- **Production Setup** - Deploy to production (see `going_to_production.md`)
|
||||||
|
|
||||||
|
## Common First Tasks
|
||||||
|
|
||||||
|
### Task: Create a Blog
|
||||||
|
1. Create `posts` collection (auth collection)
|
||||||
|
2. Add fields: `title`, `content`, `published` (bool)
|
||||||
|
3. Set rules: public read, author write
|
||||||
|
4. Create first post via Admin UI or API
|
||||||
|
|
||||||
|
### Task: User Profiles
|
||||||
|
1. Users collection already exists (auth collection)
|
||||||
|
2. Add profile fields: `name`, `bio`, `avatar`
|
||||||
|
3. Set rules: user can update own profile
|
||||||
|
4. Build profile page in frontend
|
||||||
|
|
||||||
|
### Task: Comments System
|
||||||
|
1. Create `comments` collection (base collection)
|
||||||
|
2. Add fields: `post`, `author`, `content`
|
||||||
|
3. Create relation to posts collection
|
||||||
|
4. Set rules: public read, authenticated write
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Can't access admin dashboard**
|
||||||
|
- Check if PocketBase is running
|
||||||
|
- Verify port 8090 is not blocked
|
||||||
|
- Try http://127.0.0.1:8090/_/ instead of localhost
|
||||||
|
|
||||||
|
**CORS errors in frontend**
|
||||||
|
- Go to Settings → CORS
|
||||||
|
- Add your frontend domain
|
||||||
|
- Save changes
|
||||||
|
|
||||||
|
**Can't create records**
|
||||||
|
- Check collection rules
|
||||||
|
- Verify user is authenticated
|
||||||
|
- Check required fields are provided
|
||||||
|
|
||||||
|
**File uploads failing**
|
||||||
|
- Check file size limits
|
||||||
|
- Verify MIME types allowed
|
||||||
|
- Ensure user has create permissions
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Official Docs](https://pocketbase.io/docs/)
|
||||||
|
- [Examples](https://github.com/pocketbase/examples)
|
||||||
|
- [Discord Community](https://discord.gg/G5Vd6UF)
|
||||||
|
- [GitHub Repository](https://github.com/pocketbase/pocketbase)
|
||||||
733
skills/pocketbase/references/core/going_to_production.md
Normal file
733
skills/pocketbase/references/core/going_to_production.md
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
# Going to Production with PocketBase
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide covers production deployment, optimization, security, and maintenance for PocketBase applications.
|
||||||
|
|
||||||
|
## Deployment Options
|
||||||
|
|
||||||
|
### 1. Docker Deployment
|
||||||
|
|
||||||
|
#### Production Docker Compose
|
||||||
|
|
||||||
|
Create `docker-compose.prod.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
pocketbase:
|
||||||
|
image: ghcr.io/pocketbase/pocketbase:latest
|
||||||
|
command: serve --https=0.0.0.0:443 --http=0.0.0.0:80
|
||||||
|
volumes:
|
||||||
|
- ./pb_data:/pb_data
|
||||||
|
environment:
|
||||||
|
- PB_PUBLIC_DIR=/pb_public
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
depends_on:
|
||||||
|
- pocketbase
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- ./pb_data:/pb_data:ro
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Caddyfile Configuration
|
||||||
|
|
||||||
|
Create `Caddyfile`:
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
yourdomain.com {
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
reverse_proxy pocketbase:8090
|
||||||
|
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
X-XSS-Protection "1; mode=block"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Reverse Proxy (Nginx)
|
||||||
|
|
||||||
|
#### Nginx Configuration
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/certificate.crt;
|
||||||
|
ssl_certificate_key /path/to/private.key;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8090;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Cloud Platform Deployment
|
||||||
|
|
||||||
|
#### Railway
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# railway.toml
|
||||||
|
[build]
|
||||||
|
builder = "NIXPACKS"
|
||||||
|
|
||||||
|
[deploy]
|
||||||
|
startCommand = "./pocketbase serve --https=0.0.0.0:$PORT"
|
||||||
|
restartPolicyType = "ON_FAILURE"
|
||||||
|
restartPolicyMaxRetries = 10
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fly.io
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# fly.toml
|
||||||
|
app = "your-app-name"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
builder = "paketobuildpacks/builder:base"
|
||||||
|
|
||||||
|
[[services]]
|
||||||
|
internal_port = 8090
|
||||||
|
protocol = "tcp"
|
||||||
|
|
||||||
|
[[services.ports]]
|
||||||
|
handlers = ["tls", "http"]
|
||||||
|
port = 443
|
||||||
|
|
||||||
|
[[services.ports]]
|
||||||
|
handlers = ["http"]
|
||||||
|
port = 80
|
||||||
|
|
||||||
|
[env]
|
||||||
|
PB_PUBLIC_DIR = "/app/public"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DigitalOcean App Platform
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: pocketbase-app
|
||||||
|
services:
|
||||||
|
- name: pocketbase
|
||||||
|
source_dir: /
|
||||||
|
github:
|
||||||
|
repo: your-username/pocketbase-repo
|
||||||
|
branch: main
|
||||||
|
run_command: ./pocketbase serve --https=0.0.0.0:$PORT
|
||||||
|
environment_slug: ubuntu-js
|
||||||
|
instance_count: 1
|
||||||
|
instance_size_slug: basic-xxs
|
||||||
|
envs:
|
||||||
|
- key: PB_PUBLIC_DIR
|
||||||
|
value: /app/public
|
||||||
|
http_port: 8090
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
PB_DATA_DIR=/pb_data
|
||||||
|
PB_PUBLIC_DIR=/pb_public
|
||||||
|
|
||||||
|
# Optional: Database encryption
|
||||||
|
PB_ENCRYPTION_KEY=your-32-character-encryption-key
|
||||||
|
|
||||||
|
# Optional: Email configuration
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USERNAME=your-email@gmail.com
|
||||||
|
SMTP_PASSWORD=your-app-password
|
||||||
|
|
||||||
|
# Optional: CORS
|
||||||
|
PB_CORS_ORIGINS=https://yourdomain.com,https://admin.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom PocketBase Configuration
|
||||||
|
|
||||||
|
Create `pocketbase.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||||
|
|
||||||
|
// Custom configuration
|
||||||
|
pb.baseOptions = {
|
||||||
|
files: {
|
||||||
|
// S3 configuration
|
||||||
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
|
bucket: process.env.S3_BUCKET,
|
||||||
|
region: process.env.S3_REGION,
|
||||||
|
accessKey: process.env.S3_ACCESS_KEY,
|
||||||
|
secretKey: process.env.S3_SECRET_KEY,
|
||||||
|
},
|
||||||
|
// Custom auth settings
|
||||||
|
auth: {
|
||||||
|
tokenExpDays: 7,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default pb;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Hardening
|
||||||
|
|
||||||
|
### 1. Enable HTTPS
|
||||||
|
|
||||||
|
Always use HTTPS in production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Let's Encrypt with Certbot
|
||||||
|
certbot --nginx -d yourdomain.com -d api.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Security Headers
|
||||||
|
|
||||||
|
Configure in reverse proxy (see Nginx/Caddy configuration above):
|
||||||
|
|
||||||
|
```
|
||||||
|
Strict-Transport-Security
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-Frame-Options: SAMEORIGIN
|
||||||
|
X-XSS-Protection: 1; mode=block
|
||||||
|
Content-Security-Policy
|
||||||
|
Referrer-Policy
|
||||||
|
Permissions-Policy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. File Upload Security
|
||||||
|
|
||||||
|
Configure file restrictions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"maxSize": 10485760, // 10MB
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif"
|
||||||
|
],
|
||||||
|
"privateFiles": true // Enable for sensitive files
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Database Encryption
|
||||||
|
|
||||||
|
Enable field-level encryption for sensitive data:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Enable encryption in PocketBase config
|
||||||
|
export default {
|
||||||
|
dataDir: '/pb_data',
|
||||||
|
encryptionEnv: 'PB_ENCRYPTION_KEY',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Rate Limiting
|
||||||
|
|
||||||
|
Implement at reverse proxy level:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Nginx rate limiting
|
||||||
|
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
limit_req zone=api burst=20 nodelay;
|
||||||
|
proxy_pass http://127.0.0.1:8090;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
# Caddy rate limiting
|
||||||
|
{
|
||||||
|
限流 yourdomain.com 100 # 100 requests per second
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Admin Access Restrictions
|
||||||
|
|
||||||
|
Restrict admin UI access:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Allow only specific IP
|
||||||
|
location /_/ {
|
||||||
|
allow 192.168.1.0/24;
|
||||||
|
deny all;
|
||||||
|
proxy_pass http://127.0.0.1:8090;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### 1. Database Indexing
|
||||||
|
|
||||||
|
Add indexes for frequently queried fields:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Users table
|
||||||
|
CREATE INDEX idx_users_email ON users(email);
|
||||||
|
CREATE INDEX idx_users_created ON users(created);
|
||||||
|
|
||||||
|
-- Posts table
|
||||||
|
CREATE INDEX idx_posts_status ON posts(status);
|
||||||
|
CREATE INDEX idx_posts_author ON posts(author_id);
|
||||||
|
CREATE INDEX idx_posts_created ON posts(created);
|
||||||
|
|
||||||
|
-- Comments table
|
||||||
|
CREATE INDEX idx_comments_post ON comments(post_id);
|
||||||
|
CREATE INDEX idx_comments_created ON comments(created);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Query Optimization
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Select only needed fields
|
||||||
|
const posts = await pb.collection('posts').getList(1, 50, {
|
||||||
|
fields: 'id,title,author,created,-content' // Exclude large fields
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use filters instead of fetching all
|
||||||
|
const recentPosts = await pb.collection('posts').getList(1, 50, {
|
||||||
|
filter: 'created >= "2024-01-01"',
|
||||||
|
sort: '-created'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paginate properly
|
||||||
|
const page1 = await pb.collection('posts').getList(1, 50);
|
||||||
|
const page2 = await pb.collection('posts').getList(2, 50);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Caching
|
||||||
|
|
||||||
|
Implement caching for frequently accessed data:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Client-side caching
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
|
async function getCachedPost(id) {
|
||||||
|
if (cache.has(id)) {
|
||||||
|
return cache.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = await pb.collection('posts').getOne(id);
|
||||||
|
cache.set(id, post);
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache on updates
|
||||||
|
pb.collection('posts').subscribe('*', (e) => {
|
||||||
|
cache.delete(e.record.id);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. CDN for Static Assets
|
||||||
|
|
||||||
|
Use CDN for file storage:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Configure CDN
|
||||||
|
const CDN_URL = 'https://cdn.yourdomain.com';
|
||||||
|
const fileUrl = `${CDN_URL}${pb.files.getURL(record, record.file)}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Connection Pooling
|
||||||
|
|
||||||
|
Configure in proxy:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
upstream pocketbase {
|
||||||
|
server 127.0.0.1:8090;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://pocketbase;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Logging
|
||||||
|
|
||||||
|
### 1. Health Check Endpoint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check health
|
||||||
|
curl https://yourdomain.com/api/health
|
||||||
|
# Returns: {"code":200,"data":{"status":"ok","metrics":{"clients":0}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Application Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
docker logs -f pocketbase
|
||||||
|
|
||||||
|
# Or redirect to file
|
||||||
|
docker logs -f pocketbase > /var/log/pocketbase.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Monitoring Setup
|
||||||
|
|
||||||
|
#### Prometheus Metrics
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Custom metrics endpoint
|
||||||
|
app.OnServe().Add("GET", "/metrics", func(e *core.ServeEvent) error {
|
||||||
|
// Return Prometheus metrics
|
||||||
|
return e.Next()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Log Aggregation
|
||||||
|
|
||||||
|
Configure log shipping:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Filebeat configuration
|
||||||
|
filebeat.inputs:
|
||||||
|
- type: log
|
||||||
|
enabled: true
|
||||||
|
paths:
|
||||||
|
- /var/log/pocketbase.log
|
||||||
|
fields:
|
||||||
|
service: pocketbase
|
||||||
|
fields_under_root: true
|
||||||
|
|
||||||
|
output.elasticsearch:
|
||||||
|
hosts: ["elasticsearch:9200"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Error Tracking
|
||||||
|
|
||||||
|
Integrate with Sentry:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// JavaScript SDK
|
||||||
|
import * as Sentry from "@sentry/browser";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "YOUR_SENTRY_DSN",
|
||||||
|
environment: "production"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture errors
|
||||||
|
try {
|
||||||
|
await pb.collection('posts').getList(1, 50);
|
||||||
|
} catch (error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup Strategy
|
||||||
|
|
||||||
|
### 1. Automated Backups
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# backup.sh
|
||||||
|
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
BACKUP_DIR="/backups/pocketbase"
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
cp -r /pb_data $BACKUP_DIR/pb_data_$DATE
|
||||||
|
|
||||||
|
# Upload to cloud storage
|
||||||
|
aws s3 sync $BACKUP_DIR s3://your-backup-bucket/pocketbase/
|
||||||
|
|
||||||
|
# Keep only last 7 days
|
||||||
|
find $BACKUP_DIR -type d -mtime +7 -exec rm -rf {} +
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to crontab:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Daily backup at 2 AM
|
||||||
|
0 2 * * * /path/to/backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Point-in-Time Recovery
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restore from backup
|
||||||
|
cd /path/to/new/pocketbase
|
||||||
|
cp -r /backups/pocketbase/pb_data_YYYYMMDD_HHMMSS/* ./pb_data/
|
||||||
|
./pocketbase migrate up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Cross-Region Replication
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Docker Compose with backup service
|
||||||
|
services:
|
||||||
|
pocketbase:
|
||||||
|
image: ghcr.io/pocketbase/pocketbase:latest
|
||||||
|
volumes:
|
||||||
|
- ./pb_data:/pb_data
|
||||||
|
|
||||||
|
backup:
|
||||||
|
image: alpine:latest
|
||||||
|
volumes:
|
||||||
|
- ./pb_data:/data
|
||||||
|
- ./backups:/backups
|
||||||
|
command: |
|
||||||
|
sh -c '
|
||||||
|
while true; do
|
||||||
|
tar czf /backups/pb_$$(date +%Y%m%d_%H%M%S).tar.gz -C /data .
|
||||||
|
sleep 3600
|
||||||
|
done
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scaling Considerations
|
||||||
|
|
||||||
|
### 1. Vertical Scaling
|
||||||
|
|
||||||
|
Increase server resources:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Docker Compose
|
||||||
|
services:
|
||||||
|
pocketbase:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2G
|
||||||
|
cpus: '2'
|
||||||
|
reservations:
|
||||||
|
memory: 1G
|
||||||
|
cpus: '1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Horizontal Scaling (Read Replicas)
|
||||||
|
|
||||||
|
For read-heavy workloads:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Nginx upstream
|
||||||
|
upstream pocketbase_read {
|
||||||
|
server primary:8090;
|
||||||
|
server replica1:8090;
|
||||||
|
server replica2:8090;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/records {
|
||||||
|
proxy_pass http://pocketbase_read;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Database Scaling
|
||||||
|
|
||||||
|
Consider database sharding for very large datasets:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Partition large tables
|
||||||
|
CREATE TABLE posts_2024 PARTITION OF posts
|
||||||
|
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Production Issues
|
||||||
|
|
||||||
|
### Issue 1: Out of Memory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Monitor memory usage
|
||||||
|
docker stats pocketbase
|
||||||
|
|
||||||
|
# Increase memory limit
|
||||||
|
docker run --memory=2g pocketbase
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 2: Disk Space Full
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check disk usage
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# Clean old logs
|
||||||
|
journalctl --vacuum-time=7d
|
||||||
|
|
||||||
|
# Rotate logs
|
||||||
|
logrotate -f /etc/logrotate.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 3: Slow Queries
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Analyze slow queries
|
||||||
|
EXPLAIN QUERY PLAN SELECT * FROM posts WHERE status = 'published';
|
||||||
|
|
||||||
|
-- Add missing indexes
|
||||||
|
CREATE INDEX idx_posts_status ON posts(status);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 4: SSL Certificate Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Renew Let's Encrypt certificate
|
||||||
|
certbot renew --nginx
|
||||||
|
|
||||||
|
# Check certificate expiration
|
||||||
|
openssl x509 -in /path/to/cert.pem -text -noout | grep "Not After"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 5: CORS Errors
|
||||||
|
|
||||||
|
Update CORS settings in Admin UI:
|
||||||
|
- Go to Settings → CORS
|
||||||
|
- Add production domains
|
||||||
|
- Save changes
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Regular Tasks
|
||||||
|
|
||||||
|
1. **Weekly**
|
||||||
|
- Review application logs
|
||||||
|
- Check disk usage
|
||||||
|
- Verify backup integrity
|
||||||
|
- Monitor performance metrics
|
||||||
|
|
||||||
|
2. **Monthly**
|
||||||
|
- Update PocketBase to latest version
|
||||||
|
- Security audit of collections and rules
|
||||||
|
- Review and optimize slow queries
|
||||||
|
- Test disaster recovery procedures
|
||||||
|
|
||||||
|
3. **Quarterly**
|
||||||
|
- Security penetration testing
|
||||||
|
- Performance optimization review
|
||||||
|
- Infrastructure cost review
|
||||||
|
- Update documentation
|
||||||
|
|
||||||
|
### Update Procedure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create backup
|
||||||
|
./backup.sh
|
||||||
|
|
||||||
|
# 2. Update PocketBase
|
||||||
|
docker pull ghcr.io/pocketbase/pocketbase:latest
|
||||||
|
|
||||||
|
# 3. Stop current instance
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 4. Start with new image
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 5. Verify functionality
|
||||||
|
curl https://yourdomain.com/api/health
|
||||||
|
|
||||||
|
# 6. Check logs
|
||||||
|
docker logs -f pocketbase
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Pipeline
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Deploy PocketBase
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Deploy to production
|
||||||
|
uses: appleboy/ssh-action@v0.1.5
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.HOST }}
|
||||||
|
username: ${{ secrets.USERNAME }}
|
||||||
|
key: ${{ secrets.KEY }}
|
||||||
|
script: |
|
||||||
|
cd /path/to/pocketbase
|
||||||
|
docker-compose pull
|
||||||
|
docker-compose up -d
|
||||||
|
./backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices Checklist
|
||||||
|
|
||||||
|
- [ ] HTTPS enabled with valid certificate
|
||||||
|
- [ ] Security headers configured
|
||||||
|
- [ ] File upload restrictions in place
|
||||||
|
- [ ] Database encryption enabled for sensitive data
|
||||||
|
- [ ] Rate limiting configured
|
||||||
|
- [ ] Admin UI access restricted
|
||||||
|
- [ ] Database indexes added for performance
|
||||||
|
- [ ] Automated backups scheduled
|
||||||
|
- [ ] Monitoring and alerting set up
|
||||||
|
- [ ] Logs aggregated and monitored
|
||||||
|
- [ ] Environment variables configured
|
||||||
|
- [ ] CORS settings updated for production
|
||||||
|
- [ ] SSL certificate auto-renewal configured
|
||||||
|
- [ ] Disaster recovery procedure documented
|
||||||
|
- [ ] Performance benchmarks established
|
||||||
|
|
||||||
|
## Related Topics
|
||||||
|
|
||||||
|
- [Getting Started](getting_started.md) - Initial setup
|
||||||
|
- [Authentication](authentication.md) - Security best practices
|
||||||
|
- [Files Handling](files_handling.md) - File storage security
|
||||||
|
- [Security Rules](../security_rules.md) - Access control
|
||||||
|
- [API Rules & Filters](api_rules_filters.md) - Query optimization
|
||||||
802
skills/pocketbase/references/core/working_with_relations.md
Normal file
802
skills/pocketbase/references/core/working_with_relations.md
Normal file
@@ -0,0 +1,802 @@
|
|||||||
|
# Working with Relations in PocketBase
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Relations create links between collections, allowing you to:
|
||||||
|
- Link records across collections
|
||||||
|
- Create one-to-one relationships
|
||||||
|
- Create one-to-many relationships
|
||||||
|
- Create many-to-many relationships
|
||||||
|
- Maintain data integrity
|
||||||
|
- Build complex data models
|
||||||
|
|
||||||
|
## Relation Field Types
|
||||||
|
|
||||||
|
### 1. One-to-One (Single Relation)
|
||||||
|
Each record relates to exactly one record in another collection.
|
||||||
|
|
||||||
|
**Example:** User → Profile
|
||||||
|
- Each user has one profile
|
||||||
|
- Each profile belongs to one user
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "profile",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "PROFILE_COLLECTION_ID",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"cascadeDelete": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. One-to-Many (Single Record, Multiple Related)
|
||||||
|
One record relates to many records in another collection.
|
||||||
|
|
||||||
|
**Example:** Post → Comments
|
||||||
|
- One post has many comments
|
||||||
|
- Each comment belongs to one post
|
||||||
|
|
||||||
|
**On Post Collection:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "comments",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "COMMENTS_COLLECTION_ID",
|
||||||
|
"maxSelect": null,
|
||||||
|
"cascadeDelete": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**On Comments Collection:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "post",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "POSTS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"cascadeDelete": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Many-to-Many (Junction Table)
|
||||||
|
Multiple records relate to multiple records in another collection.
|
||||||
|
|
||||||
|
**Example:** Posts ↔ Tags
|
||||||
|
- One post has many tags
|
||||||
|
- One tag belongs to many posts
|
||||||
|
|
||||||
|
**Junction Collection (posts_tags):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "post",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "POSTS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "tag",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "TAGS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relation Field Options
|
||||||
|
|
||||||
|
### collectionId
|
||||||
|
Target collection ID:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"collectionId": "abcd1234abcd1234abcd1234"
|
||||||
|
```
|
||||||
|
|
||||||
|
### maxSelect
|
||||||
|
Maximum number of related records:
|
||||||
|
- `1` - Single relation
|
||||||
|
- `null` or `2+` - Multiple relations
|
||||||
|
|
||||||
|
```json
|
||||||
|
"maxSelect": 1 // One-to-one
|
||||||
|
"maxSelect": null // One-to-many
|
||||||
|
"maxSelect": 5 // Limited multiple
|
||||||
|
```
|
||||||
|
|
||||||
|
### cascadeDelete
|
||||||
|
Delete related records when this record is deleted:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"cascadeDelete": true // Delete comments when post deleted
|
||||||
|
"cascadeDelete": false // Keep comments when post deleted
|
||||||
|
```
|
||||||
|
|
||||||
|
### displayFields
|
||||||
|
Fields to show when displaying relation:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"displayFields": ["name", "email"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Relations
|
||||||
|
|
||||||
|
### One-to-Many Example
|
||||||
|
|
||||||
|
**Collections:**
|
||||||
|
1. `posts` collection
|
||||||
|
2. `comments` collection
|
||||||
|
|
||||||
|
**Posts Schema:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Comments Schema:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "post",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "POSTS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "author",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "USERS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Related Records
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create post
|
||||||
|
const post = await pb.collection('posts').create({
|
||||||
|
title: 'My First Post',
|
||||||
|
content: 'Hello world!'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create comment with relation
|
||||||
|
const comment = await pb.collection('comments').create({
|
||||||
|
post: post.id, // Link to post
|
||||||
|
author: pb.authStore.model.id, // Link to current user
|
||||||
|
content: 'Great post!'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Querying Relations
|
||||||
|
|
||||||
|
### Get Record with Related Data
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get post with comments expanded
|
||||||
|
const post = await pb.collection('posts').getOne(postId, {
|
||||||
|
expand: 'comments'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(post.title);
|
||||||
|
post.expand.comments.forEach(comment => {
|
||||||
|
console.log(comment.content);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter by Related Field
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get all comments for a specific post
|
||||||
|
const comments = await pb.collection('comments').getList(1, 50, {
|
||||||
|
filter: 'post = "' + postId + '"'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or use expand
|
||||||
|
const post = await pb.collection('posts').getOne(postId, {
|
||||||
|
expand: 'comments'
|
||||||
|
});
|
||||||
|
const comments = post.expand.comments;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter by Nested Relation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get posts where author email is specific value
|
||||||
|
const posts = await pb.collection('posts').getList(1, 50, {
|
||||||
|
filter: 'expand.author.email = "user@example.com"'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter by Relation's Related Field
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get comments on posts by specific author
|
||||||
|
const comments = await pb.collection('comments').getList(1, 50, {
|
||||||
|
filter: 'expand.post.author.email = "user@example.com"'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating Relations
|
||||||
|
|
||||||
|
### Update One-to-One Relation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create profile for user
|
||||||
|
const profile = await pb.collection('profiles').create({
|
||||||
|
bio: 'My bio',
|
||||||
|
user: userId // Link to user
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update One-to-Many Relation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Add comment to post
|
||||||
|
const comment = await pb.collection('comments').create({
|
||||||
|
post: postId,
|
||||||
|
content: 'New comment'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Comments are automatically added to post's comments array
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Many-to-Many Relations
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create post
|
||||||
|
const post = await pb.collection('posts').create({
|
||||||
|
title: 'My Post',
|
||||||
|
content: 'Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create junction record for tag
|
||||||
|
await pb.collection('posts_tags').create({
|
||||||
|
post: post.id,
|
||||||
|
tag: tagId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all tags for post
|
||||||
|
const tags = await pb.collection('posts_tags').getList(1, 100, {
|
||||||
|
filter: 'post = "' + post.id + '"',
|
||||||
|
expand: 'tag'
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagNames = tags.items.map(item => item.expand.tag.name);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expanding Relations
|
||||||
|
|
||||||
|
### Basic Expand
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Expand single level
|
||||||
|
const post = await pb.collection('posts').getOne(postId, {
|
||||||
|
expand: 'comments'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expand multiple relations
|
||||||
|
const post = await pb.collection('posts').getOne(postId, {
|
||||||
|
expand: 'comments,author'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Expand
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Expand two levels deep
|
||||||
|
const comments = await pb.collection('comments').getList(1, 50, {
|
||||||
|
expand: 'post.author'
|
||||||
|
});
|
||||||
|
|
||||||
|
comments.items.forEach(comment => {
|
||||||
|
console.log(comment.content); // Comment content
|
||||||
|
console.log(comment.expand.post.title); // Post title
|
||||||
|
console.log(comment.expand.post.expand.author); // Author object
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Selective Field Expansion
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Expand and select specific fields
|
||||||
|
const posts = await pb.collection('posts').getList(1, 50, {
|
||||||
|
expand: 'author',
|
||||||
|
fields: 'id,title,expand.author.name,expand.author.email'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deleting Relations
|
||||||
|
|
||||||
|
### Delete with Cascade
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// If cascadeDelete is true, deleting post deletes comments
|
||||||
|
await pb.collection('posts').delete(postId);
|
||||||
|
// All comments with post = this postId are deleted
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Without Cascade
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// If cascadeDelete is false, delete comment manually
|
||||||
|
await pb.collection('comments').delete(commentId);
|
||||||
|
// Post remains
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update to Remove Relation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Remove relation by setting to null (for optional relations)
|
||||||
|
const updated = await pb.collection('comments').update(commentId, {
|
||||||
|
post: null
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Many-to-Many Pattern
|
||||||
|
|
||||||
|
### Approach 1: Junction Collection
|
||||||
|
|
||||||
|
**Collections:**
|
||||||
|
- `posts`
|
||||||
|
- `tags`
|
||||||
|
- `posts_tags` (junction)
|
||||||
|
|
||||||
|
**posts_tags Schema:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "post",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "POSTS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tag",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "TAGS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Operations:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Add tag to post
|
||||||
|
await pb.collection('posts_tags').create({
|
||||||
|
post: postId,
|
||||||
|
tag: tagId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all tags for post
|
||||||
|
const postTags = await pb.collection('posts_tags').getList(1, 100, {
|
||||||
|
filter: 'post = "' + postId + '"',
|
||||||
|
expand: 'tag'
|
||||||
|
});
|
||||||
|
|
||||||
|
const tags = postTags.items.map(item => item.expand.tag);
|
||||||
|
|
||||||
|
// Remove tag from post
|
||||||
|
await pb.collection('posts_tags').delete(junctionRecordId);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Approach 2: Array Field (Advanced)
|
||||||
|
|
||||||
|
**Posts Collection:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "tags",
|
||||||
|
"type": "json" // Store tag IDs in JSON array
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Operations:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Add tag
|
||||||
|
const post = await pb.collection('posts').getOne(postId);
|
||||||
|
const tags = post.tags || [];
|
||||||
|
tags.push(tagId);
|
||||||
|
|
||||||
|
await pb.collection('posts').update(postId, {
|
||||||
|
tags: tags
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter by tag
|
||||||
|
const posts = await pb.collection('posts').getList(1, 50, {
|
||||||
|
filter: 'tags ?~ "' + tagId + '"'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### User Posts Pattern
|
||||||
|
|
||||||
|
**Collections:**
|
||||||
|
- `users` (auth)
|
||||||
|
- `posts` (base)
|
||||||
|
|
||||||
|
**Posts Schema:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "author",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "USERS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Operations:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create post as current user
|
||||||
|
const post = await pb.collection('posts').create({
|
||||||
|
author: pb.authStore.model.id,
|
||||||
|
title: 'My Post',
|
||||||
|
content: 'Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get my posts
|
||||||
|
const myPosts = await pb.collection('posts').getList(1, 50, {
|
||||||
|
filter: 'author = "' + pb.authStore.model.id + '"'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get post with author info
|
||||||
|
const post = await pb.collection('posts').getOne(postId, {
|
||||||
|
expand: 'author'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(post.expand.author.email);
|
||||||
|
```
|
||||||
|
|
||||||
|
### E-commerce Order Pattern
|
||||||
|
|
||||||
|
**Collections:**
|
||||||
|
- `users` (auth)
|
||||||
|
- `products` (base)
|
||||||
|
- `orders` (base)
|
||||||
|
- `order_items` (base)
|
||||||
|
|
||||||
|
**Order Items Schema:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "order",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "ORDERS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "product",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "PRODUCTS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quantity",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "price",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Operations:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create order
|
||||||
|
const order = await pb.collection('orders').create({
|
||||||
|
user: userId,
|
||||||
|
status: 'pending',
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add items to order
|
||||||
|
let total = 0;
|
||||||
|
for (const item of cart) {
|
||||||
|
await pb.collection('order_items').create({
|
||||||
|
order: order.id,
|
||||||
|
product: item.productId,
|
||||||
|
quantity: item.quantity,
|
||||||
|
price: item.price
|
||||||
|
});
|
||||||
|
total += item.quantity * item.price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update order total
|
||||||
|
await pb.collection('orders').update(order.id, {
|
||||||
|
total: total
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get order with items
|
||||||
|
const orderWithItems = await pb.collection('orders').getOne(orderId, {
|
||||||
|
expand: 'items,items.product,user'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Social Media Follow Pattern
|
||||||
|
|
||||||
|
**Collections:**
|
||||||
|
- `users` (auth)
|
||||||
|
- `follows` (base)
|
||||||
|
|
||||||
|
**Follows Schema:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "follower",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "USERS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "following",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "USERS_COLLECTION_ID",
|
||||||
|
"maxSelect": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Operations:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Follow user
|
||||||
|
await pb.collection('follows').create({
|
||||||
|
follower: currentUserId,
|
||||||
|
following: targetUserId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unfollow
|
||||||
|
await pb.collection('follows').delete(followId);
|
||||||
|
|
||||||
|
// Get people I follow
|
||||||
|
const following = await pb.collection('follows').getList(1, 100, {
|
||||||
|
filter: 'follower = "' + currentUserId + '"',
|
||||||
|
expand: 'following'
|
||||||
|
});
|
||||||
|
|
||||||
|
const followingUsers = following.items.map(item => item.expand.following);
|
||||||
|
|
||||||
|
// Get my followers
|
||||||
|
const followers = await pb.collection('follows').getList(1, 100, {
|
||||||
|
filter: 'following = "' + currentUserId + '"',
|
||||||
|
expand: 'follower'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Self-Referencing Relations
|
||||||
|
|
||||||
|
Create hierarchical data (categories, organizational structure):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "parent",
|
||||||
|
"type": "relation",
|
||||||
|
"options": {
|
||||||
|
"collectionId": "CATEGORIES_COLLECTION_ID",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"cascadeDelete": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Operations:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create category with parent
|
||||||
|
const child = await pb.collection('categories').create({
|
||||||
|
name: 'JavaScript',
|
||||||
|
parent: parentCategoryId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all top-level categories
|
||||||
|
const topLevel = await pb.collection('categories').getList(1, 50, {
|
||||||
|
filter: 'parent = ""'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get children of category
|
||||||
|
const children = await pb.collection('categories').getList(1, 50, {
|
||||||
|
filter: 'parent = "' + parentId + '"'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relation Rules
|
||||||
|
|
||||||
|
Control who can create, update, or delete relations:
|
||||||
|
|
||||||
|
### Owner-Based Rules
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Comments collection
|
||||||
|
Create Rule: @request.auth.id != ""
|
||||||
|
Update Rule: author = @request.auth.id
|
||||||
|
Delete Rule: author = @request.auth.id
|
||||||
|
|
||||||
|
// Posts collection
|
||||||
|
Update Rule: author = @request.auth.id || @request.auth.role = "admin"
|
||||||
|
Delete Rule: author = @request.auth.id || @request.auth.role = "admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prevent Relation Changes
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Once created, relation cannot be changed
|
||||||
|
Update Rule: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read-Only Relations
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Anyone can read, only admins can modify
|
||||||
|
View Rule: true
|
||||||
|
Update Rule: @request.auth.role = "admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Index Related Fields
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Index foreign keys for faster joins
|
||||||
|
CREATE INDEX idx_comments_post ON comments(post_id);
|
||||||
|
CREATE INDEX idx_comments_author ON comments(author_id);
|
||||||
|
CREATE INDEX idx_follows_follower ON follows(follower_id);
|
||||||
|
CREATE INDEX idx_follows_following ON follows(following_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Views for Complex Queries
|
||||||
|
|
||||||
|
Create a view for frequently accessed relation data:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE VIEW post_with_stats AS
|
||||||
|
SELECT
|
||||||
|
p.*,
|
||||||
|
(SELECT COUNT(*) FROM comments c WHERE c.post = p.id) as comment_count,
|
||||||
|
(SELECT COUNT(*) FROM likes l WHERE l.post = p.id) as like_count
|
||||||
|
FROM posts p;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Limit Expand Depth
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Instead of
|
||||||
|
expand: 'comments,comments.author,comments.author.profile'
|
||||||
|
|
||||||
|
// Use
|
||||||
|
expand: 'comments,author'
|
||||||
|
// Then load author.profile separately if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paginate Relations
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// For large relation arrays
|
||||||
|
const page1 = await pb.collection('posts').getOne(postId, {
|
||||||
|
expand: 'comments',
|
||||||
|
filter: 'created >= "2024-01-01"' // Filter comments
|
||||||
|
});
|
||||||
|
|
||||||
|
const page2 = await pb.collection('comments').getList(1, 50, {
|
||||||
|
filter: 'post = "' + postId + '" && created >= "2024-01-15"'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Relation not showing in expand**
|
||||||
|
- Check collectionId is correct
|
||||||
|
- Verify relation field name
|
||||||
|
- Check if related record exists
|
||||||
|
- Ensure user has permission to access related record
|
||||||
|
|
||||||
|
**Can't create relation**
|
||||||
|
- Check createRule on both collections
|
||||||
|
- Verify user is authenticated
|
||||||
|
- Ensure target record exists
|
||||||
|
- Check maxSelect limit
|
||||||
|
|
||||||
|
**Slow relation queries**
|
||||||
|
- Add database indexes
|
||||||
|
- Reduce expand depth
|
||||||
|
- Use views for complex queries
|
||||||
|
- Consider denormalization for performance
|
||||||
|
|
||||||
|
**Circular reference errors**
|
||||||
|
- Avoid circular relation definitions
|
||||||
|
- Use views to flatten data
|
||||||
|
- Limit expand depth
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Plan your data model**
|
||||||
|
- Sketch relationships before implementing
|
||||||
|
- Consider query patterns
|
||||||
|
- Plan for scalability
|
||||||
|
|
||||||
|
2. **Use cascadeDelete wisely**
|
||||||
|
- True for dependent data (comments → posts)
|
||||||
|
- False for independent references (posts → authors)
|
||||||
|
|
||||||
|
3. **Index foreign keys**
|
||||||
|
- Always index fields used in relations
|
||||||
|
- Improves join performance
|
||||||
|
|
||||||
|
4. **Limit expand depth**
|
||||||
|
- 2-3 levels max
|
||||||
|
- Use views for deeper expansions
|
||||||
|
|
||||||
|
5. **Consider denormalization**
|
||||||
|
- Store frequently accessed data directly
|
||||||
|
- Use views or triggers to keep in sync
|
||||||
|
|
||||||
|
6. **Use junction tables for many-to-many**
|
||||||
|
- Most flexible approach
|
||||||
|
- Easy to query and update
|
||||||
|
|
||||||
|
7. **Test relation rules thoroughly**
|
||||||
|
- Verify permissions work correctly
|
||||||
|
- Test cascadeDelete behavior
|
||||||
|
|
||||||
|
## Related Topics
|
||||||
|
|
||||||
|
- [Collections](collections.md) - Collection design
|
||||||
|
- [API Rules & Filters](api_rules_filters.md) - Security rules
|
||||||
|
- [Schema Templates](../templates/schema_templates.md) - Pre-built relation schemas
|
||||||
|
- [API Records](../api_records.md) - CRUD with relations
|
||||||
9
skills/pocketbase/references/go/go_collections.md
Normal file
9
skills/pocketbase/references/go/go_collections.md
Normal 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.
|
||||||
85
skills/pocketbase/references/go/go_console_commands.md
Normal file
85
skills/pocketbase/references/go/go_console_commands.md
Normal 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.
|
||||||
130
skills/pocketbase/references/go/go_database.md
Normal file
130
skills/pocketbase/references/go/go_database.md
Normal 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.
|
||||||
104
skills/pocketbase/references/go/go_event_hooks.md
Normal file
104
skills/pocketbase/references/go/go_event_hooks.md
Normal 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.
|
||||||
9
skills/pocketbase/references/go/go_filesystem.md
Normal file
9
skills/pocketbase/references/go/go_filesystem.md
Normal 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.
|
||||||
9
skills/pocketbase/references/go/go_jobs_scheduling.md
Normal file
9
skills/pocketbase/references/go/go_jobs_scheduling.md
Normal 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.
|
||||||
9
skills/pocketbase/references/go/go_logging.md
Normal file
9
skills/pocketbase/references/go/go_logging.md
Normal 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.
|
||||||
16
skills/pocketbase/references/go/go_migrations.md
Normal file
16
skills/pocketbase/references/go/go_migrations.md
Normal 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).
|
||||||
9
skills/pocketbase/references/go/go_miscellaneous.md
Normal file
9
skills/pocketbase/references/go/go_miscellaneous.md
Normal 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.
|
||||||
787
skills/pocketbase/references/go/go_overview.md
Normal file
787
skills/pocketbase/references/go/go_overview.md
Normal 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
|
||||||
9
skills/pocketbase/references/go/go_realtime.md
Normal file
9
skills/pocketbase/references/go/go_realtime.md
Normal 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.
|
||||||
9
skills/pocketbase/references/go/go_record_proxy.md
Normal file
9
skills/pocketbase/references/go/go_record_proxy.md
Normal 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.
|
||||||
9
skills/pocketbase/references/go/go_records.md
Normal file
9
skills/pocketbase/references/go/go_records.md
Normal 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.
|
||||||
@@ -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.
|
||||||
61
skills/pocketbase/references/go/go_routing.md
Normal file
61
skills/pocketbase/references/go/go_routing.md
Normal 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.
|
||||||
9
skills/pocketbase/references/go/go_sdk.md
Normal file
9
skills/pocketbase/references/go/go_sdk.md
Normal 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.
|
||||||
9
skills/pocketbase/references/go/go_sending_emails.md
Normal file
9
skills/pocketbase/references/go/go_sending_emails.md
Normal 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.
|
||||||
9
skills/pocketbase/references/go/go_testing.md
Normal file
9
skills/pocketbase/references/go/go_testing.md
Normal 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.
|
||||||
1045
skills/pocketbase/references/schema_templates.md
Normal file
1045
skills/pocketbase/references/schema_templates.md
Normal file
File diff suppressed because it is too large
Load Diff
9
skills/pocketbase/references/sdk/dart_sdk.md
Normal file
9
skills/pocketbase/references/sdk/dart_sdk.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Dart SDK
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This file covers dart_sdk in PocketBase Go extensions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note:** This is a placeholder file.
|
||||||
203
skills/pocketbase/references/sdk/js_sdk.md
Normal file
203
skills/pocketbase/references/sdk/js_sdk.md
Normal 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.
|
||||||
467
skills/pocketbase/references/security_rules.md
Normal file
467
skills/pocketbase/references/security_rules.md
Normal 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
|
||||||
219
skills/pocketbase/scripts/export_data.py
Normal file
219
skills/pocketbase/scripts/export_data.py
Normal 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()
|
||||||
339
skills/pocketbase/scripts/import_data.py
Normal file
339
skills/pocketbase/scripts/import_data.py
Normal 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()
|
||||||
60
skills/pocketbase/scripts/setup_pocketbase.sh
Executable file
60
skills/pocketbase/scripts/setup_pocketbase.sh
Executable 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
|
||||||
Reference in New Issue
Block a user