PocketBase подходит не только для прототипов: на нём работают мобильные приложения, небольшие B2B-сервисы и внутренние панели. В этой статье — не установка (она описана в материале что такое PocketBase), а то, как строить на нём бэкенд: схема коллекций, правила доступа, хуки, интеграция с Next.js и бэкапы.
Когда PocketBase — правильный выбор
PocketBase закрывает разрыв между «написать бэкенд с нуля» и «взять Firebase» (с которым у российских команд сложности с оплатой и доступом, плюс привязка к вендору). Типичные сценарии:
Мобильное приложение с синхронизацией. Flutter + официальный Dart SDK: авторизация, файлы, realtime из коробки.
Небольшой SaaS. Инструмент для узкой аудитории, который живёт на одном VPS. Предел определяется нагрузкой на запись, а не числом зарегистрированных пользователей, поэтому меряйте на своём сценарии.
Внутренние инструменты. Дашборды, CMS, служебные панели, где сложный RBAC не нужен.
Агентства. Отдельный экземпляр PocketBase на клиента — в своём контейнере или на своём VPS.
Проекты с AI-ассистентом. У PocketBase подробная документация и простой SDK, поэтому ассистенты генерируют для него рабочий код. Если хотите подключить агента к самому PocketBase, посмотрите обзор MCP-серверов для PocketBase.
Архитектура бэкенда на PocketBase
PocketBase даёт три уровня кастомизации:
┌─────────────────────────────────────────────────────────┐
│ Уровень 1: Collections + API Rules (без кода) │
│ Схема данных, правила доступа, валидация полей │
├─────────────────────────────────────────────────────────┤
│ Уровень 2: Хуки (JavaScript в pb_hooks/) │
│ Бизнес-логика, письма, внешние API, cron │
├─────────────────────────────────────────────────────────┤
│ Уровень 3: Go (PocketBase как библиотека) │
│ Свои маршруты, тяжёлая логика, Go-пакеты │
└─────────────────────────────────────────────────────────┘
Большинству проектов хватает первых двух уровней.
Проектирование коллекций
Схема коллекций — главное архитектурное решение: ошибки здесь дорого исправлять, когда данные уже есть.
Пример: SaaS с командами (multi-tenant)
Коллекции:
├── users (auth-коллекция, встроенная)
├── teams
│ ├── name: Text (required)
│ ├── slug: Text (unique)
│ ├── plan: Select [free, pro, enterprise]
│ └── owner: Relation → users
├── team_members (связующая коллекция)
│ ├── team: Relation → teams (required)
│ ├── user: Relation → users (required)
│ └── role: Select [admin, member, viewer]
├── projects
│ ├── name: Text (required)
│ ├── team: Relation → teams (required)
│ ├── settings: JSON
│ └── archived: Bool
└── tasks
├── title: Text (required)
├── description: Editor
├── project: Relation → projects (required)
├── assignee: Relation → users
├── status: Select [todo, in_progress, done]
├── due_date: DateTime
└── attachments: File (до 5 файлов)
API Rules для multi-tenant
API Rules — основа безопасности PocketBase. Ошибка в правиле означает утечку данных между командами. У teams нет поля со списком участников, поэтому членство проверяем через обратную связь team_members_via_team или через @collection.team_members:
# teams: пользователь видит только свои команды
listRule: team_members_via_team.user ?= @request.auth.id
viewRule: team_members_via_team.user ?= @request.auth.id
createRule: @request.auth.id != ""
deleteRule: owner = @request.auth.id
# projects: видны всем участникам команды
listRule: team.team_members_via_team.user ?= @request.auth.id
viewRule: team.team_members_via_team.user ?= @request.auth.id
# projects: менять могут участники с ролью не ниже member
updateRule: @collection.team_members.team ?= team &&
@collection.team_members.user ?= @request.auth.id &&
@collection.team_members.role ?!= "viewer"
?= означает «хотя бы одна связанная запись удовлетворяет условию». Самое опасное место — проверки ролей: условия на user и role должны относиться к одной и той же записи членства, иначе viewer одной команды может получить права admin через другую запись. Прежде чем выкатывать правила, заведите тестовых пользователей с разными ролями в двух командах и проверьте каждую операцию запросами к API. Если правило получается слишком сложным, перенесите проверку роли в хук. Синтаксис и типовые ошибки — в статье API Rules в PocketBase.
JavaScript-хуки: бизнес-логика без Go
Команда при регистрации пользователя
// pb_hooks/on_user_create.pb.js
onRecordAfterCreateSuccess((e) => {
e.next()
const user = e.record
try {
const teams = $app.findCollectionByNameOrId('teams')
const team = new Record(teams)
const base = user.getString('name') || user.email().split('@')[0]
team.set('name', `Команда ${base}`)
team.set('slug', base.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '-' +
$security.randomStringWithAlphabet(6, 'abcdefghijklmnopqrstuvwxyz0123456789'))
team.set('plan', 'free')
team.set('owner', user.id)
$app.save(team)
const membership = new Record($app.findCollectionByNameOrId('team_members'))
membership.set('team', team.id)
membership.set('user', user.id)
membership.set('role', 'admin')
$app.save(membership)
} catch (err) {
console.error('Не удалось создать команду для пользователя', user.id, err)
}
}, 'users')
Webhook при смене статуса задачи
// pb_hooks/task_status_webhook.pb.js
onRecordUpdateRequest((e) => {
const oldStatus = e.record.original().getString('status')
e.next() // сначала сохраняем запись
const newStatus = e.record.getString('status')
const webhookUrl = $os.getenv('TASK_WEBHOOK_URL')
if (!webhookUrl || oldStatus === newStatus) return
$http.send({
url: webhookUrl,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'task.status_changed',
taskId: e.record.id,
projectId: e.record.getString('project'),
oldStatus,
newStatus,
changedBy: e.auth ? e.auth.id : null,
}),
timeout: 10,
})
}, 'tasks')
Приглашение в команду по email
// pb_hooks/team_invite.pb.js
routerAdd('POST', '/api/custom/teams/{teamId}/invite', (e) => {
if (!e.auth) {
throw new UnauthorizedError('Требуется авторизация')
}
const body = e.requestInfo().body
const email = body.email
const role = body.role || 'member'
const teamId = e.request.pathValue('teamId')
// Только admin команды; параметры передаём через плейсхолдеры, а не конкатенацией
try {
$app.findFirstRecordByFilter(
'team_members',
'team = {:team} && user = {:user} && role = "admin"',
{ team: teamId, user: e.auth.id }
)
} catch (_) {
throw new ForbiddenError('Приглашать могут только администраторы команды')
}
const team = $app.findRecordById('teams', teamId)
const token = $security.randomString(32)
// Сохраните token в отдельной коллекции invites, чтобы потом проверить его
const message = new MailerMessage({
from: {
address: $app.settings().meta.senderAddress,
name: $app.settings().meta.senderName,
},
to: [{ address: email }],
subject: `Приглашение в команду ${team.getString('name')}`,
html: `<p><a href="https://myapp.ru/invite?token=${token}">Принять приглашение</a></p>`,
})
$app.newMailClient().send(message)
return e.json(200, { message: 'Приглашение отправлено' })
}, $apis.requireAuth())
Интеграция PocketBase с Next.js
// lib/pocketbase.ts
import PocketBase from 'pocketbase'
// Клиентская сторона: один экземпляр на вкладку
let browserPb: PocketBase | null = null
export function getPocketBase(): PocketBase {
if (!browserPb) browserPb = new PocketBase(process.env.NEXT_PUBLIC_PB_URL!)
return browserPb
}
// Серверная сторона: новый экземпляр на каждый запрос,
// чтобы сессии разных пользователей не смешивались
export function getPocketBaseServer(): PocketBase {
return new PocketBase(process.env.PB_URL!)
}
// app/projects/page.tsx — Server Component
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPocketBaseServer } from '@/lib/pocketbase'
export default async function ProjectsPage() {
const pb = getPocketBaseServer()
const cookieStore = await cookies()
const pbAuth = cookieStore.get('pb_auth')?.value
if (pbAuth) pb.authStore.loadFromCookie(`pb_auth=${pbAuth}`)
if (!pb.authStore.isValid) redirect('/login')
const projects = await pb.collection('projects').getList(1, 50, {
filter: 'archived = false',
sort: '-created',
expand: 'team',
})
return (
<div>
{projects.items.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
)
}
// app/actions/projects.ts — Server Action
'use server'
import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'
import { getPocketBaseServer } from '@/lib/pocketbase'
export async function createProject(formData: FormData) {
const pb = getPocketBaseServer()
const pbAuth = (await cookies()).get('pb_auth')?.value
if (pbAuth) pb.authStore.loadFromCookie(`pb_auth=${pbAuth}`)
if (!pb.authStore.isValid) throw new Error('Not authenticated')
// API Rules на сервере PocketBase всё равно проверят права
await pb.collection('projects').create({
name: formData.get('name') as string,
team: formData.get('teamId') as string,
})
revalidatePath('/projects')
}
Масштабирование PocketBase
Главное ограничение — SQLite допускает одного писателя в каждый момент. Чтение масштабируется хорошо, запись упирается в диск и длину транзакций. Практические выводы:
- PocketBase масштабируется вертикально: быстрый NVMe-диск и больше CPU дают больше, чем любые ухищрения;
- держите транзакции в хуках короткими, не делайте внешние HTTP-запросы внутри транзакции;
- реальный предел измеряйте нагрузочным тестом на своих запросах — универсальных цифр «N пользователей на сервер» нет;
- если нужен второй сервер на запись — это сигнал переходить на PostgreSQL.
Litestream: непрерывная репликация в S3
Litestream — это не горизонтальное масштабирование, а непрерывная выгрузка WAL в S3: он позволяет восстановить базу почти на любой момент времени.
# docker-compose.yml: PocketBase + Litestream
services:
pocketbase:
build: .
volumes:
- pb_data:/pb_data
litestream:
image: litestream/litestream
volumes:
- pb_data:/pb_data
- ./litestream.yml:/etc/litestream.yml
command: replicate
environment:
- LITESTREAM_ACCESS_KEY_ID=${S3_KEY}
- LITESTREAM_SECRET_ACCESS_KEY=${S3_SECRET}
volumes:
pb_data:
# litestream.yml
dbs:
- path: /pb_data/data.db
replicas:
- type: s3
bucket: my-pocketbase-backup
path: pocketbase/data.db
endpoint: https://storage.yandexcloud.net
region: ru-central1
force-path-style: true
sync-interval: 1s
Litestream копирует только базу, не файлы из storage/. Чем он отличается от периодических версий и когда что выбирать — в сравнении Litestream vs cron-скрипт vs dbsend.
Бэкап и восстановление PocketBase
pb_data/ содержит не только базу, но и загруженные файлы (pb_data/storage/). Полный бэкап должен покрывать оба. Не архивируйте data.db через tar на работающем сервере: в WAL-режиме такая копия может оказаться неконсистентной.
# База: CLI dbsend сам делает консистентную копию SQLite перед загрузкой
dbsend backup /opt/pocketbase/pb_data/data.db \
-d "$DBSEND_DATABASE_ID" -e sqlite -l "nightly-$(date +%F)"
# Файлы: отдельный архив как generic_file (хранится как есть, проверяется контрольная сумма)
tar -czf /tmp/pb_storage.tar.gz -C /opt/pocketbase/pb_data storage
dbsend backup /tmp/pb_storage.tar.gz -d "$DBSEND_FILES_ID" -e generic_file
Восстановление базы:
# Найти нужную версию
dbsend log "$DBSEND_DATABASE_ID"
# Остановить PocketBase и восстановить файл
systemctl stop pocketbase
dbsend restore "$SNAPSHOT_ID" /opt/pocketbase/pb_data/data.db --yes
chown www-data:www-data /opt/pocketbase/pb_data/data.db
systemctl start pocketbase
dbsend restore скачивает файл во временный, сверяет SHA-256, выполняет PRAGMA integrity_check, удаляет устаревшие -wal/-shm рядом с целевым файлом и атомарно подменяет базу. Полная схема с cron, встроенными бэкапами PocketBase и проверкой восстановления — в статье бэкап PocketBase.
Частые вопросы
Подходит ли PocketBase для мобильных приложений?
Да. Официальный Dart SDK для Flutter покрывает CRUD, авторизацию, файлы и realtime. Для React Native подходит JavaScript SDK. Realtime работает через SSE.
Сколько пользователей выдерживает PocketBase?
Зависит не от числа аккаунтов, а от характера нагрузки: доли записей, длины транзакций, размера файлов и диска. Приложения с преобладанием чтения на одном сервере держат заметную нагрузку, но конкретный предел измерьте нагрузочным тестом на своих сценариях. Если упираетесь в запись — сначала оптимизируйте хуки и индексы, затем увеличивайте сервер, а дальше думайте о PostgreSQL.
Как организовать права доступа в PocketBase для SaaS?
Связующая коллекция team_members с ролями плюс API Rules, которые проверяют членство через team_members_via_team или @collection.team_members. Обязательно протестируйте правила на пользователях с разными ролями — особенно комбинации проверок пользователя и роли.
Поддерживает ли PocketBase OAuth2?
Да: Google, GitHub, Apple, Discord, GitLab и другие провайдеры, а также универсальный OpenID Connect. Настраивается в панели в параметрах auth-коллекции.
Как перенести данные из Firebase в PocketBase?
Экспортируйте данные Firestore через Admin SDK в JSON, создайте коллекции в PocketBase и загрузите записи скриптом через API от имени superuser. Файлы из Firebase Storage скачайте и загрузите через PocketBase API. Хеши паролей Firebase напрямую перенести не получится — пользователям придётся сбросить пароль или войти через OAuth2.
Где хостить PocketBase в России?
На любом VPS у российского провайдера: Timeweb Cloud, Selectel, Yandex Cloud и других. Достаточно systemd-сервиса или Docker Compose. Сравнение провайдеров — в статье облачная инфраструктура: Cloud.ru, Yandex, Selectel.



