PocketBase как бэкенд для проекта: полный гайд для продакшна
PocketBase — это не просто инструмент для MVP. В 2026 году тысячи проектов запущены в продакшне на PocketBase: мобильные приложения, B2B SaaS-инструменты, внутренние дашборды, API для Flutter-приложений. В этой статье разберём не «как установить», а «как строить» — архитектурные паттерны, работу с API Rules, кастомную логику и стратегию бэкапов.
Когда PocketBase — правильный выбор для бэкенда
PocketBase закрывает типичный разрыв между «написать бэкенд с нуля» (долго) и «использовать Firebase» (нет доступа из России, vendor lock-in). Конкретные сценарии:
Мобильное приложение с облачным синхронизацией: Flutter + PocketBase SDK. Auth, хранилище, realtime-subscriptions — всё из коробки. Один разработчик вместо команды.
Indie SaaS-продукт: Инструмент для небольшой команды или узкой аудитории. До 10,000 пользователей PocketBase справляется без проблем на VPS с 2 CPU / 4 GB RAM.
Внутренние инструменты компании: Дашборды, системы управления контентом, тулы для команды. Сложный RBAC не нужен — API Rules достаточно.
Агентства и студии: Один PocketBase-экземпляр на клиента, каждый на отдельном VPS или в отдельном Docker-контейнере. Сниженные DevOps-расходы.
Вайбкодинг-проекты: AI хорошо генерирует код для PocketBase (официальная документация, SDK, много примеров). Прототип за часы.
Архитектура бэкенда на PocketBase
PocketBase даёт три уровня кастомизации:
┌─────────────────────────────────────────────────────────┐
│ Уровень 1: Collections + API Rules (нет кода) │
│ Схема данных, правила доступа, валидация │
├─────────────────────────────────────────────────────────┤
│ Уровень 2: Hooks (JavaScript в pb_hooks/) │
│ Бизнес-логика, email, внешние API, вычисляемые поля │
├─────────────────────────────────────────────────────────┤
│ Уровень 3: Go-расширение (extend as framework) │
│ Кастомные роуты, тяжёлая логика, npm-пакеты │
└─────────────────────────────────────────────────────────┘
Для большинства проектов достаточно уровней 1 и 2. Go нужен только для специфических требований.
Проектирование коллекций
Схема коллекций — самое важное архитектурное решение. Ошибки на этом уровне дорого исправлять.
Пример: SaaS с командами (multi-tenant)
Коллекции:
├── users (auth collection, встроенная)
├── 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 (default: false)
└── tasks
├── title: Text (required)
├── description: Editor
├── project: Relation → projects (required)
├── assignee: Relation → users
├── status: Select [todo, in_progress, done]
├── due_date: Date
└── attachments: File (Max files: 5)
API Rules для multi-tenant
API Rules — сердце безопасности PocketBase. Неправильные правила — утечка данных между тенантами:
# teams: пользователь видит только свои команды
listRule: members.user ?= @request.auth.id
viewRule: members.user ?= @request.auth.id
createRule: @request.auth.id != ""
updateRule: owner = @request.auth.id ||
members_via_team.user ?= @request.auth.id &&
members_via_team.role ?= "admin"
deleteRule: owner = @request.auth.id
# projects: виден всем членам команды
listRule: team.members.user ?= @request.auth.id
viewRule: team.members.user ?= @request.auth.id
createRule: team.members.user ?= @request.auth.id
updateRule: team.members.user ?= @request.auth.id &&
team.members.role != "viewer"
deleteRule: team.owner = @request.auth.id ||
team.members.user ?= @request.auth.id &&
team.members.role = "admin"
# tasks: видны всем в команде проекта
listRule: project.team.members.user ?= @request.auth.id
viewRule: project.team.members.user ?= @request.auth.id
createRule: project.team.members.user ?= @request.auth.id
updateRule: project.team.members.user ?= @request.auth.id
deleteRule: project.team.members.user ?= @request.auth.id &&
project.team.members.role != "viewer"
?= в PocketBase означает «хотя бы один элемент в relation соответствует условию» — это оператор для работы с множественными связями.
JavaScript-хуки: бизнес-логика без Go
JavaScript в pb_hooks/ — мощный инструмент для расширения без сборки Go-бинарника.
Автоматическое создание Team при регистрации
// pb_hooks/on_user_create.pb.js
onRecordAfterCreateSuccess((e) => {
const user = e.record
try {
// Создаём команду для нового пользователя
const team = new Record($app.findCollectionByNameOrId('teams'))
team.set('name', `${user.getString('name') || user.email().split('@')[0]}'s Team`)
team.set('slug', generateSlug(user.email().split('@')[0]))
team.set('plan', 'free')
team.set('owner', user.id)
$app.save(team)
// Добавляем пользователя как admin в эту команду
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)
console.log(`Created team ${team.id} for user ${user.id}`)
} catch (err) {
console.error('Failed to create team for user:', err)
}
}, 'users')
function generateSlug(str) {
return str.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
+ '-' + $security.randomStringWithAlphabet(6, 'abcdefghijklmnopqrstuvwxyz0123456789')
}
Webhook при изменении статуса задачи
// pb_hooks/task_status_webhook.pb.js
onRecordUpdateRequest((e) => {
const oldStatus = e.record.getString('status')
const newStatus = e.requestInfo().body.status
// Отправляем webhook только при смене статуса
if (newStatus && oldStatus !== newStatus) {
const webhookUrl = $app.settings().meta.webhookUrl
if (!webhookUrl) return e.next()
const task = e.record
const project = $app.findRecordById('projects', task.getString('project'))
$http.send({
url: webhookUrl,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'task.status_changed',
taskId: task.id,
taskTitle: task.getString('title'),
projectId: project.id,
projectName: project.getString('name'),
oldStatus,
newStatus,
changedBy: e.requestInfo().auth.id,
timestamp: new Date().toISOString(),
}),
})
}
e.next()
}, 'tasks')
Отправка email при приглашении в команду
// pb_hooks/team_invite.pb.js
routerAdd('POST', '/api/custom/teams/:teamId/invite', (e) => {
const auth = e.requestInfo().auth
if (!auth) {
return e.error(401, 'Unauthorized')
}
const { email, role = 'member' } = e.requestInfo().body
const teamId = e.request.pathValue('teamId')
// Проверяем права (только admin команды)
const team = $app.findRecordById('teams', teamId)
const membership = $app.findFirstRecordByFilter(
'team_members',
`team = "${teamId}" && user = "${auth.id}" && role = "admin"`
)
if (!membership) {
return e.error(403, 'Only team admins can invite members')
}
// Отправляем email
const mailer = $app.newMailClient()
const inviteToken = $security.randomStringWithAlphabet(32, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
mailer.send({
to: [{ address: email }],
subject: `Вас пригласили в команду ${team.getString('name')}`,
html: `
<p>Вас пригласили присоединиться к команде <b>${team.getString('name')}</b>.</p>
<p><a href="https://myapp.com/invite?token=${inviteToken}&team=${teamId}&role=${role}">
Принять приглашение
</a></p>
`,
})
return e.json(200, { message: 'Invitation sent' })
})
Интеграция PocketBase с Next.js
// lib/pocketbase.ts — singleton с persistent auth
import PocketBase from 'pocketbase'
let _pb: PocketBase | null = null
export function getPocketBase(): PocketBase {
if (!_pb) {
_pb = new PocketBase(process.env.NEXT_PUBLIC_PB_URL!)
}
return _pb
}
// Для Server Components — новый экземпляр без auth state
export function getPocketBaseServer(): PocketBase {
const pb = new PocketBase(process.env.PB_URL!)
return pb
}
// app/projects/page.tsx — Server Component
import { getPocketBaseServer } from '@/lib/pocketbase'
import { cookies } from 'next/headers'
export default async function ProjectsPage() {
const pb = getPocketBaseServer()
// Восстанавливаем сессию из cookie
const cookieStore = 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 Actions
'use server'
import { getPocketBaseServer } from '@/lib/pocketbase'
import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'
export async function createProject(formData: FormData) {
const pb = getPocketBaseServer()
// Аутентификация из cookie
const pbAuth = cookies().get('pb_auth')?.value
if (pbAuth) pb.authStore.loadFromCookie(`pb_auth=${pbAuth}`)
if (!pb.authStore.isValid) throw new Error('Not authenticated')
await pb.collection('projects').create({
name: formData.get('name') as string,
team: formData.get('teamId') as string,
})
revalidatePath('/projects')
}
Масштабирование PocketBase
Главное ограничение: SQLite — single-writer. Вертикальное масштабирование (мощнее сервер) работает хорошо:
- 1 CPU / 1 GB RAM → до ~1,000 RPS на чтение, ~200 на запись
- 4 CPU / 8 GB RAM → до ~5,000 RPS на чтение, ~1,000 на запись
- 8 CPU / 16 GB RAM → до ~10,000 RPS на чтение, ~2,000 на запись
Для большинства SaaS до 50,000 пользователей хватит сервера за 2,000-4,000 ₽/мес.
Горизонтальное масштабирование через Litestream + read-реплики:
# docker-compose.yml: PocketBase + Litestream для HA
services:
pocketbase:
image: ghcr.io/muchobello/pocketbase:latest
volumes:
- pb_data:/pb_data
environment:
- LITESTREAM_ACCESS_KEY_ID=${S3_KEY}
- LITESTREAM_SECRET_ACCESS_KEY=${S3_SECRET}
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
force-path-style: true
# Репликация каждые 1 секунду
sync-interval: 1s
Бэкап и восстановление PocketBase
Критический момент: pb_data/ содержит не только базу данных, но и загруженные файлы (pb_data/storage/). Полный бэкап должен включать оба.
# Бэкап через dbsend.ru — полная директория
dbsend backup \
--path=/opt/pocketbase/pb_data \
--destination=s3://bucket/pocketbase \
--schedule="*/30 * * * *" \
--keep=100 \
--compress=gzip
# Только база данных (без файлов — легче)
dbsend backup \
--db=sqlite:///opt/pocketbase/pb_data/data.db \
--destination=s3://bucket/pocketbase-db \
--schedule="*/5 * * * *" \
--keep=500
Восстановление из бэкапа:
# Остановить PocketBase
systemctl stop pocketbase
# Восстановить из последнего бэкапа
dbsend restore pocketbase --output=/opt/pocketbase/pb_data --latest
# Запустить
systemctl start pocketbase
FAQ
Подходит ли PocketBase для мобильных приложений?
Отлично подходит. Официальный Dart SDK для Flutter покрывает все операции: CRUD, аутентификацию, файлы, realtime. JavaScript SDK — для React Native. Realtime-подписки через SSE работают стабильно и потребляют мало ресурсов. Многие Flutter-разработчики используют PocketBase как основной бэкенд.
Сколько пользователей выдерживает PocketBase?
На VPS 2 CPU / 4 GB RAM с WAL-режимом: 10,000-50,000 зарегистрированных пользователей, 1,000-5,000 активных одновременно без проблем. Это подтверждается публичными кейсами в r/pocketbase. При большей нагрузке — апгрейд сервера (вертикальное масштабирование) или переход на Supabase/PostgreSQL.
Как организовать права доступа в PocketBase для SaaS?
Связующая таблица team_members с ролями (admin, member, viewer) + API Rules на уровне коллекций с проверкой team.members.user ?= @request.auth.id. Это стандартный паттерн для multi-tenant SaaS на PocketBase. Детальный пример с кодом — в этой статье выше.
Поддерживает ли PocketBase OAuth2?
Да, из коробки: Google, GitHub, Facebook, Apple, Twitter/X, Discord, GitLab, Spotify, Twitch, Kakao, Patreon. Настройка через Admin UI: Authentication → Providers. Для кастомного OAuth2-провайдера нужно расширение на Go.
Как мигрировать данные из Firebase на PocketBase?
Экспортируйте данные Firebase в JSON через Firebase Admin SDK. Напишите migration-скрипт, который создаёт коллекции и записи через PocketBase Admin API. Файлы (Firebase Storage) скачайте и загрузите через PocketBase API. Авторизацию придётся перенести через email-приглашения (пароли Firebase не экспортируются). Процесс трудоёмкий, но выполнимый.
Где хостить PocketBase в России?
Timeweb Cloud VPS: от 399 ₽/мес (1 CPU, 1 GB RAM) — для dev и MVP. Selectel: от 600 ₽/мес — надёжный выбор для продакшна. Яндекс Cloud: Compute Cloud VM, чуть дороже, но с managed-базами рядом. На любом хосте ставится через systemd-сервис или Docker Compose, займёт 15-30 минут.