PocketBase — это self-hosted Backend-as-a-Service в одном бинарном файле. База данных SQLite, авторизация, файловое хранилище, realtime-подписки и REST API — всё в одном исполняемом файле размером ~25 MB. В паре с Astro это один из лучших стеков для быстрой разработки.
Почему PocketBase + Astro?
- Простота деплоя: один бинарник на VPS, никаких сложных настроек
- Self-hosted: ваши данные на вашем сервере, никаких Firebase/Supabase vendor lock-in
- Встроенная авторизация: email/пароль, OAuth (Google, GitHub, VK), magic link
- Файловое хранилище: загрузка файлов прямо в PocketBase
- Realtime: WebSocket-подписки на изменения коллекций
- Бесплатно: открытый исходный код, MIT лицензия
Установка PocketBase
# Скачать последнюю версию для Linux
wget https://github.com/pocketbase/pocketbase/releases/latest/download/pocketbase_linux_amd64.zip
unzip pocketbase_linux_amd64.zip
# Запустить
./pocketbase serve
# → Admin UI: http://127.0.0.1:8090/_/
# → API: http://127.0.0.1:8090/api/ На первом запуске создайте суперадмина через браузер. Затем создайте нужные коллекции.
Подключение к Astro
npm install pocketbase // src/lib/pocketbase.ts
import PocketBase from 'pocketbase';
// Сервер: для SSR и API-роутов
export function createServerPB() {
return new PocketBase(import.meta.env.PB_URL);
}
// Клиент: для Preact-островов (браузер)
export const pb = new PocketBase(import.meta.env.PUBLIC_PB_URL); // src/env.d.ts
interface ImportMetaEnv {
readonly PB_URL: string; // Серверный URL (внутренний)
readonly PUBLIC_PB_URL: string; // Клиентский URL (публичный)
readonly PB_ADMIN_EMAIL: string; // Для серверных операций
readonly PB_ADMIN_PASSWORD: string;
} CRUD-операции в SSR-страницах
---
// src/pages/posts.astro
export const prerender = false;
import { createServerPB } from '../lib/pocketbase';
const pb = createServerPB();
// Авторизоваться как суперадмин для серверных операций
await pb
.collection('_superusers')
.authWithPassword(import.meta.env.PB_ADMIN_EMAIL, import.meta.env.PB_ADMIN_PASSWORD);
// Получить список постов
const posts = await pb.collection('posts').getList(1, 20, {
sort: '-created',
filter: 'published = true',
expand: 'author', // Развернуть связанную запись автора
});
---
<ul>
{
posts.items.map((post) => (
<li>
<h2>{post.title}</h2>
<p>Автор: {post.expand?.author?.name}</p>
<time>{new Date(post.created).toLocaleDateString('ru-RU')}</time>
</li>
))
}
</ul> API-роуты с PocketBase
// src/pages/api/posts/index.ts
import type { APIRoute } from 'astro';
import { createServerPB } from '../../../lib/pocketbase';
// GET /api/posts — список постов
export const GET: APIRoute = async ({ url }) => {
const pb = createServerPB();
const page = Number(url.searchParams.get('page') ?? 1);
const perPage = Number(url.searchParams.get('perPage') ?? 20);
const posts = await pb.collection('posts').getList(page, perPage, {
sort: '-created',
filter: 'published = true',
});
return new Response(JSON.stringify(posts), {
headers: { 'Content-Type': 'application/json' },
});
};
// POST /api/posts — создать пост (требует авторизации)
export const POST: APIRoute = async ({ request, locals }) => {
if (!locals.user) {
return new Response('Unauthorized', { status: 401 });
}
const pb = createServerPB();
pb.authStore.loadFromCookie(request.headers.get('cookie') ?? '');
const data = await request.json();
const post = await pb.collection('posts').create({
...data,
author: locals.user.id,
published: false, // Черновик
});
return new Response(JSON.stringify(post), { status: 201 });
}; Авторизация пользователей
---
// src/pages/login.astro
export const prerender = false;
import { createServerPB } from '../lib/pocketbase';
let error = '';
if (Astro.request.method === 'POST') {
const data = await Astro.request.formData();
const pb = createServerPB();
try {
await pb
.collection('users')
.authWithPassword(data.get('email') as string, data.get('password') as string);
// Экспортировать сессию в куки
Astro.response.headers.append(
'Set-Cookie',
pb.authStore.exportToCookie({ httpOnly: true, sameSite: 'strict' }),
);
return Astro.redirect('/dashboard');
} catch {
error = 'Неверный email или пароль';
}
}
---
{error && <p class="error">{error}</p>}
<form method="POST">
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Пароль" required />
<button type="submit">Войти</button>
</form> OAuth-авторизация (Google, GitHub, VK)
---
// src/pages/auth/oauth.astro
export const prerender = false;
import { createServerPB } from '../../lib/pocketbase';
const code = Astro.url.searchParams.get('code');
const state = Astro.url.searchParams.get('state');
const provider = Astro.url.searchParams.get('provider') ?? 'google';
if (code && state) {
const pb = createServerPB();
try {
await pb
.collection('users')
.authWithOAuth2Code(
provider,
code,
'',
`${import.meta.env.PUBLIC_SITE_URL}/auth/oauth?provider=${provider}`,
);
Astro.response.headers.append('Set-Cookie', pb.authStore.exportToCookie());
return Astro.redirect('/dashboard');
} catch {
return Astro.redirect('/login?error=oauth');
}
}
--- Файловое хранилище
PocketBase хранит файлы прямо в файловой системе сервера:
// src/components/FileUpload.tsx (Preact-остров)
import { pb } from '../lib/pocketbase';
export default function FileUpload({ recordId }: { recordId: string }) {
async function handleUpload(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
const formData = new FormData();
formData.append('avatar', file);
// Обновить запись пользователя с файлом
const updated = await pb.collection('users').update(recordId, formData);
// Получить URL файла
const avatarUrl = pb.files.getUrl(updated, updated.avatar, { thumb: '100x100' });
console.log('Avatar:', avatarUrl);
}
return (
<label>
Загрузить фото
<input type="file" accept="image/*" onChange={handleUpload} />
</label>
);
} Realtime-подписки
// src/components/LiveFeed.tsx (Preact-остров)
import { useEffect, useState } from 'preact/hooks';
import { pb } from '../lib/pocketbase';
interface Message {
id: string;
text: string;
created: string;
}
export default function LiveFeed() {
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
// Подписаться на изменения коллекции
pb.collection('messages').subscribe('*', (e) => {
if (e.action === 'create') {
setMessages((prev) => [e.record as Message, ...prev]);
}
});
// Загрузить начальные данные
pb.collection('messages')
.getList(1, 20, { sort: '-created' })
.then((r) => setMessages(r.items as Message[]));
return () => {
pb.collection('messages').unsubscribe('*');
};
}, []);
return (
<ul>
{messages.map((m) => (
<li key={m.id}>
<span>{m.text}</span>
<time>{new Date(m.created).toLocaleTimeString('ru-RU')}</time>
</li>
))}
</ul>
);
} Деплой: PocketBase на VPS + Astro на Cloudflare
PocketBase (на Beget VPS или любом Linux-сервере):
# Запуск как systemd-сервис
sudo nano /etc/systemd/system/pocketbase.service
[Unit]
Description=PocketBase
After=network.target
[Service]
User=www-data
WorkingDirectory=/opt/pocketbase
ExecStart=/opt/pocketbase/pocketbase serve --http 0.0.0.0:8090
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now pocketbase Astro — деплой на Cloudflare Pages (бесплатно). В astro.config.mjs укажите публичный URL PocketBase в переменных окружения.
Итог
PocketBase + Astro — идеальный стек для инди-разработчиков и небольших команд. Один бинарник заменяет целый стек (PostgreSQL + Auth + S3 + WebSockets). Astro обеспечивает быстрый фронтенд. Связка разворачивается за 30 минут и стоит ~$5/мес за VPS. Подробнее о выборе базы данных — в статье Базы данных для Astro.