A simple Go client for PocketBase that handles the common stuff you need - authentication, fetching records, and working with collections.
- User and superuser authentication
- Create new records in collections
- Update existing records in collections
- File uploads with records (single and multiple files)
- Fetch records from collections (with automatic pagination)
- Query single records by ID
- User impersonation for superusers
- Filtering, sorting, and expanding relations
- No external dependencies - just the Go standard library
- Thread-safe token management
- Proper error handling
go get github.com/0x113/pocketbase-gopackage main
import (
"context"
"fmt"
"log"
"github.com/0x113/pocketbase-go"
)
func main() {
client := pocketbase.NewClient("http://localhost:8090")
// Login
user, err := client.AuthenticateWithPassword(
context.Background(),
"users",
"[email protected]",
"password123",
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Logged in as: %s\n", user["email"])
// Get all posts
posts, err := client.GetAllRecords(context.Background(), "posts")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d posts\n", len(posts))
// Get one post
if len(posts) > 0 {
post, err := client.GetRecord(
context.Background(),
"posts",
fmt.Sprintf("%v", posts[0]["id"]),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Post title: %s\n", post["title"])
}
}client := pocketbase.NewClient("http://localhost:8090")You can pass options to customize the client:
client := pocketbase.NewClient("http://localhost:8090",
pocketbase.WithTimeout(30*time.Second),
pocketbase.WithUserAgent("MyApp/1.0"),
)Available options:
WithHTTPClient(client *http.Client)- Use your own HTTP clientWithTimeout(timeout time.Duration)- Set request timeoutWithUserAgent(userAgent string)- Custom User-Agent header
user, err := client.AuthenticateWithPassword(ctx, "users", "[email protected]", "secret123")
if err != nil {
if apiErr, ok := err.(*pocketbase.APIError); ok {
if apiErr.IsBadRequest() {
fmt.Println("Wrong email or password")
}
}
return err
}
fmt.Printf("Logged in as: %s\n", user["email"])superuser, err := client.AuthenticateAsSuperuser(ctx, "[email protected]", "admin_password")
if err != nil {
log.Fatal("Failed to authenticate as superuser:", err)
}
fmt.Printf("Superuser: %s\n", superuser["email"])Only superusers can impersonate other users. This generates a non-refreshable token for the target user:
// First authenticate as superuser
_, err := client.AuthenticateAsSuperuser(ctx, "[email protected]", "admin_password")
if err != nil {
log.Fatal(err)
}
// Then impersonate a user for 1 hour
result, err := client.Impersonate(ctx, "users", "user_record_id", 3600)
if err != nil {
log.Fatal("Impersonation failed:", err)
}
// Use the impersonation token
impersonatedClient := pocketbase.NewClient("http://localhost:8090")
impersonatedClient.SetToken(result.Token)
// Now make requests as the impersonated user
records, err := impersonatedClient.GetAllRecords(ctx, "user_posts")You can set tokens manually if you have them from somewhere else:
client.SetToken("your-token-here")
token := client.GetToken() // Get current tokenposts, err := client.GetAllRecords(ctx, "posts")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d posts\n", len(posts))The client automatically handles pagination for you. You can also add filters and sorting:
posts, err := client.GetAllRecords(ctx, "posts",
pocketbase.WithFilter("status='published'"),
pocketbase.WithSort("-created"),
pocketbase.WithListExpand("author", "category"),
pocketbase.WithPerPage(50),
)Available options for GetAllRecords:
WithSort(sort string)- Sort records (e.g., "-created", "+title")WithFilter(filter string)- Filter records (e.g., "status='published'")WithListExpand(fields ...string)- Expand relation fieldsWithListFields(fields ...string)- Select specific fields onlyWithPerPage(perPage int)- Records per pageWithPage(page int)- Get specific page only
post, err := client.GetRecord(ctx, "posts", "RECORD_ID_HERE")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Post title: %s\n", post["title"])You can also expand relations and select specific fields:
post, err := client.GetRecord(ctx, "posts", "RECORD_ID_HERE",
pocketbase.WithExpand("author", "comments"),
pocketbase.WithFields("id", "title", "content", "author"),
)// Create a new record in a collection
recordData := pocketbase.Record{
"title": "My New Post",
"content": "This is the content of my new post",
"status": "published",
"tags": []string{"golang", "tutorial"},
}
createdRecord, err := client.CreateRecord(ctx, "posts", recordData)
if err != nil {
if apiErr, ok := err.(*pocketbase.APIError); ok {
if apiErr.IsBadRequest() {
fmt.Println("Validation error:", apiErr.Message)
}
}
log.Fatal(err)
}
fmt.Printf("Created record with ID: %s\n", createdRecord["id"])You can also expand relations and select specific fields when creating:
createdRecord, err := client.CreateRecord(ctx, "posts", recordData,
pocketbase.WithExpand("author", "category"),
pocketbase.WithFields("id", "title", "content", "author"),
)// Update a record by providing only the fields you want to change
updateData := pocketbase.Record{
"title": "Updated Post Title",
"content": "This post has been updated with new content",
"status": "published",
"tags": []string{"golang", "tutorial", "updated"},
}
updatedRecord, err := client.UpdateRecord(ctx, "posts", "RECORD_ID_HERE", updateData)
if err != nil {
if apiErr, ok := err.(*pocketbase.APIError); ok {
if apiErr.IsBadRequest() {
fmt.Println("Validation error:", apiErr.Message)
}
}
log.Fatal(err)
}
fmt.Printf("Updated record: %s\n", updatedRecord["title"])You can also expand relations and select specific fields when updating:
updatedRecord, err := client.UpdateRecord(ctx, "posts", "RECORD_ID_HERE", updateData,
pocketbase.WithExpand("author", "category"),
pocketbase.WithFields("id", "title", "content", "author"),
)The library supports uploading files to PocketBase collections with file fields.
// Open files
file1, err := os.Open("document.pdf")
if err != nil {
log.Fatal(err)
}
defer file1.Close()
file2, err := os.Open("image.jpg")
if err != nil {
log.Fatal(err)
}
defer file2.Close()
// Prepare files for upload
files := []pocketbase.FileData{
{Reader: file1, Filename: "document.pdf"},
{Reader: file2, Filename: "image.jpg"},
}
// Prepare record data
data := pocketbase.Record{
"title": "Important Document",
"description": "This document contains important information",
}
// Create record with files
createdRecord, err := client.CreateRecordWithFiles(ctx, "documents",
pocketbase.WithFormData(data),
pocketbase.WithFileUpload("files", files))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created record with files: %s\n", createdRecord["id"])Replace existing files:
newFile, err := os.Open("new-avatar.jpg")
if err != nil {
log.Fatal(err)
}
defer newFile.Close()
files := []pocketbase.FileData{
{Reader: newFile, Filename: "new-avatar.jpg"},
}
data := pocketbase.Record{
"name": "Updated User",
}
updatedRecord, err := client.UpdateRecordWithFiles(ctx, "users", "RECORD_ID",
pocketbase.WithFormData(data),
pocketbase.WithFileUpload("avatar", files))Append files to existing ones:
newFile, err := os.Open("document3.pdf")
if err != nil {
log.Fatal(err)
}
defer newFile.Close()
files := []pocketbase.FileData{
{Reader: newFile, Filename: "document3.pdf"},
}
updatedRecord, err := client.UpdateRecordWithFiles(ctx, "documents", "RECORD_ID",
pocketbase.WithFileUpload("files", files, pocketbase.WithAppend()))Delete specific files:
updatedRecord, err := client.UpdateRecordWithFiles(ctx, "documents", "RECORD_ID",
pocketbase.WithFileUpload("files", nil,
pocketbase.WithDelete("old-file1.pdf", "old-file2.pdf")))The library provides several helper functions to create FileData:
// From an io.Reader
fileData := pocketbase.CreateFileData(reader, "filename.txt")
// From byte data
content := []byte("Hello, World!")
fileData := pocketbase.CreateFileDataFromBytes(content, "hello.txt")
// From file path (caller must close the file)
fileData, err := pocketbase.CreateFileDataFromFile("path/to/file.pdf")
if err != nil {
log.Fatal(err)
}
// Don't forget to close the file when done
if fileReader, ok := fileData.Reader.(*os.File); ok {
defer fileReader.Close()
}
// Use in upload
createdRecord, err := client.CreateRecordWithFiles(ctx, "documents",
pocketbase.WithFormData(data),
pocketbase.WithFileUpload("file", []pocketbase.FileData{fileData}))You can use expand and fields options with file uploads:
// Upload with expanded relations
createdRecord, err := client.CreateRecordWithFiles(ctx, "documents",
pocketbase.WithFormData(data),
pocketbase.WithFileUpload("files", files),
func(opts *pocketbase.FileUploadOptions) {
opts.Expand = []string{"author", "category"}
opts.Fields = []string{"id", "title", "files", "author"}
})Records are returned as map[string]any, so you can access any field:
fmt.Printf("Title: %s\n", record["title"])
fmt.Printf("Created: %s\n", record["created"])
// Type assertion for specific types
if id, ok := record["id"].(string); ok {
fmt.Printf("Record ID: %s\n", id)
}API errors are returned as *pocketbase.APIError with useful methods:
record, err := client.GetRecord(ctx, "posts", "invalid-id")
if err != nil {
if apiErr, ok := err.(*pocketbase.APIError); ok {
fmt.Printf("API Error: %s (Status: %d)\n", apiErr.Message, apiErr.Status)
if apiErr.IsNotFound() {
fmt.Println("Record not found")
} else if apiErr.IsUnauthorized() {
fmt.Println("Need to login")
}
} else {
fmt.Printf("Network error: %v\n", err)
}
}Available error check methods:
IsNotFound()- 404 errorsIsUnauthorized()- 401 errorsIsForbidden()- 403 errorsIsBadRequest()- 400 errors
import "crypto/tls"
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // Only for development!
},
},
}
client := pocketbase.NewClient("https://your-pb-instance.com",
pocketbase.WithHTTPClient(httpClient),
)records, err := client.GetAllRecords(ctx, "posts",
pocketbase.WithFilter("(status='published' || status='featured') && author.verified=true"),
pocketbase.WithSort("-featured, -created, +title"),
pocketbase.WithListExpand("author", "tags", "category"),
)// Get specific page
page2, err := client.GetAllRecords(ctx, "posts",
pocketbase.WithPage(2),
pocketbase.WithPerPage(20),
)
// Or get everything (default - handles pagination automatically)
allPosts, err := client.GetAllRecords(ctx, "posts")ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
records, err := client.GetAllRecords(ctx, "large_collection")
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Request timed out")
}
}Run the tests locally:
go test ./...
go test -cover ./... # with coverage
go test -v ./... # verboseThe tests use httptest.Server to mock PocketBase responses and cover:
- Authentication (regular users and superusers)
- Record fetching with pagination
- Impersonation functionality
- Error handling
- Query options
- Thread safety
This project uses GitHub Actions for continuous integration:
- Triggers: Runs on every push to
mainbranch and every pull request targetingmain - Go versions: Tests against Go 1.21.x and 1.22.x
- Coverage: Generates and reports test coverage to Codecov
- Quality checks: Includes formatting validation and
go vet
- Trigger: Can be manually triggered via GitHub Actions interface
- Custom Go version: Allows specifying a custom Go version for testing
- Purpose: Useful for testing other branches or specific Go versions
The CI pipeline ensures code quality and compatibility across supported Go versions.
The examples directory contains well-documented code examples that demonstrate different features:
common.go- Shared utilities and client setupauth_example.go- User authenticationcreate_record_example.go- Creating new records in collectionsupdate_record_example.go- Updating existing records in collectionsfile_upload_example.go- Uploading files with recordsfetch_all_example.go- Fetching all records from collectionsfetch_options_example.go- Filtering, sorting, and expanding recordsfetch_single_example.go- Fetching individual recordserror_handling_example.go- Proper error handlingmultiple_collections_example.go- Working with different collectionssuperuser_example.go- Superuser authentication and impersonation
Each example file is self-contained and includes detailed comments explaining the functionality. To learn how to use the library:
- Read the example files - Each file demonstrates a specific aspect of the PocketBase Go client
- Study the comments - Detailed explanations are provided inline
- Understand the patterns - See how to handle authentication, errors, and data fetching
- Adapt to your needs - Use the patterns as templates for your own code
The examples show real-world usage patterns including proper error handling, context management, and best practices for working with PocketBase collections.
- Go 1.21+
- PocketBase 0.20+
- No external dependencies
This covers the basic read and write operations. Future versions might add:
- Deleting records
- Real-time subscriptions
- Admin API
- OAuth2 login
Pull requests welcome! This is a simple library so let's keep it that way.
MIT - see LICENSE file.