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.