PocketBase - Backend w Jednym Pliku
Czym jest PocketBase?
PocketBase to open-source backend aplikacji skompresowany do jednego wykonywalnego pliku o wielkości około 15MB. Zawiera w sobie wszystko, czego potrzebujesz do uruchomienia backendu: bazę danych SQLite, system autentykacji, realtime subscriptions, file storage oraz wbudowany admin panel. Jest napisany w Go, co zapewnia wysoką wydajność i łatwość deploymentu.
PocketBase został stworzony przez Gani Georgieva jako alternatywa dla rozbudowanych rozwiązań BaaS (Backend as a Service) jak Firebase czy Supabase. Jego filozofia opiera się na prostocie - zamiast konfigurować dziesiątki serwisów, uruchamiasz jeden plik i masz działający backend w sekundy.
Dlaczego PocketBase?
Kluczowe zalety
- Jeden plik wykonywalny - Cały backend w jednym pliku ~15MB
- Zero konfiguracji - Uruchom i działa od razu
- SQLite - Sprawdzona, szybka baza danych dla małych/średnich projektów
- Wbudowany Admin UI - Zarządzaj danymi bez dodatkowych narzędzi
- Realtime - Subskrypcje WebSocket dla live updates
- Self-hosted - Pełna kontrola nad danymi
- Darmowy i open-source - MIT License
PocketBase vs inne rozwiązania BaaS
| Cecha | PocketBase | Firebase | Supabase | Appwrite |
|---|---|---|---|---|
| Self-hosted | Tak | Nie | Tak | Tak |
| Cena | Darmowy | Freemium | Freemium | Darmowy |
| Baza danych | SQLite | Firestore | PostgreSQL | MariaDB |
| Rozmiar deploymentu | ~15MB | Cloud | ~2GB | ~1GB |
| Konfiguracja | Zero | Średnia | Duża | Średnia |
| Realtime | Tak | Tak | Tak | Tak |
| Admin Panel | Tak | Tak | Tak | Tak |
| SDK JS | Tak | Tak | Tak | Tak |
| Extendable w Go | Tak | Nie | Nie | Nie |
| OAuth providers | Tak | Tak | Tak | Tak |
| File Storage | Tak | Tak | Tak | Tak |
Kiedy wybrać PocketBase?
Idealne zastosowania:
- MVP i prototypy
- Projekty side-project
- Małe i średnie aplikacje (do ~100K użytkowników)
- Aplikacje wymagające self-hostingu
- Projekty z ograniczonym budżetem
- Szybkie proof-of-concept
Nie zalecane dla:
- Aplikacji enterprise z milionami użytkowników
- Projektów wymagających zaawansowanych zapytań SQL
- Aplikacji z bardzo złożonymi relacjami danych
- Systemów wymagających horizontal scaling
Instalacja i szybki start
Download i uruchomienie
# Linux AMD64
wget https://github.com/pocketbase/pocketbase/releases/download/v0.22.0/pocketbase_0.22.0_linux_amd64.zip
unzip pocketbase_0.22.0_linux_amd64.zip
# macOS ARM64 (Apple Silicon)
wget https://github.com/pocketbase/pocketbase/releases/download/v0.22.0/pocketbase_0.22.0_darwin_arm64.zip
unzip pocketbase_0.22.0_darwin_arm64.zip
# macOS AMD64 (Intel)
wget https://github.com/pocketbase/pocketbase/releases/download/v0.22.0/pocketbase_0.22.0_darwin_amd64.zip
unzip pocketbase_0.22.0_darwin_amd64.zip
# Windows
# Pobierz pocketbase_0.22.0_windows_amd64.zip z GitHub Releases
# Uruchomienie serwera
./pocketbase serve
# Z custom portem
./pocketbase serve --http="127.0.0.1:8080"
# Z publicznym dostępem
./pocketbase serve --http="0.0.0.0:8090"Struktura katalogów
Po uruchomieniu PocketBase tworzy katalog pb_data/:
my-app/
├── pocketbase # Plik wykonywalny
├── pb_data/
│ ├── data.db # Baza danych SQLite
│ ├── storage/ # Przechowywane pliki
│ └── backups/ # Automatyczne backupy
└── pb_migrations/ # Opcjonalne migracjePierwsze uruchomienie Admin UI
Po starcie serwera przejdź do:
http://127.0.0.1:8090/_/Przy pierwszym uruchomieniu zostaniesz poproszony o utworzenie konta administratora. Podaj email i hasło - będą to Twoje dane logowania do panelu administracyjnego.
Admin Panel
Tworzenie kolekcji (tabel)
Admin Panel pozwala na wizualne tworzenie i zarządzanie kolekcjami:
- Przejdź do Settings > Collections
- Kliknij "New collection"
- Wybierz typ kolekcji:
- Base - Standardowa kolekcja danych
- Auth - Kolekcja użytkowników z autentykacją
- View - Widok (read-only) oparty na innych kolekcjach
Definiowanie schematu
Dostępne typy pól:
| Typ | Opis | Przykład |
|---|---|---|
| Text | Krótki tekst | nazwa, tytuł |
| Editor | Rich text (HTML) | treść artykułu |
| Number | Liczba (int/float) | cena, ilość |
| Bool | Prawda/fałsz | opublikowany, aktywny |
| Walidowany email | email użytkownika | |
| URL | Walidowany URL | link do strony |
| Date | Data i czas | data_utworzenia |
| Select | Jeden z listy | status, kategoria |
| File | Przesłany plik | avatar, załącznik |
| Relation | Relacja do innej kolekcji | autor -> users |
| JSON | Dowolny obiekt JSON | metadata, settings |
Przykładowa struktura dla bloga
Kolekcja: posts
├── title (Text, required)
├── slug (Text, unique)
├── content (Editor)
├── excerpt (Text)
├── cover (File, single)
├── published (Bool, default: false)
├── author (Relation -> users)
├── categories (Relation -> categories, multiple)
├── tags (JSON)
└── created, updated (auto)
Kolekcja: categories
├── name (Text, required)
├── slug (Text, unique)
└── description (Text)
Kolekcja: comments (Base)
├── content (Text, required)
├── post (Relation -> posts)
├── author (Relation -> users)
└── approved (Bool, default: false)API Rules (Reguły dostępu)
Dla każdej kolekcji możesz zdefiniować reguły dostępu:
List/Search Rule: published = true || @request.auth.id != ""
View Rule: published = true || @request.auth.id = author.id
Create Rule: @request.auth.id != ""
Update Rule: @request.auth.id = author.id
Delete Rule: @request.auth.id = author.idSkładnia reguł:
# Sprawdzenie czy użytkownik zalogowany
@request.auth.id != ""
# Sprawdzenie pola rekordu
published = true
# Sprawdzenie właściciela
@request.auth.id = author.id
# Sprawdzenie roli użytkownika (custom field w users)
@request.auth.role = "admin"
# Łączenie warunków
(published = true) || (@request.auth.id = author.id)
# Sprawdzenie relacji
@request.auth.id ?= members.id
# Dostęp do danych z request body
@request.data.status = "draft"JavaScript/TypeScript SDK
Instalacja
# npm
npm install pocketbase
# yarn
yarn add pocketbase
# pnpm
pnpm add pocketbasePodstawowa konfiguracja
import PocketBase from 'pocketbase'
// Inicjalizacja klienta
const pb = new PocketBase('http://127.0.0.1:8090')
// Opcjonalnie: automatyczne odświeżanie tokenu
pb.autoCancellation(false)
// Eksport dla użycia w całej aplikacji
export default pbKonfiguracja z Next.js
// lib/pocketbase.ts
import PocketBase from 'pocketbase'
// Singleton pattern dla server-side
let pb: PocketBase | null = null
export function getPocketBase(): PocketBase {
if (!pb) {
pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL)
}
return pb
}
// Hook dla client-side z auth state
// hooks/usePocketBase.ts
import PocketBase from 'pocketbase'
import { useEffect, useState } from 'react'
export function usePocketBase() {
const [pb] = useState(() => new PocketBase(
process.env.NEXT_PUBLIC_POCKETBASE_URL
))
const [user, setUser] = useState(pb.authStore.model)
useEffect(() => {
// Nasłuchuj zmian auth state
const unsubscribe = pb.authStore.onChange((token, model) => {
setUser(model)
})
return () => unsubscribe()
}, [pb])
return { pb, user }
}Typy TypeScript
// types/pocketbase.ts
import PocketBase, { RecordModel } from 'pocketbase'
// Definicja typów dla kolekcji
export interface User extends RecordModel {
email: string
name: string
avatar?: string
role: 'user' | 'admin'
}
export interface Post extends RecordModel {
title: string
slug: string
content: string
excerpt?: string
cover?: string
published: boolean
author: string // ID użytkownika
categories: string[] // IDs kategorii
tags?: string[]
// Expanded relations
expand?: {
author?: User
categories?: Category[]
}
}
export interface Category extends RecordModel {
name: string
slug: string
description?: string
}
// Typed PocketBase client
export type TypedPocketBase = PocketBase & {
collection(name: 'users'): ReturnType<PocketBase['collection']>
collection(name: 'posts'): ReturnType<PocketBase['collection']>
collection(name: 'categories'): ReturnType<PocketBase['collection']>
}Autentykacja
Email/Password
import PocketBase from 'pocketbase'
const pb = new PocketBase('http://127.0.0.1:8090')
// Rejestracja nowego użytkownika
async function register(email: string, password: string, name: string) {
try {
const user = await pb.collection('users').create({
email,
password,
passwordConfirm: password,
name,
emailVisibility: true
})
// Opcjonalnie: wyślij email weryfikacyjny
await pb.collection('users').requestVerification(email)
return user
} catch (error) {
console.error('Registration failed:', error)
throw error
}
}
// Logowanie
async function login(email: string, password: string) {
try {
const authData = await pb.collection('users').authWithPassword(
email,
password
)
console.log('Logged in user:', authData.record)
console.log('Auth token:', authData.token)
// Token jest automatycznie przechowywany w pb.authStore
return authData
} catch (error) {
console.error('Login failed:', error)
throw error
}
}
// Wylogowanie
function logout() {
pb.authStore.clear()
}
// Sprawdzenie czy zalogowany
function isLoggedIn(): boolean {
return pb.authStore.isValid
}
// Pobranie aktualnego użytkownika
function getCurrentUser() {
return pb.authStore.model
}
// Odświeżenie tokenu
async function refreshAuth() {
if (pb.authStore.isValid) {
await pb.collection('users').authRefresh()
}
}OAuth2 Providers
PocketBase obsługuje wielu dostawców OAuth2:
// Dostępni providerzy (konfiguracja w Admin Panel > Settings > Auth providers)
// Google, Facebook, GitHub, GitLab, Discord, Twitter, Microsoft, Spotify, etc.
// Lista dostępnych metod OAuth
async function getOAuthProviders() {
const methods = await pb.collection('users').listAuthMethods()
console.log('Available providers:', methods.authProviders)
return methods.authProviders
}
// Logowanie przez OAuth (np. Google)
async function loginWithGoogle() {
try {
const authData = await pb.collection('users').authWithOAuth2({
provider: 'google',
// Opcjonalnie: custom scopes
scopes: ['email', 'profile']
})
console.log('OAuth user:', authData.record)
return authData
} catch (error) {
console.error('OAuth login failed:', error)
throw error
}
}
// OAuth z przekierowaniem (dla SSR)
async function loginWithOAuth2Redirect(provider: string) {
const authMethods = await pb.collection('users').listAuthMethods()
const providerConfig = authMethods.authProviders.find(p => p.name === provider)
if (!providerConfig) {
throw new Error(`Provider ${provider} not found`)
}
// Przekieruj użytkownika do providera
const redirectUrl = `${window.location.origin}/auth/callback`
// Zapisz state do weryfikacji
localStorage.setItem('oauth_state', providerConfig.state)
localStorage.setItem('oauth_verifier', providerConfig.codeVerifier)
// Przekierowanie
window.location.href = providerConfig.authUrl + redirectUrl
}
// Callback handler
async function handleOAuthCallback(code: string, state: string) {
const savedState = localStorage.getItem('oauth_state')
const codeVerifier = localStorage.getItem('oauth_verifier')
if (state !== savedState) {
throw new Error('Invalid OAuth state')
}
const authData = await pb.collection('users').authWithOAuth2Code(
'google',
code,
codeVerifier!,
`${window.location.origin}/auth/callback`
)
localStorage.removeItem('oauth_state')
localStorage.removeItem('oauth_verifier')
return authData
}Reset hasła
// Wysłanie emaila z linkiem do resetu
async function requestPasswordReset(email: string) {
await pb.collection('users').requestPasswordReset(email)
}
// Potwierdzenie resetu (z tokenu w URL)
async function confirmPasswordReset(
token: string,
newPassword: string
) {
await pb.collection('users').confirmPasswordReset(
token,
newPassword,
newPassword
)
}
// Zmiana hasła zalogowanego użytkownika
async function changePassword(
oldPassword: string,
newPassword: string
) {
const userId = pb.authStore.model?.id
if (!userId) throw new Error('Not logged in')
await pb.collection('users').update(userId, {
oldPassword,
password: newPassword,
passwordConfirm: newPassword
})
}Weryfikacja email
// Wysłanie emaila weryfikacyjnego
async function requestEmailVerification(email: string) {
await pb.collection('users').requestVerification(email)
}
// Potwierdzenie weryfikacji (z tokenu w URL)
async function confirmEmailVerification(token: string) {
await pb.collection('users').confirmVerification(token)
}Operacje CRUD
Create (tworzenie)
// Tworzenie pojedynczego rekordu
async function createPost(data: {
title: string
content: string
author: string
}) {
const post = await pb.collection('posts').create({
title: data.title,
content: data.content,
author: data.author,
slug: slugify(data.title),
published: false
})
return post
}
// Tworzenie z plikiem
async function createPostWithCover(
data: {
title: string
content: string
author: string
},
coverFile: File
) {
const formData = new FormData()
formData.append('title', data.title)
formData.append('content', data.content)
formData.append('author', data.author)
formData.append('cover', coverFile)
const post = await pb.collection('posts').create(formData)
return post
}Read (odczyt)
// Pobierz pojedynczy rekord po ID
async function getPostById(id: string) {
const post = await pb.collection('posts').getOne(id, {
expand: 'author,categories'
})
return post
}
// Pobierz pierwszy pasujący rekord
async function getPostBySlug(slug: string) {
const post = await pb.collection('posts').getFirstListItem(
`slug = "${slug}"`,
{ expand: 'author,categories' }
)
return post
}
// Lista z paginacją
async function getPosts(page = 1, perPage = 20) {
const posts = await pb.collection('posts').getList(page, perPage, {
filter: 'published = true',
sort: '-created',
expand: 'author,categories'
})
return {
items: posts.items,
page: posts.page,
perPage: posts.perPage,
totalItems: posts.totalItems,
totalPages: posts.totalPages
}
}
// Pobierz wszystkie rekordy (uwaga na performance!)
async function getAllPosts() {
const posts = await pb.collection('posts').getFullList({
filter: 'published = true',
sort: '-created',
expand: 'author'
})
return posts
}
// Pobierz tylko ID dla statycznych ścieżek
async function getAllPostSlugs() {
const posts = await pb.collection('posts').getFullList({
fields: 'slug'
})
return posts.map(p => p.slug)
}Update (aktualizacja)
// Aktualizacja całego rekordu
async function updatePost(id: string, data: Partial<Post>) {
const post = await pb.collection('posts').update(id, data)
return post
}
// Aktualizacja z nowym plikiem (zastąpi stary)
async function updatePostCover(id: string, coverFile: File) {
const formData = new FormData()
formData.append('cover', coverFile)
const post = await pb.collection('posts').update(id, formData)
return post
}
// Usunięcie pliku (ustaw na null)
async function removePostCover(id: string) {
const post = await pb.collection('posts').update(id, {
cover: null
})
return post
}Delete (usuwanie)
// Usunięcie pojedynczego rekordu
async function deletePost(id: string) {
await pb.collection('posts').delete(id)
}
// Usunięcie wielu rekordów (batch)
async function deleteMultiplePosts(ids: string[]) {
await Promise.all(
ids.map(id => pb.collection('posts').delete(id))
)
}Filtry i sortowanie
Składnia filtrów
// Podstawowe porównania
await pb.collection('posts').getList(1, 20, {
filter: 'published = true'
})
// Porównanie liczb
await pb.collection('products').getList(1, 20, {
filter: 'price >= 100 && price <= 500'
})
// Wyszukiwanie tekstowe (LIKE)
await pb.collection('posts').getList(1, 20, {
filter: 'title ~ "JavaScript"'
})
// Wyszukiwanie case-insensitive
await pb.collection('posts').getList(1, 20, {
filter: 'title ~ "%react%"'
})
// Sprawdzenie czy pole nie jest puste
await pb.collection('posts').getList(1, 20, {
filter: 'cover != ""'
})
// Sprawdzenie daty
await pb.collection('posts').getList(1, 20, {
filter: 'created >= "2024-01-01 00:00:00"'
})
// Sprawdzenie obecności w tablicy (dla relacji many)
await pb.collection('posts').getList(1, 20, {
filter: 'categories ?~ "abc123"' // zawiera kategorię o ID
})
// Kombinacja filtrów
await pb.collection('posts').getList(1, 20, {
filter: `
published = true &&
(title ~ "${searchTerm}" || content ~ "${searchTerm}") &&
author = "${authorId}"
`
})Sortowanie
// Sortowanie rosnąco
await pb.collection('posts').getList(1, 20, {
sort: 'created'
})
// Sortowanie malejąco (prefix -)
await pb.collection('posts').getList(1, 20, {
sort: '-created'
})
// Wielokrotne sortowanie
await pb.collection('posts').getList(1, 20, {
sort: '-published,created' // najpierw published (desc), potem created (asc)
})
// Sortowanie po relacji
await pb.collection('posts').getList(1, 20, {
sort: 'author.name'
})
// Losowe sortowanie
await pb.collection('posts').getList(1, 20, {
sort: '@random'
})Wybór pól i expand
// Tylko wybrane pola (oszczędność bandwidth)
await pb.collection('posts').getList(1, 20, {
fields: 'id,title,slug,created'
})
// Expand relacji (dodaj dane z powiązanych kolekcji)
await pb.collection('posts').getList(1, 20, {
expand: 'author,categories'
})
// Dostęp do expanded data
const posts = await pb.collection('posts').getList(1, 20, {
expand: 'author'
})
posts.items.forEach(post => {
console.log('Author name:', post.expand?.author?.name)
})
// Nested expand (relacja w relacji)
await pb.collection('comments').getList(1, 20, {
expand: 'post.author' // komentarz -> post -> autor
})
// Indirect expand (back-relations)
await pb.collection('users').getList(1, 20, {
expand: 'posts_via_author' // posty gdzie user jest autorem
})Realtime Subscriptions
Subskrypcja kolekcji
// Subskrybuj wszystkie zmiany w kolekcji
pb.collection('posts').subscribe('*', (e) => {
console.log('Action:', e.action) // create, update, delete
console.log('Record:', e.record)
switch (e.action) {
case 'create':
addPostToUI(e.record)
break
case 'update':
updatePostInUI(e.record)
break
case 'delete':
removePostFromUI(e.record.id)
break
}
})
// Subskrybuj konkretny rekord
pb.collection('posts').subscribe('RECORD_ID', (e) => {
console.log('Post updated:', e.record)
})
// Subskrypcja z filtrem (client-side)
pb.collection('posts').subscribe('*', (e) => {
if (e.record.published) {
handlePublishedPost(e.record)
}
})Zarządzanie subskrypcjami
// Anuluj subskrypcję dla konkretnej kolekcji
pb.collection('posts').unsubscribe('*')
pb.collection('posts').unsubscribe('RECORD_ID')
// Anuluj wszystkie subskrypcje
pb.realtime.unsubscribe()
// React Hook dla realtime
function useRealtimePosts() {
const [posts, setPosts] = useState<Post[]>([])
const pb = usePocketBase()
useEffect(() => {
// Pobierz początkowe dane
pb.collection('posts').getFullList({
filter: 'published = true',
sort: '-created'
}).then(setPosts)
// Subskrybuj zmiany
pb.collection('posts').subscribe('*', (e) => {
setPosts(current => {
switch (e.action) {
case 'create':
if (e.record.published) {
return [e.record as Post, ...current]
}
return current
case 'update':
return current.map(p =>
p.id === e.record.id ? e.record as Post : p
)
case 'delete':
return current.filter(p => p.id !== e.record.id)
default:
return current
}
})
})
// Cleanup
return () => {
pb.collection('posts').unsubscribe('*')
}
}, [pb])
return posts
}Realtime dla autoryzowanych użytkowników
// Subskrypcje działają tylko dla rekordów,
// do których użytkownik ma dostęp (zgodnie z API rules)
// Przykład: prywatne wiadomości
pb.collection('messages').subscribe('*', (e) => {
// Otrzymasz tylko wiadomości, gdzie:
// sender = @request.auth.id || receiver = @request.auth.id
console.log('New message:', e.record)
})File Storage
Upload plików
// Upload pojedynczego pliku
async function uploadAvatar(userId: string, file: File) {
const formData = new FormData()
formData.append('avatar', file)
const user = await pb.collection('users').update(userId, formData)
return user
}
// Upload wielu plików
async function uploadGalleryImages(postId: string, files: FileList) {
const formData = new FormData()
// Dla pola typu File z opcją "multiple"
for (let i = 0; i < files.length; i++) {
formData.append('gallery', files[i])
}
const post = await pb.collection('posts').update(postId, formData)
return post
}
// Upload z progress tracking (fetch API)
async function uploadWithProgress(
collectionName: string,
recordId: string,
fieldName: string,
file: File,
onProgress: (percent: number) => void
) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
const formData = new FormData()
formData.append(fieldName, file)
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
onProgress(Math.round((e.loaded / e.total) * 100))
}
})
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(xhr.statusText))
}
})
xhr.addEventListener('error', () => reject(new Error('Upload failed')))
xhr.open('PATCH', `${pb.baseUrl}/api/collections/${collectionName}/records/${recordId}`)
xhr.setRequestHeader('Authorization', pb.authStore.token)
xhr.send(formData)
})
}Pobieranie URL plików
// Pobierz URL do pliku
const avatarUrl = pb.files.getUrl(user, user.avatar)
// Z thumbnailem (dla obrazów)
const thumbUrl = pb.files.getUrl(user, user.avatar, {
thumb: '100x100'
})
// Dostępne opcje thumb:
// - '100x100' - resize do dokładnie 100x100
// - '100x0' - resize zachowując proporcje (szerokość 100)
// - '0x100' - resize zachowując proporcje (wysokość 100)
// - '100x100t' - crop do środka
// - '100x100b' - crop od dołu
// - '100x100f' - fit (zmieść w 100x100 zachowując proporcje)
// Dla wielu plików (pole z multiple)
const galleryUrls = post.gallery.map(filename =>
pb.files.getUrl(post, filename)
)
// Helper dla Next.js Image
function getPocketBaseImageUrl(
record: RecordModel,
filename: string,
thumb?: string
): string {
if (!filename) return '/placeholder.png'
const url = pb.files.getUrl(record, filename, { thumb })
return url
}
// Użycie w komponencie
<Image
src={getPocketBaseImageUrl(user, user.avatar, '200x200')}
alt={user.name}
width={200}
height={200}
/>Usuwanie plików
// Usuń konkretny plik (ustaw na null lub pusty string)
await pb.collection('users').update(userId, {
avatar: null
})
// Dla pola z wieloma plikami - usuń jeden
await pb.collection('posts').update(postId, {
'gallery-': 'filename-to-remove.jpg' // prefix - oznacza usunięcie
})
// Dodaj nowy plik do istniejącej kolekcji
const formData = new FormData()
formData.append('gallery+', newFile) // prefix + oznacza dodanie
await pb.collection('posts').update(postId, formData)Rozszerzanie PocketBase w Go
Custom backend
// main.go
package main
import (
"log"
"net/http"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/apis"
)
func main() {
app := pocketbase.New()
// Custom route - publiczny endpoint
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.GET("/api/hello", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Hello from PocketBase!",
})
})
return nil
})
// Custom route - chroniony (wymaga auth)
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.GET("/api/protected", func(c echo.Context) error {
authRecord, _ := c.Get(apis.ContextAuthRecordKey).(*models.Record)
if authRecord == nil {
return apis.NewForbiddenError("Unauthorized", nil)
}
return c.JSON(http.StatusOK, map[string]any{
"user": authRecord.Email(),
"message": "Welcome!",
})
}, apis.RequireRecordAuth())
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}Hooki na eventy
package main
import (
"log"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
)
func main() {
app := pocketbase.New()
// Hook przed utworzeniem rekordu
app.OnRecordBeforeCreateRequest("posts").Add(func(e *core.RecordCreateEvent) error {
// Automatycznie ustaw autora na zalogowanego użytkownika
if authRecord := e.HttpContext.Get(apis.ContextAuthRecordKey); authRecord != nil {
e.Record.Set("author", authRecord.(*models.Record).Id)
}
// Generuj slug z tytułu
title := e.Record.GetString("title")
slug := slugify(title)
e.Record.Set("slug", slug)
return nil
})
// Hook po utworzeniu rekordu
app.OnRecordAfterCreateRequest("posts").Add(func(e *core.RecordCreateEvent) error {
// Wyślij notyfikację
log.Printf("New post created: %s", e.Record.GetString("title"))
// Wyślij email do adminów
// sendEmailToAdmins(e.Record)
return nil
})
// Hook przed aktualizacją
app.OnRecordBeforeUpdateRequest("posts").Add(func(e *core.RecordUpdateEvent) error {
// Sprawdź czy użytkownik może edytować
authRecord := e.HttpContext.Get(apis.ContextAuthRecordKey).(*models.Record)
if e.Record.GetString("author") != authRecord.Id {
// Sprawdź czy admin
if authRecord.GetString("role") != "admin" {
return apis.NewForbiddenError("You can only edit your own posts", nil)
}
}
return nil
})
// Hook na uwierzytelnienie
app.OnRecordAuthRequest("users").Add(func(e *core.RecordAuthEvent) error {
log.Printf("User logged in: %s", e.Record.Email())
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}Scheduled tasks (cron)
package main
import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/tools/cron"
)
func main() {
app := pocketbase.New()
// Scheduler uruchamiany co godzinę
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
scheduler := cron.New()
// Co godzinę - cleanup starych sesji
scheduler.MustAdd("cleanup", "0 * * * *", func() {
log.Println("Running cleanup...")
// Implementacja cleanup
})
// Codziennie o północy - backup
scheduler.MustAdd("backup", "0 0 * * *", func() {
log.Println("Running backup...")
// app.CreateBackup("backup_" + time.Now().Format("2006-01-02"))
})
// Co 5 minut - sprawdź zaplanowane posty
scheduler.MustAdd("publish", "*/5 * * * *", func() {
publishScheduledPosts(app)
})
scheduler.Start()
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
func publishScheduledPosts(app *pocketbase.PocketBase) {
records, err := app.Dao().FindRecordsByFilter(
"posts",
"published = false && scheduled_at <= {:now}",
"-scheduled_at",
100,
0,
dbx.Params{"now": time.Now().UTC().Format(time.RFC3339)},
)
if err != nil {
log.Printf("Error finding scheduled posts: %v", err)
return
}
for _, record := range records {
record.Set("published", true)
if err := app.Dao().SaveRecord(record); err != nil {
log.Printf("Error publishing post %s: %v", record.Id, err)
}
}
}Deployment
Docker
# Dockerfile
FROM alpine:latest
ARG PB_VERSION=0.22.0
RUN apk add --no-cache \
unzip \
ca-certificates
# Download PocketBase
ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip /tmp/pb.zip
RUN unzip /tmp/pb.zip -d /pb/
# Create data directory
RUN mkdir -p /pb/pb_data
EXPOSE 8090
# Start PocketBase
CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8090"]# docker-compose.yml
version: '3.8'
services:
pocketbase:
build: .
container_name: pocketbase
restart: unless-stopped
ports:
- "8090:8090"
volumes:
- pocketbase_data:/pb/pb_data
- ./pb_migrations:/pb/pb_migrations
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8090/api/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
pocketbase_data:Railway
# railway.toml
[build]
builder = "dockerfile"
[deploy]
startCommand = "/pb/pocketbase serve --http=0.0.0.0:$PORT"
healthcheckPath = "/api/health"
healthcheckTimeout = 100
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10Fly.io
# fly.toml
app = "my-pocketbase-app"
primary_region = "waw"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 8090
force_https = true
auto_stop_machines = false
auto_start_machines = true
min_machines_running = 1
[mounts]
source = "pb_data"
destination = "/pb/pb_data"
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 256# Deploy na Fly.io
fly launch
fly volumes create pb_data --region waw --size 1
fly deployVPS (systemd)
# /etc/systemd/system/pocketbase.service
[Unit]
Description=PocketBase
After=network.target
[Service]
Type=simple
User=pocketbase
Group=pocketbase
LimitNOFILE=4096
Restart=always
RestartSec=5s
WorkingDirectory=/home/pocketbase
ExecStart=/home/pocketbase/pocketbase serve --http="0.0.0.0:8090"
[Install]
WantedBy=multi-user.target# Instalacja
sudo useradd -r -s /bin/false pocketbase
sudo mkdir -p /home/pocketbase
sudo chown pocketbase:pocketbase /home/pocketbase
# Skopiuj pocketbase do /home/pocketbase/
# Uruchom
sudo systemctl enable pocketbase
sudo systemctl start pocketbase
sudo systemctl status pocketbaseNginx Reverse Proxy
# /etc/nginx/sites-available/pocketbase
server {
listen 80;
server_name api.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# Max upload size
client_max_body_size 50M;
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;
# WebSocket support
proxy_read_timeout 86400;
}
}Backup i migracje
Automatyczne backupy
// Konfiguracja backupów w Go
package main
import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/tools/cron"
)
func main() {
app := pocketbase.New()
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
scheduler := cron.New()
// Backup codziennie o 3:00
scheduler.MustAdd("backup", "0 3 * * *", func() {
name := fmt.Sprintf("backup_%s.zip", time.Now().Format("2006-01-02"))
if err := app.CreateBackup(name); err != nil {
log.Printf("Backup failed: %v", err)
return
}
log.Printf("Backup created: %s", name)
// Opcjonalnie: upload do S3
// uploadToS3(name)
})
scheduler.Start()
return nil
})
app.Start()
}Ręczne backupy
# Backup przez Admin UI
# Settings > Backups > Create backup
# Backup przez command line
./pocketbase --dir=/path/to/pb_data backup
# Kopia katalogów
cp -r pb_data pb_data_backup_$(date +%Y%m%d)
# Restore
./pocketbase --dir=/path/to/pb_data restore /path/to/backup.zipMigracje
// pb_migrations/1234567890_create_posts.go
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
collection := &models.Collection{}
collection.Name = "posts"
collection.Type = models.CollectionTypeBase
collection.Schema = schema.NewSchema(
&schema.SchemaField{
Name: "title",
Type: schema.FieldTypeText,
Required: true,
},
&schema.SchemaField{
Name: "content",
Type: schema.FieldTypeEditor,
},
&schema.SchemaField{
Name: "published",
Type: schema.FieldTypeBool,
},
)
return daos.New(db).SaveCollection(collection)
}, func(db dbx.Builder) error {
// Rollback
collection, err := daos.New(db).FindCollectionByNameOrId("posts")
if err != nil {
return err
}
return daos.New(db).DeleteCollection(collection)
})
}# Generowanie migracji ze zmian w Admin UI
./pocketbase migrate collections
# Zastosowanie migracji
./pocketbase migrate up
# Rollback
./pocketbase migrate downIntegracja z frameworkami
Next.js App Router
// app/posts/page.tsx
import { getPocketBase } from '@/lib/pocketbase'
export const revalidate = 60 // ISR - revalidate co 60 sekund
export default async function PostsPage() {
const pb = getPocketBase()
const posts = await pb.collection('posts').getList(1, 10, {
filter: 'published = true',
sort: '-created',
expand: 'author'
})
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.items.map(post => (
<li key={post.id}>
<a href={`/posts/${post.slug}`}>{post.title}</a>
<span>by {post.expand?.author?.name}</span>
</li>
))}
</ul>
</div>
)
}// app/posts/[slug]/page.tsx
import { getPocketBase } from '@/lib/pocketbase'
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
const pb = getPocketBase()
const posts = await pb.collection('posts').getFullList({
fields: 'slug'
})
return posts.map(post => ({
slug: post.slug
}))
}
export default async function PostPage({
params
}: {
params: { slug: string }
}) {
const pb = getPocketBase()
try {
const post = await pb.collection('posts').getFirstListItem(
`slug = "${params.slug}" && published = true`,
{ expand: 'author,categories' }
)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
} catch {
notFound()
}
}SvelteKit
// src/lib/pocketbase.ts
import PocketBase from 'pocketbase'
import { writable } from 'svelte/store'
export const pb = new PocketBase(import.meta.env.VITE_POCKETBASE_URL)
export const currentUser = writable(pb.authStore.model)
pb.authStore.onChange((auth) => {
currentUser.set(pb.authStore.model)
})<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
import { pb } from '$lib/pocketbase'
import { onMount } from 'svelte'
let posts = []
onMount(async () => {
posts = await pb.collection('posts').getFullList({
filter: 'published = true',
sort: '-created'
})
})
</script>
<h1>Posts</h1>
{#each posts as post}
<article>
<h2><a href="/posts/{post.slug}">{post.title}</a></h2>
</article>
{/each}React Native / Expo
// lib/pocketbase.ts
import PocketBase from 'pocketbase'
import AsyncStorage from '@react-native-async-storage/async-storage'
// Custom auth store dla React Native
const pb = new PocketBase('https://api.example.com')
// Persist auth w AsyncStorage
pb.authStore.onChange(async () => {
try {
if (pb.authStore.isValid) {
await AsyncStorage.setItem('pb_auth', JSON.stringify({
token: pb.authStore.token,
model: pb.authStore.model
}))
} else {
await AsyncStorage.removeItem('pb_auth')
}
} catch (e) {
console.error('Error saving auth state:', e)
}
})
// Restore auth przy starcie aplikacji
export async function initPocketBase() {
try {
const authData = await AsyncStorage.getItem('pb_auth')
if (authData) {
const { token, model } = JSON.parse(authData)
pb.authStore.save(token, model)
}
} catch (e) {
console.error('Error restoring auth state:', e)
}
return pb
}
export default pbBest practices
Struktura projektu
my-app/
├── pocketbase/
│ ├── pocketbase # Binary
│ ├── pb_data/ # Data directory
│ ├── pb_migrations/ # Migrations
│ └── main.go # Custom backend (opcjonalnie)
├── frontend/
│ ├── src/
│ │ ├── lib/
│ │ │ └── pocketbase.ts
│ │ ├── types/
│ │ │ └── pocketbase.ts
│ │ └── hooks/
│ │ └── usePocketBase.ts
│ └── ...
└── docker-compose.ymlBezpieczeństwo
// 1. Walidacja danych wejściowych
function validatePostData(data: unknown): data is CreatePostInput {
if (typeof data !== 'object' || data === null) return false
const { title, content } = data as Record<string, unknown>
return (
typeof title === 'string' &&
title.length > 0 &&
title.length <= 200 &&
typeof content === 'string'
)
}
// 2. Sanityzacja HTML (dla pól Editor)
import DOMPurify from 'dompurify'
function sanitizeContent(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'h2', 'h3'],
ALLOWED_ATTR: ['href', 'target', 'rel']
})
}
// 3. Ochrona przed SQL injection (już wbudowane w PocketBase)
// Używaj zawsze parametryzowanych zapytań
const posts = await pb.collection('posts').getList(1, 20, {
filter: `author = "${sanitizedUserId}"` // PocketBase escapuje automatycznie
})
// 4. Rate limiting (w custom Go backend)
// Implementuj w middlewareObsługa błędów
import PocketBase, { ClientResponseError } from 'pocketbase'
async function safePocketBaseCall<T>(
operation: () => Promise<T>
): Promise<{ data: T | null; error: string | null }> {
try {
const data = await operation()
return { data, error: null }
} catch (err) {
if (err instanceof ClientResponseError) {
// Błąd z PocketBase API
console.error('PocketBase error:', err.status, err.message)
switch (err.status) {
case 400:
return { data: null, error: 'Invalid request data' }
case 401:
return { data: null, error: 'Please login to continue' }
case 403:
return { data: null, error: 'You don\'t have permission' }
case 404:
return { data: null, error: 'Resource not found' }
default:
return { data: null, error: 'Something went wrong' }
}
}
// Inny błąd
console.error('Unknown error:', err)
return { data: null, error: 'An unexpected error occurred' }
}
}
// Użycie
const { data: posts, error } = await safePocketBaseCall(() =>
pb.collection('posts').getList(1, 20)
)
if (error) {
showToast(error)
} else {
displayPosts(posts)
}Optymalizacja wydajności
// 1. Pobieraj tylko potrzebne pola
const posts = await pb.collection('posts').getList(1, 20, {
fields: 'id,title,slug,excerpt,created' // Bez content
})
// 2. Używaj expand zamiast wielu zapytań
const post = await pb.collection('posts').getOne(id, {
expand: 'author,categories' // 1 zapytanie zamiast 3
})
// 3. Cachuj na client-side
const CACHE_TTL = 5 * 60 * 1000 // 5 minut
const cache = new Map<string, { data: any; timestamp: number }>()
async function getCachedPosts() {
const cacheKey = 'posts_list'
const cached = cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data
}
const posts = await pb.collection('posts').getList(1, 20)
cache.set(cacheKey, { data: posts, timestamp: Date.now() })
return posts
}
// 4. Lazy loading dla dużych list
async function loadMorePosts(page: number) {
const posts = await pb.collection('posts').getList(page, 20, {
filter: 'published = true',
sort: '-created'
})
return {
items: posts.items,
hasMore: posts.page < posts.totalPages
}
}FAQ - Najczęściej zadawane pytania
Czy PocketBase nadaje się do produkcji?
Tak, PocketBase jest używany w produkcji przez wiele aplikacji. Ograniczenia:
- SQLite ma limity dla bardzo dużego ruchu (>1000 req/s write)
- Brak horizontal scaling (single instance)
- Odpowiedni dla małych/średnich aplikacji (do ~100K użytkowników)
Jak wykonać backup PocketBase?
# 1. Przez Admin UI: Settings > Backups
# 2. Przez CLI: ./pocketbase backup
# 3. Ręcznie: cp -r pb_data/ backup/Czy mogę używać PostgreSQL zamiast SQLite?
Nie, PocketBase używa wyłącznie SQLite. Jeśli potrzebujesz PostgreSQL, rozważ Supabase lub własny backend z Prisma.
Jak zaimportować dane do PocketBase?
// Import przez SDK
async function importData(data: any[]) {
for (const item of data) {
await pb.collection('posts').create(item)
}
}
// Lub przez Admin API
// Settings > Collections > ImportJak skonfigurować CORS?
PocketBase ma wbudowaną obsługę CORS. Domyślnie akceptuje wszystkie origins. Możesz to zmienić w custom backend:
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://example.com"},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
}))
return nil
})Jak wysyłać emaile z PocketBase?
Skonfiguruj SMTP w Admin Panel > Settings > Mail settings:
SMTP Server: smtp.gmail.com
Port: 587
Username: your-email@gmail.com
Password: app-specific-passwordJak debugować zapytania?
// Włącz debug mode
pb.beforeSend = function (url, options) {
console.log('Request:', url, options)
return { url, options }
}
pb.afterSend = function (response, data) {
console.log('Response:', response.status, data)
return data
}Podsumowanie
PocketBase to rewolucyjne podejście do backendów aplikacji. Oferuje:
- Prostotę - Jeden plik zamiast dziesiątków serwisów
- Wydajność - SQLite + Go = szybki backend
- Kompletność - Auth, realtime, files, admin panel
- Elastyczność - Rozszerzalny w Go
- Kontrolę - Self-hosted, Twoje dane
Dla projektów MVP, side-projects i małych/średnich aplikacji PocketBase jest idealnym wyborem. Uruchamiasz jeden plik i masz działający backend w sekundy - bez konfiguracji, bez kosztów infrastruktury, bez vendor lock-in.
PocketBase - backend in a single file
What is PocketBase?
PocketBase is an open-source application backend compressed into a single executable file roughly 15MB in size. It contains everything you need to run a backend: an SQLite database, an authentication system, realtime subscriptions, file storage, and a built-in admin panel. It is written in Go, which ensures high performance and straightforward deployment.
PocketBase was created by Gani Georgieva as an alternative to sprawling BaaS (Backend as a Service) solutions like Firebase or Supabase. Its philosophy is rooted in simplicity -- instead of configuring dozens of services, you launch a single file and have a working backend in seconds.
Why PocketBase?
Key advantages
- Single executable file - The entire backend in one ~15MB file
- Zero configuration - Run it and it works right away
- SQLite - A proven, fast database for small and medium projects
- Built-in Admin UI - Manage data without additional tools
- Realtime - WebSocket subscriptions for live updates
- Self-hosted - Full control over your data
- Free and open-source - MIT License
PocketBase vs other BaaS solutions
| Feature | PocketBase | Firebase | Supabase | Appwrite |
|---|---|---|---|---|
| Self-hosted | Yes | No | Yes | Yes |
| Price | Free | Freemium | Freemium | Free |
| Database | SQLite | Firestore | PostgreSQL | MariaDB |
| Deployment size | ~15MB | Cloud | ~2GB | ~1GB |
| Configuration | Zero | Medium | High | Medium |
| Realtime | Yes | Yes | Yes | Yes |
| Admin Panel | Yes | Yes | Yes | Yes |
| JS SDK | Yes | Yes | Yes | Yes |
| Extendable in Go | Yes | No | No | No |
| OAuth providers | Yes | Yes | Yes | Yes |
| File Storage | Yes | Yes | Yes | Yes |
When to choose PocketBase?
Ideal use cases:
- MVPs and prototypes
- Side projects
- Small and medium applications (up to ~100K users)
- Applications requiring self-hosting
- Projects with a limited budget
- Quick proof-of-concept builds
Not recommended for:
- Enterprise applications with millions of users
- Projects requiring advanced SQL queries
- Applications with very complex data relationships
- Systems requiring horizontal scaling
Installation and quick start
Download and run
# Linux AMD64
wget https://github.com/pocketbase/pocketbase/releases/download/v0.22.0/pocketbase_0.22.0_linux_amd64.zip
unzip pocketbase_0.22.0_linux_amd64.zip
# macOS ARM64 (Apple Silicon)
wget https://github.com/pocketbase/pocketbase/releases/download/v0.22.0/pocketbase_0.22.0_darwin_arm64.zip
unzip pocketbase_0.22.0_darwin_arm64.zip
# macOS AMD64 (Intel)
wget https://github.com/pocketbase/pocketbase/releases/download/v0.22.0/pocketbase_0.22.0_darwin_amd64.zip
unzip pocketbase_0.22.0_darwin_amd64.zip
# Windows
# Download pocketbase_0.22.0_windows_amd64.zip from GitHub Releases
# Start the server
./pocketbase serve
# With a custom port
./pocketbase serve --http="127.0.0.1:8080"
# With public access
./pocketbase serve --http="0.0.0.0:8090"Directory structure
After launching, PocketBase creates a pb_data/ directory:
my-app/
├── pocketbase # Executable file
├── pb_data/
│ ├── data.db # SQLite database
│ ├── storage/ # Stored files
│ └── backups/ # Automatic backups
└── pb_migrations/ # Optional migrationsFirst launch of the Admin UI
After starting the server, navigate to:
http://127.0.0.1:8090/_/On first launch you will be asked to create an administrator account. Enter an email and password -- these will be your credentials for the admin panel.
Admin Panel
Creating collections (tables)
The Admin Panel allows you to visually create and manage collections:
- Go to Settings > Collections
- Click "New collection"
- Choose the collection type:
- Base - Standard data collection
- Auth - User collection with authentication
- View - Read-only view based on other collections
Defining the schema
Available field types:
| Type | Description | Example |
|---|---|---|
| Text | Short text | name, title |
| Editor | Rich text (HTML) | article body |
| Number | Number (int/float) | price, quantity |
| Bool | True/false | published, active |
| Validated email | user email | |
| URL | Validated URL | website link |
| Date | Date and time | created_at |
| Select | One from a list | status, category |
| File | Uploaded file | avatar, attachment |
| Relation | Relation to another collection | author -> users |
| JSON | Any JSON object | metadata, settings |
Example blog structure
Collection: posts
├── title (Text, required)
├── slug (Text, unique)
├── content (Editor)
├── excerpt (Text)
├── cover (File, single)
├── published (Bool, default: false)
├── author (Relation -> users)
├── categories (Relation -> categories, multiple)
├── tags (JSON)
└── created, updated (auto)
Collection: categories
├── name (Text, required)
├── slug (Text, unique)
└── description (Text)
Collection: comments (Base)
├── content (Text, required)
├── post (Relation -> posts)
├── author (Relation -> users)
└── approved (Bool, default: false)API rules (access rules)
For each collection you can define access rules:
List/Search Rule: published = true || @request.auth.id != ""
View Rule: published = true || @request.auth.id = author.id
Create Rule: @request.auth.id != ""
Update Rule: @request.auth.id = author.id
Delete Rule: @request.auth.id = author.idRule syntax:
# Check if the user is logged in
@request.auth.id != ""
# Check a record field
published = true
# Check the owner
@request.auth.id = author.id
# Check the user role (custom field in users)
@request.auth.role = "admin"
# Combining conditions
(published = true) || (@request.auth.id = author.id)
# Check a relation
@request.auth.id ?= members.id
# Access data from the request body
@request.data.status = "draft"JavaScript/TypeScript SDK
Installation
# npm
npm install pocketbase
# yarn
yarn add pocketbase
# pnpm
pnpm add pocketbaseBasic configuration
import PocketBase from 'pocketbase'
const pb = new PocketBase('http://127.0.0.1:8090')
pb.autoCancellation(false)
export default pbConfiguration with Next.js
// lib/pocketbase.ts
import PocketBase from 'pocketbase'
let pb: PocketBase | null = null
export function getPocketBase(): PocketBase {
if (!pb) {
pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL)
}
return pb
}
// hooks/usePocketBase.ts
import PocketBase from 'pocketbase'
import { useEffect, useState } from 'react'
export function usePocketBase() {
const [pb] = useState(() => new PocketBase(
process.env.NEXT_PUBLIC_POCKETBASE_URL
))
const [user, setUser] = useState(pb.authStore.model)
useEffect(() => {
const unsubscribe = pb.authStore.onChange((token, model) => {
setUser(model)
})
return () => unsubscribe()
}, [pb])
return { pb, user }
}TypeScript types
// types/pocketbase.ts
import PocketBase, { RecordModel } from 'pocketbase'
export interface User extends RecordModel {
email: string
name: string
avatar?: string
role: 'user' | 'admin'
}
export interface Post extends RecordModel {
title: string
slug: string
content: string
excerpt?: string
cover?: string
published: boolean
author: string
categories: string[]
tags?: string[]
expand?: {
author?: User
categories?: Category[]
}
}
export interface Category extends RecordModel {
name: string
slug: string
description?: string
}
export type TypedPocketBase = PocketBase & {
collection(name: 'users'): ReturnType<PocketBase['collection']>
collection(name: 'posts'): ReturnType<PocketBase['collection']>
collection(name: 'categories'): ReturnType<PocketBase['collection']>
}Authentication
Email/password
import PocketBase from 'pocketbase'
const pb = new PocketBase('http://127.0.0.1:8090')
async function register(email: string, password: string, name: string) {
try {
const user = await pb.collection('users').create({
email,
password,
passwordConfirm: password,
name,
emailVisibility: true
})
await pb.collection('users').requestVerification(email)
return user
} catch (error) {
console.error('Registration failed:', error)
throw error
}
}
async function login(email: string, password: string) {
try {
const authData = await pb.collection('users').authWithPassword(
email,
password
)
console.log('Logged in user:', authData.record)
console.log('Auth token:', authData.token)
return authData
} catch (error) {
console.error('Login failed:', error)
throw error
}
}
function logout() {
pb.authStore.clear()
}
function isLoggedIn(): boolean {
return pb.authStore.isValid
}
function getCurrentUser() {
return pb.authStore.model
}
async function refreshAuth() {
if (pb.authStore.isValid) {
await pb.collection('users').authRefresh()
}
}OAuth2 providers
PocketBase supports many OAuth2 providers:
// Available providers (configured in Admin Panel > Settings > Auth providers)
// Google, Facebook, GitHub, GitLab, Discord, Twitter, Microsoft, Spotify, etc.
async function getOAuthProviders() {
const methods = await pb.collection('users').listAuthMethods()
console.log('Available providers:', methods.authProviders)
return methods.authProviders
}
async function loginWithGoogle() {
try {
const authData = await pb.collection('users').authWithOAuth2({
provider: 'google',
scopes: ['email', 'profile']
})
console.log('OAuth user:', authData.record)
return authData
} catch (error) {
console.error('OAuth login failed:', error)
throw error
}
}
async function loginWithOAuth2Redirect(provider: string) {
const authMethods = await pb.collection('users').listAuthMethods()
const providerConfig = authMethods.authProviders.find(p => p.name === provider)
if (!providerConfig) {
throw new Error(`Provider ${provider} not found`)
}
const redirectUrl = `${window.location.origin}/auth/callback`
localStorage.setItem('oauth_state', providerConfig.state)
localStorage.setItem('oauth_verifier', providerConfig.codeVerifier)
window.location.href = providerConfig.authUrl + redirectUrl
}
async function handleOAuthCallback(code: string, state: string) {
const savedState = localStorage.getItem('oauth_state')
const codeVerifier = localStorage.getItem('oauth_verifier')
if (state !== savedState) {
throw new Error('Invalid OAuth state')
}
const authData = await pb.collection('users').authWithOAuth2Code(
'google',
code,
codeVerifier!,
`${window.location.origin}/auth/callback`
)
localStorage.removeItem('oauth_state')
localStorage.removeItem('oauth_verifier')
return authData
}Password reset
async function requestPasswordReset(email: string) {
await pb.collection('users').requestPasswordReset(email)
}
async function confirmPasswordReset(
token: string,
newPassword: string
) {
await pb.collection('users').confirmPasswordReset(
token,
newPassword,
newPassword
)
}
async function changePassword(
oldPassword: string,
newPassword: string
) {
const userId = pb.authStore.model?.id
if (!userId) throw new Error('Not logged in')
await pb.collection('users').update(userId, {
oldPassword,
password: newPassword,
passwordConfirm: newPassword
})
}Email verification
async function requestEmailVerification(email: string) {
await pb.collection('users').requestVerification(email)
}
async function confirmEmailVerification(token: string) {
await pb.collection('users').confirmVerification(token)
}CRUD operations
Create
async function createPost(data: {
title: string
content: string
author: string
}) {
const post = await pb.collection('posts').create({
title: data.title,
content: data.content,
author: data.author,
slug: slugify(data.title),
published: false
})
return post
}
async function createPostWithCover(
data: {
title: string
content: string
author: string
},
coverFile: File
) {
const formData = new FormData()
formData.append('title', data.title)
formData.append('content', data.content)
formData.append('author', data.author)
formData.append('cover', coverFile)
const post = await pb.collection('posts').create(formData)
return post
}Read
async function getPostById(id: string) {
const post = await pb.collection('posts').getOne(id, {
expand: 'author,categories'
})
return post
}
async function getPostBySlug(slug: string) {
const post = await pb.collection('posts').getFirstListItem(
`slug = "${slug}"`,
{ expand: 'author,categories' }
)
return post
}
async function getPosts(page = 1, perPage = 20) {
const posts = await pb.collection('posts').getList(page, perPage, {
filter: 'published = true',
sort: '-created',
expand: 'author,categories'
})
return {
items: posts.items,
page: posts.page,
perPage: posts.perPage,
totalItems: posts.totalItems,
totalPages: posts.totalPages
}
}
async function getAllPosts() {
const posts = await pb.collection('posts').getFullList({
filter: 'published = true',
sort: '-created',
expand: 'author'
})
return posts
}
async function getAllPostSlugs() {
const posts = await pb.collection('posts').getFullList({
fields: 'slug'
})
return posts.map(p => p.slug)
}Update
async function updatePost(id: string, data: Partial<Post>) {
const post = await pb.collection('posts').update(id, data)
return post
}
async function updatePostCover(id: string, coverFile: File) {
const formData = new FormData()
formData.append('cover', coverFile)
const post = await pb.collection('posts').update(id, formData)
return post
}
async function removePostCover(id: string) {
const post = await pb.collection('posts').update(id, {
cover: null
})
return post
}Delete
async function deletePost(id: string) {
await pb.collection('posts').delete(id)
}
async function deleteMultiplePosts(ids: string[]) {
await Promise.all(
ids.map(id => pb.collection('posts').delete(id))
)
}Filters and sorting
Filter syntax
await pb.collection('posts').getList(1, 20, {
filter: 'published = true'
})
await pb.collection('products').getList(1, 20, {
filter: 'price >= 100 && price <= 500'
})
await pb.collection('posts').getList(1, 20, {
filter: 'title ~ "JavaScript"'
})
await pb.collection('posts').getList(1, 20, {
filter: 'title ~ "%react%"'
})
await pb.collection('posts').getList(1, 20, {
filter: 'cover != ""'
})
await pb.collection('posts').getList(1, 20, {
filter: 'created >= "2024-01-01 00:00:00"'
})
await pb.collection('posts').getList(1, 20, {
filter: 'categories ?~ "abc123"'
})
await pb.collection('posts').getList(1, 20, {
filter: `
published = true &&
(title ~ "${searchTerm}" || content ~ "${searchTerm}") &&
author = "${authorId}"
`
})Sorting
await pb.collection('posts').getList(1, 20, {
sort: 'created'
})
await pb.collection('posts').getList(1, 20, {
sort: '-created'
})
await pb.collection('posts').getList(1, 20, {
sort: '-published,created'
})
await pb.collection('posts').getList(1, 20, {
sort: 'author.name'
})
await pb.collection('posts').getList(1, 20, {
sort: '@random'
})Field selection and expand
await pb.collection('posts').getList(1, 20, {
fields: 'id,title,slug,created'
})
await pb.collection('posts').getList(1, 20, {
expand: 'author,categories'
})
const posts = await pb.collection('posts').getList(1, 20, {
expand: 'author'
})
posts.items.forEach(post => {
console.log('Author name:', post.expand?.author?.name)
})
await pb.collection('comments').getList(1, 20, {
expand: 'post.author'
})
await pb.collection('users').getList(1, 20, {
expand: 'posts_via_author'
})Realtime subscriptions
Subscribing to a collection
pb.collection('posts').subscribe('*', (e) => {
console.log('Action:', e.action)
console.log('Record:', e.record)
switch (e.action) {
case 'create':
addPostToUI(e.record)
break
case 'update':
updatePostInUI(e.record)
break
case 'delete':
removePostFromUI(e.record.id)
break
}
})
pb.collection('posts').subscribe('RECORD_ID', (e) => {
console.log('Post updated:', e.record)
})
pb.collection('posts').subscribe('*', (e) => {
if (e.record.published) {
handlePublishedPost(e.record)
}
})Managing subscriptions
pb.collection('posts').unsubscribe('*')
pb.collection('posts').unsubscribe('RECORD_ID')
pb.realtime.unsubscribe()
function useRealtimePosts() {
const [posts, setPosts] = useState<Post[]>([])
const pb = usePocketBase()
useEffect(() => {
pb.collection('posts').getFullList({
filter: 'published = true',
sort: '-created'
}).then(setPosts)
pb.collection('posts').subscribe('*', (e) => {
setPosts(current => {
switch (e.action) {
case 'create':
if (e.record.published) {
return [e.record as Post, ...current]
}
return current
case 'update':
return current.map(p =>
p.id === e.record.id ? e.record as Post : p
)
case 'delete':
return current.filter(p => p.id !== e.record.id)
default:
return current
}
})
})
return () => {
pb.collection('posts').unsubscribe('*')
}
}, [pb])
return posts
}Realtime for authenticated users
// Subscriptions only work for records the user has access to
// (according to the API rules)
pb.collection('messages').subscribe('*', (e) => {
// You will only receive messages where:
// sender = @request.auth.id || receiver = @request.auth.id
console.log('New message:', e.record)
})File storage
Uploading files
async function uploadAvatar(userId: string, file: File) {
const formData = new FormData()
formData.append('avatar', file)
const user = await pb.collection('users').update(userId, formData)
return user
}
async function uploadGalleryImages(postId: string, files: FileList) {
const formData = new FormData()
for (let i = 0; i < files.length; i++) {
formData.append('gallery', files[i])
}
const post = await pb.collection('posts').update(postId, formData)
return post
}
async function uploadWithProgress(
collectionName: string,
recordId: string,
fieldName: string,
file: File,
onProgress: (percent: number) => void
) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
const formData = new FormData()
formData.append(fieldName, file)
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
onProgress(Math.round((e.loaded / e.total) * 100))
}
})
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(xhr.statusText))
}
})
xhr.addEventListener('error', () => reject(new Error('Upload failed')))
xhr.open('PATCH', `${pb.baseUrl}/api/collections/${collectionName}/records/${recordId}`)
xhr.setRequestHeader('Authorization', pb.authStore.token)
xhr.send(formData)
})
}Getting file URLs
const avatarUrl = pb.files.getUrl(user, user.avatar)
const thumbUrl = pb.files.getUrl(user, user.avatar, {
thumb: '100x100'
})
// Available thumb options:
// - '100x100' - resize to exactly 100x100
// - '100x0' - resize preserving aspect ratio (width 100)
// - '0x100' - resize preserving aspect ratio (height 100)
// - '100x100t' - crop to center
// - '100x100b' - crop from bottom
// - '100x100f' - fit (fit within 100x100 preserving aspect ratio)
const galleryUrls = post.gallery.map(filename =>
pb.files.getUrl(post, filename)
)
function getPocketBaseImageUrl(
record: RecordModel,
filename: string,
thumb?: string
): string {
if (!filename) return '/placeholder.png'
const url = pb.files.getUrl(record, filename, { thumb })
return url
}
<Image
src={getPocketBaseImageUrl(user, user.avatar, '200x200')}
alt={user.name}
width={200}
height={200}
/>Deleting files
await pb.collection('users').update(userId, {
avatar: null
})
await pb.collection('posts').update(postId, {
'gallery-': 'filename-to-remove.jpg'
})
const formData = new FormData()
formData.append('gallery+', newFile)
await pb.collection('posts').update(postId, formData)Extending PocketBase in Go
Custom backend
// main.go
package main
import (
"log"
"net/http"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/apis"
)
func main() {
app := pocketbase.New()
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.GET("/api/hello", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Hello from PocketBase!",
})
})
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.GET("/api/protected", func(c echo.Context) error {
authRecord, _ := c.Get(apis.ContextAuthRecordKey).(*models.Record)
if authRecord == nil {
return apis.NewForbiddenError("Unauthorized", nil)
}
return c.JSON(http.StatusOK, map[string]any{
"user": authRecord.Email(),
"message": "Welcome!",
})
}, apis.RequireRecordAuth())
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}Event hooks
package main
import (
"log"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
)
func main() {
app := pocketbase.New()
app.OnRecordBeforeCreateRequest("posts").Add(func(e *core.RecordCreateEvent) error {
if authRecord := e.HttpContext.Get(apis.ContextAuthRecordKey); authRecord != nil {
e.Record.Set("author", authRecord.(*models.Record).Id)
}
title := e.Record.GetString("title")
slug := slugify(title)
e.Record.Set("slug", slug)
return nil
})
app.OnRecordAfterCreateRequest("posts").Add(func(e *core.RecordCreateEvent) error {
log.Printf("New post created: %s", e.Record.GetString("title"))
// sendEmailToAdmins(e.Record)
return nil
})
app.OnRecordBeforeUpdateRequest("posts").Add(func(e *core.RecordUpdateEvent) error {
authRecord := e.HttpContext.Get(apis.ContextAuthRecordKey).(*models.Record)
if e.Record.GetString("author") != authRecord.Id {
if authRecord.GetString("role") != "admin" {
return apis.NewForbiddenError("You can only edit your own posts", nil)
}
}
return nil
})
app.OnRecordAuthRequest("users").Add(func(e *core.RecordAuthEvent) error {
log.Printf("User logged in: %s", e.Record.Email())
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}Scheduled tasks (cron)
package main
import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/tools/cron"
)
func main() {
app := pocketbase.New()
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
scheduler := cron.New()
scheduler.MustAdd("cleanup", "0 * * * *", func() {
log.Println("Running cleanup...")
})
scheduler.MustAdd("backup", "0 0 * * *", func() {
log.Println("Running backup...")
// app.CreateBackup("backup_" + time.Now().Format("2006-01-02"))
})
scheduler.MustAdd("publish", "*/5 * * * *", func() {
publishScheduledPosts(app)
})
scheduler.Start()
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
func publishScheduledPosts(app *pocketbase.PocketBase) {
records, err := app.Dao().FindRecordsByFilter(
"posts",
"published = false && scheduled_at <= {:now}",
"-scheduled_at",
100,
0,
dbx.Params{"now": time.Now().UTC().Format(time.RFC3339)},
)
if err != nil {
log.Printf("Error finding scheduled posts: %v", err)
return
}
for _, record := range records {
record.Set("published", true)
if err := app.Dao().SaveRecord(record); err != nil {
log.Printf("Error publishing post %s: %v", record.Id, err)
}
}
}Deployment
Docker
# Dockerfile
FROM alpine:latest
ARG PB_VERSION=0.22.0
RUN apk add --no-cache \
unzip \
ca-certificates
# Download PocketBase
ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip /tmp/pb.zip
RUN unzip /tmp/pb.zip -d /pb/
# Create data directory
RUN mkdir -p /pb/pb_data
EXPOSE 8090
# Start PocketBase
CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8090"]# docker-compose.yml
version: '3.8'
services:
pocketbase:
build: .
container_name: pocketbase
restart: unless-stopped
ports:
- "8090:8090"
volumes:
- pocketbase_data:/pb/pb_data
- ./pb_migrations:/pb/pb_migrations
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8090/api/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
pocketbase_data:Railway
# railway.toml
[build]
builder = "dockerfile"
[deploy]
startCommand = "/pb/pocketbase serve --http=0.0.0.0:$PORT"
healthcheckPath = "/api/health"
healthcheckTimeout = 100
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10Fly.io
# fly.toml
app = "my-pocketbase-app"
primary_region = "waw"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 8090
force_https = true
auto_stop_machines = false
auto_start_machines = true
min_machines_running = 1
[mounts]
source = "pb_data"
destination = "/pb/pb_data"
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 256# Deploy to Fly.io
fly launch
fly volumes create pb_data --region waw --size 1
fly deployVPS (systemd)
# /etc/systemd/system/pocketbase.service
[Unit]
Description=PocketBase
After=network.target
[Service]
Type=simple
User=pocketbase
Group=pocketbase
LimitNOFILE=4096
Restart=always
RestartSec=5s
WorkingDirectory=/home/pocketbase
ExecStart=/home/pocketbase/pocketbase serve --http="0.0.0.0:8090"
[Install]
WantedBy=multi-user.target# Installation
sudo useradd -r -s /bin/false pocketbase
sudo mkdir -p /home/pocketbase
sudo chown pocketbase:pocketbase /home/pocketbase
# Copy pocketbase to /home/pocketbase/
# Start
sudo systemctl enable pocketbase
sudo systemctl start pocketbase
sudo systemctl status pocketbaseNginx reverse proxy
# /etc/nginx/sites-available/pocketbase
server {
listen 80;
server_name api.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# Max upload size
client_max_body_size 50M;
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;
# WebSocket support
proxy_read_timeout 86400;
}
}Backups and migrations
Automatic backups
package main
import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/tools/cron"
)
func main() {
app := pocketbase.New()
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
scheduler := cron.New()
scheduler.MustAdd("backup", "0 3 * * *", func() {
name := fmt.Sprintf("backup_%s.zip", time.Now().Format("2006-01-02"))
if err := app.CreateBackup(name); err != nil {
log.Printf("Backup failed: %v", err)
return
}
log.Printf("Backup created: %s", name)
// uploadToS3(name)
})
scheduler.Start()
return nil
})
app.Start()
}Manual backups
# Backup via Admin UI
# Settings > Backups > Create backup
# Backup via command line
./pocketbase --dir=/path/to/pb_data backup
# Copy directories
cp -r pb_data pb_data_backup_$(date +%Y%m%d)
# Restore
./pocketbase --dir=/path/to/pb_data restore /path/to/backup.zipMigrations
// pb_migrations/1234567890_create_posts.go
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
collection := &models.Collection{}
collection.Name = "posts"
collection.Type = models.CollectionTypeBase
collection.Schema = schema.NewSchema(
&schema.SchemaField{
Name: "title",
Type: schema.FieldTypeText,
Required: true,
},
&schema.SchemaField{
Name: "content",
Type: schema.FieldTypeEditor,
},
&schema.SchemaField{
Name: "published",
Type: schema.FieldTypeBool,
},
)
return daos.New(db).SaveCollection(collection)
}, func(db dbx.Builder) error {
collection, err := daos.New(db).FindCollectionByNameOrId("posts")
if err != nil {
return err
}
return daos.New(db).DeleteCollection(collection)
})
}# Generate migrations from Admin UI changes
./pocketbase migrate collections
# Apply migrations
./pocketbase migrate up
# Rollback
./pocketbase migrate downFramework integrations
Next.js App Router
// app/posts/page.tsx
import { getPocketBase } from '@/lib/pocketbase'
export const revalidate = 60
export default async function PostsPage() {
const pb = getPocketBase()
const posts = await pb.collection('posts').getList(1, 10, {
filter: 'published = true',
sort: '-created',
expand: 'author'
})
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.items.map(post => (
<li key={post.id}>
<a href={`/posts/${post.slug}`}>{post.title}</a>
<span>by {post.expand?.author?.name}</span>
</li>
))}
</ul>
</div>
)
}// app/posts/[slug]/page.tsx
import { getPocketBase } from '@/lib/pocketbase'
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
const pb = getPocketBase()
const posts = await pb.collection('posts').getFullList({
fields: 'slug'
})
return posts.map(post => ({
slug: post.slug
}))
}
export default async function PostPage({
params
}: {
params: { slug: string }
}) {
const pb = getPocketBase()
try {
const post = await pb.collection('posts').getFirstListItem(
`slug = "${params.slug}" && published = true`,
{ expand: 'author,categories' }
)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
} catch {
notFound()
}
}SvelteKit
// src/lib/pocketbase.ts
import PocketBase from 'pocketbase'
import { writable } from 'svelte/store'
export const pb = new PocketBase(import.meta.env.VITE_POCKETBASE_URL)
export const currentUser = writable(pb.authStore.model)
pb.authStore.onChange((auth) => {
currentUser.set(pb.authStore.model)
})<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
import { pb } from '$lib/pocketbase'
import { onMount } from 'svelte'
let posts = []
onMount(async () => {
posts = await pb.collection('posts').getFullList({
filter: 'published = true',
sort: '-created'
})
})
</script>
<h1>Posts</h1>
{#each posts as post}
<article>
<h2><a href="/posts/{post.slug}">{post.title}</a></h2>
</article>
{/each}React Native / Expo
// lib/pocketbase.ts
import PocketBase from 'pocketbase'
import AsyncStorage from '@react-native-async-storage/async-storage'
const pb = new PocketBase('https://api.example.com')
pb.authStore.onChange(async () => {
try {
if (pb.authStore.isValid) {
await AsyncStorage.setItem('pb_auth', JSON.stringify({
token: pb.authStore.token,
model: pb.authStore.model
}))
} else {
await AsyncStorage.removeItem('pb_auth')
}
} catch (e) {
console.error('Error saving auth state:', e)
}
})
export async function initPocketBase() {
try {
const authData = await AsyncStorage.getItem('pb_auth')
if (authData) {
const { token, model } = JSON.parse(authData)
pb.authStore.save(token, model)
}
} catch (e) {
console.error('Error restoring auth state:', e)
}
return pb
}
export default pbBest practices
Project structure
my-app/
├── pocketbase/
│ ├── pocketbase # Binary
│ ├── pb_data/ # Data directory
│ ├── pb_migrations/ # Migrations
│ └── main.go # Custom backend (optional)
├── frontend/
│ ├── src/
│ │ ├── lib/
│ │ │ └── pocketbase.ts
│ │ ├── types/
│ │ │ └── pocketbase.ts
│ │ └── hooks/
│ │ └── usePocketBase.ts
│ └── ...
└── docker-compose.ymlSecurity
function validatePostData(data: unknown): data is CreatePostInput {
if (typeof data !== 'object' || data === null) return false
const { title, content } = data as Record<string, unknown>
return (
typeof title === 'string' &&
title.length > 0 &&
title.length <= 200 &&
typeof content === 'string'
)
}
import DOMPurify from 'dompurify'
function sanitizeContent(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'h2', 'h3'],
ALLOWED_ATTR: ['href', 'target', 'rel']
})
}
const posts = await pb.collection('posts').getList(1, 20, {
filter: `author = "${sanitizedUserId}"`
})Error handling
import PocketBase, { ClientResponseError } from 'pocketbase'
async function safePocketBaseCall<T>(
operation: () => Promise<T>
): Promise<{ data: T | null; error: string | null }> {
try {
const data = await operation()
return { data, error: null }
} catch (err) {
if (err instanceof ClientResponseError) {
console.error('PocketBase error:', err.status, err.message)
switch (err.status) {
case 400:
return { data: null, error: 'Invalid request data' }
case 401:
return { data: null, error: 'Please login to continue' }
case 403:
return { data: null, error: 'You don\'t have permission' }
case 404:
return { data: null, error: 'Resource not found' }
default:
return { data: null, error: 'Something went wrong' }
}
}
console.error('Unknown error:', err)
return { data: null, error: 'An unexpected error occurred' }
}
}
const { data: posts, error } = await safePocketBaseCall(() =>
pb.collection('posts').getList(1, 20)
)
if (error) {
showToast(error)
} else {
displayPosts(posts)
}Performance optimization
const posts = await pb.collection('posts').getList(1, 20, {
fields: 'id,title,slug,excerpt,created'
})
const post = await pb.collection('posts').getOne(id, {
expand: 'author,categories'
})
const CACHE_TTL = 5 * 60 * 1000
const cache = new Map<string, { data: any; timestamp: number }>()
async function getCachedPosts() {
const cacheKey = 'posts_list'
const cached = cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data
}
const posts = await pb.collection('posts').getList(1, 20)
cache.set(cacheKey, { data: posts, timestamp: Date.now() })
return posts
}
async function loadMorePosts(page: number) {
const posts = await pb.collection('posts').getList(page, 20, {
filter: 'published = true',
sort: '-created'
})
return {
items: posts.items,
hasMore: posts.page < posts.totalPages
}
}FAQ - frequently asked questions
Is PocketBase production-ready?
Yes, PocketBase is used in production by many applications. Keep these limitations in mind:
- SQLite has limits for very heavy traffic (>1000 write req/s)
- No horizontal scaling (single instance)
- Suitable for small to medium applications (up to ~100K users)
How do I back up PocketBase?
# 1. Via Admin UI: Settings > Backups
# 2. Via CLI: ./pocketbase backup
# 3. Manually: cp -r pb_data/ backup/Can I use PostgreSQL instead of SQLite?
No, PocketBase uses SQLite exclusively. If you need PostgreSQL, consider Supabase or a custom backend with Prisma.
How do I import data into PocketBase?
async function importData(data: any[]) {
for (const item of data) {
await pb.collection('posts').create(item)
}
}
// Or via the Admin API
// Settings > Collections > ImportHow do I configure CORS?
PocketBase has built-in CORS support. By default it accepts all origins. You can change this in a custom backend:
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://example.com"},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
}))
return nil
})How do I send emails from PocketBase?
Configure SMTP in Admin Panel > Settings > Mail settings:
SMTP Server: smtp.gmail.com
Port: 587
Username: your-email@gmail.com
Password: app-specific-passwordHow do I debug queries?
pb.beforeSend = function (url, options) {
console.log('Request:', url, options)
return { url, options }
}
pb.afterSend = function (response, data) {
console.log('Response:', response.status, data)
return data
}Summary
PocketBase is a revolutionary approach to application backends. It offers:
- Simplicity - A single file instead of dozens of services
- Performance - SQLite + Go = a fast backend
- Completeness - Auth, realtime, files, admin panel
- Flexibility - Extensible in Go
- Control - Self-hosted, your data
For MVP projects, side projects, and small to medium applications, PocketBase is an ideal choice. You launch a single file and have a working backend in seconds -- no configuration, no infrastructure costs, no vendor lock-in.