PocketBase как бэкенд для проекта: архитектура, паттерны и продакшн 2026

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 минут.