Supabase для стартапа: полный гайд — от нуля до production
Что такое Supabase?
Supabase — open-source платформа для разработки на PostgreSQL. Включает базу данных, аутентификацию (GoTrue) с passkeys (публичная beta с мая 2026), автоматический REST API (PostgREST), GraphQL (pg_graphql, opt-in), Realtime WebSocket-подписки, S3-совместимое хранилище, Edge Functions (Deno runtime), pgvector для AI/vector search и branching для preview-сред. Альтернатива Firebase без vendor lock-in. Free-план: 500 MB база, 50K MAU. Pro: $25/мес, 8 GB база, 100K MAU.
TL;DR
- -Supabase — open-source Backend-as-a-Service на PostgreSQL. Полноценный SQL, RLS-безопасность на уровне строк, автогенерация REST API из схемы базы. Без vendor lock-in — можно self-host через Docker
- -Архитектура: PostgreSQL (ядро) + PostgREST (REST API) + GoTrue (auth) + Realtime (WebSocket) + Storage (S3-совместимое) + Edge Functions (Deno) + pgvector (AI embeddings)
- -RLS — ключевая концепция безопасности. Политики пишутся на SQL и применяются на уровне базы. Включай с первого дня — добавлять RLS в уже работающее приложение сложнее
- -Edge Functions — serverless на Deno/TypeScript. Идеальны для webhooks, интеграций с AI API, обработки платежей. Для тяжёлой логики — PostgreSQL functions быстрее. Cold start сократился на 97% в 2025
- -Тарифы: Free (500 MB, 50K MAU), Pro ($25/мес, 8 GB, 100K MAU), Team ($599/мес, SOC2, SSO), Enterprise (кастом). Self-hosted — бесплатно
- -Branching — preview-среда под каждый pull request. Отдельный Supabase instance, свои credentials, без production-данных. Must-have для стартапа с командой
Supabase — open-source платформа на PostgreSQL. Полноценный Postgres с надстройками: аутентификация, realtime-подписки, хранилище файлов, serverless-функции, vector search. Всё из коробки, с единым SDK.
За 2025–2026 годы Supabase вырос из «Firebase-альтернативы» в самостоятельную платформу. 100K+ звёзд на GitHub, оценка $10.5B после раунда Series F на $500M в июне 2026, более 2 миллионов еженедельных загрузок npm-пакета.
Из ключевого за 2025–2026: новая модель API keys (publishable/secret вместо anon/service_role), security-by-default — новые таблицы больше не открываются автоматически через Data API без явного GRANT, branching 2.0 без привязки к Git, passkeys (публичная beta), PostgREST v14 (+20% RPS), 97% ускорение cold start для Edge Functions. pg_graphql теперь opt-in для новых проектов. Появился Supabase MCP — агенты читают схему, применяют миграции, смотрят логи прямо из IDE.
Почему стартапы выбирают Supabase: PostgreSQL с 30-летней историей, данные под контролем, self-hosted через Docker — без vendor lock-in. Бесплатный план покрывает MVP и первые тысячи пользователей.
Supabase vs Firebase: честное сравнение
| Критерий | Supabase | Firebase |
|---|---|---|
| База данных | PostgreSQL (реляционная, SQL) | Firestore (NoSQL, документы) |
| Запросы | Полный SQL, JOIN, оконные функции | Ограниченные запросы, нет JOIN |
| Auth | GoTrue: email, OAuth, magic link, phone, SSO | Firebase Auth: email, OAuth, phone |
| Storage | S3-совместимое, RLS на уровне бакетов | Cloud Storage for Firebase |
| Functions | Edge Functions (Deno/TypeScript) | Cloud Functions (Node.js) |
| Realtime | WebSocket-подписки на изменения в БД | Встроенный realtime sync |
| Vector/AI | pgvector — нативный vector search | Нет (нужен Vertex AI отдельно) |
| Pricing | Предсказуемый, тарифные планы | Pay-per-read/write, сложно предсказать |
| Open-source | Да, self-hosted через Docker | Нет |
| Vendor lock-in | Минимальный (стандартный PostgreSQL) | Высокий (проприетарная база) |
| GraphQL | Встроенный (pg_graphql, opt-in для новых проектов) | Нет (нужен Apollo или аналог) |
Когда Firebase лучше
- Mobile-first продукт с offline-sync — Firestore offline persistence работает из коробки
- Realtime как ядро продукта — коллаборативные документы, мультиплеер, live-аукционы. Firebase Realtime Database оптимизирован под это
- Google-экосистема — уже используешь GCP, Firebase Analytics, Cloud Messaging
- Быстрый прототип без SQL-знаний — Firestore проще для фронтенд-разработчиков
Когда Supabase лучше
- Сложные связи данных — foreign keys, JOIN, агрегации, оконные функции
- Контроль над данными — self-hosted, миграции, стандартный SQL
- AI-фичи — pgvector для embeddings и semantic search без отдельного сервиса
- Предсказуемые расходы — фиксированные тарифы вместо pay-per-operation
- Безопасность — Row Level Security на уровне базы, SOC2 на Team-плане
- Полнотекстовый поиск — встроенный в PostgreSQL, без Algolia/Elasticsearch
Архитектура Supabase
Supabase — набор open-source инструментов вокруг PostgreSQL, связанных через Kong API Gateway.
┌─────────────────────────────────────────────────┐
│ Kong API Gateway │
│ (маршрутизация, rate limiting) │
├──────┬──────┬──────┬──────┬──────┬──────┬───────┤
│ Post │ Go │ Real │ Stor │ pg_ │ Edge │ pg_ │
│ gREST│ True │ time │ age │ meta │ Func │graphql│
│ │ │ │ │ │ │ │
│ REST │ Auth │ WS │ S3 │ Mgmt │ Deno │GraphQL│
│ API │ API │ Sub │ API │ API │ RT │ API │
└──┬───┴──┬───┴──┬───┴──┬───┴──┬───┴──┬───┴───┬───┘
│ │ │ │ │ │ │
└──────┴──────┴──────┴──────┴──────┴───────┘
│
┌────────┴────────┐
│ PostgreSQL │
│ + extensions │
│ (pgvector, │
│ pg_cron, │
│ pg_net, ...) │
└─────────────────┘
Компоненты
PostgreSQL — ядро. Все данные, вся логика. Supabase не абстрагирует базу — даёт к ней прямой доступ. Версия 15+ с расширениями: pgvector, pg_cron, pg_net, pg_stat_statements.
PostgREST — автоматически генерирует REST API из схемы базы. Создал таблицу — API готов. Поддерживает фильтрацию, пагинацию, вложенные ресурсы через foreign keys. CRUD-эндпоинты писать вручную не нужно. Версия 14 (2026) добавила JWT cache и, по бенчмаркам Supabase, дала ~20% прирост RPS на GET-запросах.
pg_graphql — GraphQL API прямо из PostgreSQL schema. Альтернатива PostgREST для тех, кто предпочитает GraphQL. С 2026 года для новых проектов — opt-in: GraphQL не включается автоматически, его нужно активировать в Dashboard.
GoTrue — сервер аутентификации. Форк проекта Netlify, переписанный и поддерживаемый Supabase. Email/password, OAuth (Google, GitHub, Apple, Discord и другие), magic link, phone (SMS), SSO (SAML).
Realtime — Elixir-сервер, который мониторит PostgreSQL replication log и транслирует изменения через WebSocket. Три режима: Postgres Changes (подписка на INSERT/UPDATE/DELETE), Broadcast (произвольные сообщения между клиентами), Presence (отслеживание онлайн-статуса).
Storage — S3-совместимое хранилище файлов. Поддерживает RLS-политики на уровне бакетов и объектов. Трансформация изображений (resize, crop) на лету. CDN через встроенный кэш.
Edge Functions — serverless-функции на Deno runtime. TypeScript/JavaScript. Деплой через CLI или Dashboard. Изолированный контекст, встроенная поддержка secrets.
pgvector — расширение PostgreSQL для хранения и поиска по vector embeddings. Semantic search, RAG, AI-фичи — без отдельной vector database.
Быстрый старт: от нуля до рабочего приложения
Создание проекта
- Регистрация на supabase.com
- «New Project» → название, пароль базы данных, регион (выбирай ближайший к пользователям)
- Проект поднимается за 1–2 минуты. Supabase выделяет отдельный PostgreSQL-инстанс
Настройка схемы
Через SQL Editor в Dashboard:
-- Таблица задач
create table todos (
id bigint generated always as identity primary key,
user_id uuid references auth.users(id) not null,
title text not null,
description text,
is_complete boolean default false,
created_at timestamptz default now()
);
-- Включаем RLS
alter table todos enable row level security;
-- Политика: пользователь видит только свои задачи
create policy "Users can view own todos"
on todos for select
using (auth.uid() = user_id);
-- Политика: пользователь создаёт задачи только от своего имени
create policy "Users can create own todos"
on todos for insert
with check (auth.uid() = user_id);
-- Политика: пользователь обновляет только свои задачи
create policy "Users can update own todos"
on todos for update
using (auth.uid() = user_id);
-- Политика: пользователь удаляет только свои задачи
create policy "Users can delete own todos"
on todos for delete
using (auth.uid() = user_id);
Подключение клиента
Установка:
npm install @supabase/supabase-js
Инициализация:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
)
anon-key — публичный ключ. Безопасен для клиентского кода, потому что RLS ограничивает доступ. Не путай с service_role ключом — тот обходит RLS и используется только на сервере.
Новые ключи (2025+). В новых проектах вместо anon-key используй sb_publishable_*, вместо service_role — sb_secret_*. Legacy ключи работают до конца 2026, потом будут удалены. Смотри Settings → API Keys.
Todo-приложение за 20 минут
Полный CRUD:
// Регистрация пользователя
const { data: authData, error: authError } = await supabase.auth.signUp({
email: '[email protected]',
password: 'secure-password-123'
})
// Вход
const { data: signInData, error: loginError } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'secure-password-123'
})
// Создание задачи
const { data: newTodo, error: createError } = await supabase
.from('todos')
.insert({
title: 'Настроить Supabase',
description: 'Создать проект и подключить клиент',
user_id: signInData.user.id
})
.select()
.single()
// Получение списка задач (RLS автоматически фильтрует по user_id)
const { data: todos, error: fetchError } = await supabase
.from('todos')
.select('*')
.order('created_at', { ascending: false })
// Обновление задачи
const { error: updateError } = await supabase
.from('todos')
.update({ is_complete: true })
.eq('id', newTodo.id)
// Удаление задачи
const { error: deleteError } = await supabase
.from('todos')
.delete()
.eq('id', newTodo.id)
Каждый запрос автоматически прикрепляет JWT-токен из сессии. RLS-политики проверяют auth.uid() на стороне базы. Клиентский код не получит чужие данные — даже если модифицировать запрос.
Аутентификация
Методы входа
GoTrue поддерживает все популярные методы. В мае 2026 passkeys вышли в публичную beta — вход по биометрии (Face ID, Touch ID) или аппаратному ключу через WebAuthn. Пользователи регистрируют passkeys через Dashboard, CLI или Management API. API стабилизирован, но всё ещё beta:
// Email + Password
await supabase.auth.signUp({ email, password })
await supabase.auth.signInWithPassword({ email, password })
// Magic Link (email без пароля)
await supabase.auth.signInWithOtp({ email })
// OAuth (Google, GitHub, Apple, Discord, и другие)
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://your-app.com/auth/callback'
}
})
// Phone (SMS)
await supabase.auth.signInWithOtp({ phone: '+79001234567' })
// Верификация SMS-кода
await supabase.auth.verifyOtp({
phone: '+79001234567',
token: '123456',
type: 'sms'
})
// Anonymous sign-in — для onboarding без регистрации, корзин, demo-режима
const { data, error } = await supabase.auth.signInAnonymously()
// Анонимный пользователь получает authenticated роль — RLS политики работают
// Конвертация в полноценного пользователя через linkIdentity() при желании зарегистрироваться
Управление сессией
// Текущая сессия
const { data: { session } } = await supabase.auth.getSession()
// Текущий пользователь
const { data: { user } } = await supabase.auth.getUser()
// Подписка на изменения состояния auth
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
// Пользователь вошёл
}
if (event === 'SIGNED_OUT') {
// Пользователь вышел
}
if (event === 'TOKEN_REFRESHED') {
// Токен обновлён
}
})
// Выход
await supabase.auth.signOut()
Row Level Security — ключевая концепция
RLS — главное отличие Supabase от Firebase Security Rules. Политики пишутся на SQL и выполняются прямо в PostgreSQL — на уровне базы, а не в middleware или серверном коде приложения.
Принцип простой: таблица без RLS открыта всем, с включённым RLS — закрыта по умолчанию. Доступ открывается через политики.
-- Включить RLS
alter table profiles enable row level security;
-- Базовые политики для профилей пользователей
-- Профиль виден всем (публичные данные)
create policy "Public profiles are viewable by everyone"
on profiles for select
using (true);
-- Пользователь редактирует только свой профиль
create policy "Users can update own profile"
on profiles for update
using (auth.uid() = id)
with check (auth.uid() = id);
-- Пользователь создаёт только свой профиль
create policy "Users can insert own profile"
on profiles for insert
with check (auth.uid() = id);
Продвинутые политики:
-- Доступ на основе ролей (RBAC)
create policy "Admins can do anything"
on todos for all
using (
exists (
select 1 from profiles
where profiles.id = auth.uid()
and profiles.role = 'admin'
)
);
-- Доступ к данным организации
create policy "Members can view org data"
on projects for select
using (
exists (
select 1 from org_members
where org_members.org_id = projects.org_id
and org_members.user_id = auth.uid()
)
);
-- Доступ по временному окну
create policy "Can only edit recent records"
on posts for update
using (
auth.uid() = author_id
and created_at > now() - interval '24 hours'
);
Новая модель API keys (с 2025)
Supabase перешёл с legacy JWT-based ключей на новую систему. Legacy anon и service_role ключи deprecated, удаление — конец 2026:
| Ключ | Замена | Где использовать |
|---|---|---|
anon key | sb_publishable_* | Публичный клиентский код, браузер, мобильное приложение |
service_role key | sb_secret_* | Только trusted backend: Edge Functions, серверный код |
Publishable key несёт те же ограниченные права, что и anon key — RLS работает как прежде. Secret key обходит RLS, как service_role.
Для миграции: Settings → API Keys → создай publishable и secret ключи, обнови код.
Security-by-default (с мая 2026)
Breaking change. Новые таблицы в схеме public больше не открываются через Data API автоматически. Нужен явный GRANT:
-- После создания таблицы — явно открываем доступ для нужных ролей
grant select, insert, update, delete
on table todos
to anon, authenticated;
-- И включаем RLS — без него таблица открыта всем ролям с грантом
alter table todos enable row level security;
До 30 мая 2026 Supabase автоматически давал SELECT, INSERT, UPDATE, DELETE на все таблицы в public. Теперь — нет. Для существующих проектов изменение вступит в силу 30 октября 2026. Если таблица не видна через API, проверь grants через Security Advisor в Dashboard.
Безопасность: лучшие практики
- Включай RLS на каждой таблице — без исключений. Забытая таблица без RLS = утечка данных
- Явный GRANT после создания таблицы — начиная с мая 2026 таблицы не открываются в Data API без явных грантов
- Никогда не используй
service_role/ secret key в клиентском коде — он обходит все RLS-политики - Проверяй
with check—usingфильтрует чтение,with checkвалидирует запись. Нужны оба - Тестируй политики — SQL Editor в Dashboard позволяет проверить RLS от имени конкретного пользователя
- Используй
auth.uid()вместо передачи user_id от клиента — клиент не может подделатьauth.uid() - Auth Hooks — кастомные обработчики для access token, email/SMS отправки, MFA-верификации. Позволяют добавлять кастомные claims в JWT без хаоса в
user_metadata - RLS Tester (Feature Preview) — новый инструмент в Dashboard: запускает SELECT от имени anon/authenticated пользователя и показывает, какая политика сработала. Удобен для отладки без ручного
SET role. Пока поддерживает только SELECT
Работа с данными
CRUD-операции
// SELECT с фильтрами
const { data } = await supabase
.from('products')
.select('id, name, price, category:categories(name)')
.gte('price', 100)
.lte('price', 500)
.order('price', { ascending: true })
.range(0, 9) // пагинация: первые 10 записей
// INSERT нескольких записей
const { data } = await supabase
.from('products')
.insert([
{ name: 'Widget A', price: 150 },
{ name: 'Widget B', price: 250 }
])
.select()
// UPSERT (вставка или обновление)
const { data } = await supabase
.from('products')
.upsert(
{ id: 1, name: 'Widget A', price: 175 },
{ onConflict: 'id' }
)
.select()
Связи и вложенные запросы
PostgREST автоматически определяет связи по foreign keys:
// Получить посты с автором и комментариями
const { data } = await supabase
.from('posts')
.select(`
id,
title,
content,
author:profiles(id, name, avatar_url),
comments(
id,
text,
user:profiles(name)
)
`)
.eq('published', true)
.order('created_at', { ascending: false })
Один запрос — один SQL-запрос с JOIN. Никаких N+1, никаких лишних round-trip.
Realtime-подписки
Три режима работы Realtime:
// 1. Postgres Changes — подписка на изменения в таблице
const channel = supabase
.channel('todos-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE или *
schema: 'public',
table: 'todos',
filter: `user_id=eq.${userId}`
},
(payload) => {
console.log('Change:', payload.eventType, payload.new)
}
)
.subscribe()
// 2. Broadcast — произвольные сообщения между клиентами
const channel = supabase.channel('room-1')
// Отправка
channel.send({
type: 'broadcast',
event: 'cursor-move',
payload: { x: 100, y: 200, userId: 'abc' }
})
// Получение
channel
.on('broadcast', { event: 'cursor-move' }, (payload) => {
console.log('Cursor:', payload.payload)
})
.subscribe()
// 3. Presence — отслеживание онлайн-пользователей
const channel = supabase.channel('online-users')
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online:', Object.keys(state).length)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString()
})
}
})
// Отключение
supabase.removeChannel(channel)
Database Functions
Серверная логика прямо в PostgreSQL. Быстрее Edge Functions, нет cold start:
-- Функция для подсчёта статистики пользователя
create or replace function get_user_stats(target_user_id uuid)
returns json
language plpgsql
security definer -- выполняется с правами создателя, не вызывающего
as $$
declare
result json;
begin
select json_build_object(
'total_todos', (select count(*) from todos where user_id = target_user_id),
'completed', (select count(*) from todos where user_id = target_user_id and is_complete = true),
'pending', (select count(*) from todos where user_id = target_user_id and is_complete = false)
) into result;
return result;
end;
$$;
Вызов из клиента:
const { data, error } = await supabase.rpc('get_user_stats', {
target_user_id: userId
})
// { total_todos: 42, completed: 35, pending: 7 }
Полнотекстовый поиск
В PostgreSQL встроенный full-text search. Elasticsearch не нужен:
-- Добавляем колонку для поиска
alter table posts add column fts tsvector
generated always as (
setweight(to_tsvector('russian', coalesce(title, '')), 'A') ||
setweight(to_tsvector('russian', coalesce(content, '')), 'B')
) stored;
-- Индекс для быстрого поиска
create index posts_fts_idx on posts using gin(fts);
// Поиск из клиента
const { data } = await supabase
.from('posts')
.select('id, title, content')
.textSearch('fts', 'supabase & стартап', {
type: 'websearch',
config: 'russian'
})
Edge Functions
Что это и когда использовать
Edge Functions — serverless-функции на Deno runtime. TypeScript из коробки, без настройки сборки. Деплой через CLI.
Используй Edge Functions для:
- Webhook-обработчиков (Stripe, GitHub, Telegram)
- Интеграций с внешними API (Claude, OpenAI, отправка email)
- Логики, которая не ложится в SQL (сложные трансформации, генерация PDF)
- Cron-задач
Не используй для:
- CRUD-операций — PostgREST быстрее
- Тяжёлых вычислений — ограничения по времени и памяти
- Логики, которую хорошо выразить в SQL — PostgreSQL functions быстрее, нет сетевого round-trip
Создание и деплой
# Установка Supabase CLI
npm install -g supabase
# Инициализация проекта (если ещё не сделана)
supabase init
# Создание функции
supabase functions new process-webhook
# Структура:
# supabase/functions/process-webhook/index.ts
Пример: webhook-обработчик
// supabase/functions/process-webhook/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { crypto } from 'https://deno.land/std/crypto/mod.ts'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
// Проверка подписи webhook (HMAC-SHA256, как у Stripe и большинства сервисов)
async function verifySignature(body: string, signature: string, secret: string): Promise<boolean> {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signed = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body))
const expected = Array.from(new Uint8Array(signed))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
return signature === expected
}
Deno.serve(async (req) => {
// Обработка CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
const body = await req.text()
// Проверяем подпись webhook — отклоняем запросы без валидной подписи
const signature = req.headers.get('x-webhook-signature') ?? ''
const webhookSecret = Deno.env.get('WEBHOOK_SECRET')!
const isValid = await verifySignature(body, signature, webhookSecret)
if (!isValid) {
return new Response(JSON.stringify({ error: 'Invalid signature' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
const payload = JSON.parse(body)
// Базовая валидация payload
if (!payload || typeof payload.type !== 'string') {
return new Response(JSON.stringify({ error: 'Invalid payload' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
// Создаём Supabase-клиент с service_role ключом
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Обработка webhook
const { error } = await supabase
.from('webhook_events')
.insert({
event_type: payload.type,
data: payload,
processed_at: new Date().toISOString()
})
if (error) throw error
return new Response(
JSON.stringify({ success: true }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Internal error'
return new Response(
JSON.stringify({ error: message }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
})
Интеграция с AI
// supabase/functions/ai-summarize/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
Deno.serve(async (req) => {
const { text, postId } = await req.json()
// Вызов Claude API
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': Deno.env.get('ANTHROPIC_API_KEY')!,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-6',
max_tokens: 300,
messages: [{
role: 'user',
content: `Кратко перескажи текст в 2-3 предложения:\n\n${text}`
}]
})
})
const result = await response.json()
const summary = result.content[0].text
// Сохраняем в базу
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
await supabase
.from('posts')
.update({ summary })
.eq('id', postId)
return new Response(JSON.stringify({ summary }), {
headers: { 'Content-Type': 'application/json' }
})
})
Secrets management
# Установка секретов (не коммитятся в git)
supabase secrets set ANTHROPIC_API_KEY=sk-ant-...
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...
# Просмотр установленных секретов
supabase secrets list
# Деплой функции
supabase functions deploy ai-summarize
В Edge Functions переменные SUPABASE_URL и SUPABASE_SERVICE_ROLE_KEY доступны автоматически — устанавливать вручную не нужно.
Storage
Загрузка файлов
// Создание бакета (через Dashboard или SQL)
// Dashboard → Storage → New bucket
// Загрузка файла
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${userId}/avatar.png`, file, {
cacheControl: '3600',
upsert: true // перезаписать, если существует
})
// Получение публичного URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.png`)
// Скачивание
const { data: fileData, error: downloadError } = await supabase.storage
.from('avatars')
.download(`${userId}/avatar.png`)
// Удаление
const { error: deleteError } = await supabase.storage
.from('avatars')
.remove([`${userId}/avatar.png`])
// Список файлов в директории
const { data: files } = await supabase.storage
.from('avatars')
.list(userId, {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})
Трансформации изображений
Supabase трансформирует изображения на лету через URL-параметры:
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.png`, {
transform: {
width: 200,
height: 200,
resize: 'cover', // cover, contain, fill
quality: 80,
format: 'origin' // или 'webp' для автоматической конвертации
}
})
Результат кэшируется CDN. Оригинальный файл не изменяется.
RLS для Storage
Storage использует те же RLS-политики, что и обычные таблицы. Бакеты хранятся в схеме storage:
-- Пользователь загружает только в свою папку
create policy "Users can upload own files"
on storage.objects for insert
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = auth.uid()::text
);
-- Публичные аватары видны всем
create policy "Public avatar access"
on storage.objects for select
using (bucket_id = 'avatars');
-- Пользователь удаляет только свои файлы
create policy "Users can delete own files"
on storage.objects for delete
using (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = auth.uid()::text
);
AI-фичи: pgvector и embeddings
Vector search в PostgreSQL
pgvector превращает PostgreSQL в vector database. Pinecone, Weaviate и Qdrant не нужны — embeddings хранятся рядом с остальными данными.
-- Включение расширения
create extension if not exists vector;
-- Таблица с embeddings
create table documents (
id bigint generated always as identity primary key,
title text not null,
content text not null,
embedding vector(1536), -- размерность зависит от модели
metadata jsonb,
created_at timestamptz default now()
);
-- Индекс для быстрого поиска (HNSW — рекомендуемый дефолт)
create index on documents using hnsw (embedding vector_cosine_ops)
with (m = 16, ef_construction = 64);
-- Альтернатива при жёстком ограничении памяти — IVFFlat (строй только после загрузки данных):
-- create index on documents using ivfflat (embedding vector_cosine_ops)
-- with (lists = 100);
RAG-паттерн с Supabase
RAG (Retrieval-Augmented Generation) — подход, при котором LLM получает релевантный контекст из базы знаний перед генерацией ответа.
Edge Function для генерации embeddings и поиска:
// supabase/functions/search/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
Deno.serve(async (req) => {
const { query } = await req.json()
// 1. Генерация embedding для запроса
const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`
},
body: JSON.stringify({
model: 'text-embedding-3-small',
input: query
})
})
const { data: [{ embedding }] } = await embeddingResponse.json()
// 2. Поиск похожих документов
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { data: documents } = await supabase.rpc('match_documents', {
query_embedding: embedding,
match_threshold: 0.78,
match_count: 5
})
return new Response(JSON.stringify({ documents }), {
headers: { 'Content-Type': 'application/json' }
})
})
SQL-функция для similarity search:
create or replace function match_documents(
query_embedding vector(1536),
match_threshold float default 0.78,
match_count int default 5
)
returns table (
id bigint,
title text,
content text,
similarity float
)
language sql stable
as $$
select
documents.id,
documents.title,
documents.content,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where 1 - (documents.embedding <=> query_embedding) > match_threshold
order by documents.embedding <=> query_embedding
limit match_count;
$$;
Оператор <=> — cosine distance. 1 - distance = similarity. Чем ближе к 1, тем более похожие документы.
HNSW vs IVFFlat. С pgvector 0.6+ рекомендуемый индекс — HNSW. Преимущества: строится сразу при создании таблицы (IVFFlat требует данных для обучения), не нужно пересоздавать при росте данных, лучше recall при схожей скорости поиска. IVFFlat остаётся вариантом при жёстком ограничении памяти.
Hybrid search. Для лучшей точности комбинируй vector search с PostgreSQL full-text search через RRF (Reciprocal Rank Fusion). Supabase docs предоставляет готовый паттерн.
Пример: полный RAG-pipeline
// Edge Function: вопрос → поиск контекста → ответ LLM
Deno.serve(async (req) => {
const { question } = await req.json()
// 1. Embed вопрос
const embResponse = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`
},
body: JSON.stringify({
model: 'text-embedding-3-small',
input: question
})
})
const { data: [{ embedding }] } = await embResponse.json()
// 2. Найти релевантные документы
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { data: docs } = await supabase.rpc('match_documents', {
query_embedding: embedding,
match_threshold: 0.75,
match_count: 3
})
const context = docs.map(d => d.content).join('\n\n')
// 3. Сгенерировать ответ с контекстом
const chatResponse = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': Deno.env.get('ANTHROPIC_API_KEY')!,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
system: `Отвечай на вопросы, используя только предоставленный контекст. Если ответа нет в контексте — скажи об этом.\n\nКонтекст:\n${context}`,
messages: [{ role: 'user', content: question }]
})
})
const answer = await chatResponse.json()
return new Response(JSON.stringify({
answer: answer.content[0].text,
sources: docs.map(d => ({ id: d.id, title: d.title, similarity: d.similarity }))
}), {
headers: { 'Content-Type': 'application/json' }
})
})
Тарифы и масштабирование
Планы (актуально на июнь 2026)
| Free | Pro | Team | Enterprise | |
|---|---|---|---|---|
| Цена | $0 | $25/мес | $599/мес | По запросу |
| Проекты | 2 | Без лимита | Без лимита | Без лимита |
| База данных | 500 MB | 8 GB | 8 GB | Кастом |
| Storage | 1 GB | 100 GB | 100 GB | Кастом |
| Egress (БД) | 5 GB | 250 GB | 250 GB | Кастом |
| Auth MAU | 50K | 100K | 100K | Кастом |
| Edge Functions | 500K вызовов | 2M вызовов | 2M вызовов | Кастом |
| Realtime | 200 подключений | 500 подключений | 500 подключений | Кастом |
| Бэкапы | Нет | Ежедневные (7 дней) | Ежедневные (14 дней) + PITR | Кастом |
| Branching | Нет | Да (preview + persistent) | Да | Да |
| Пауза проекта | Через 7 дней неактивности | Нет | Нет | Нет |
| SOC2 / ISO 27001 | Нет | Нет | Да | Да |
| SSO/SAML | Нет | Нет | Да | Да |
| SLA | Нет | Нет | 99.9% | 99.95%+ |
| Поддержка | Community | Приоритетный email | Dedicated |
Pro и Team — usage-based. Превышение включённых лимитов оплачивается отдельно. Пример: на Pro дополнительный 1 GB базы — $0.125/GB/мес, дополнительные MAU — $0.00325/MAU. Pro включает $10/мес compute credits, которых достаточно для покрытия одного Micro-инстанса.
Self-hosted
Supabase полностью open-source. Можно поднять на своём сервере через Docker:
# Клонирование
git clone --depth 1 https://github.com/supabase/supabase
cd supabase/docker
# Копирование env-файла
cp .env.example .env
# Обязательно измени: POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY
# Запуск
docker compose up -d
Self-hosted Supabase включает все компоненты: PostgreSQL, GoTrue, PostgREST, Realtime, Storage, pg_meta, Kong, Edge Runtime (Deno). Не включает: автоматические бэкапы, управляемый Dashboard Supabase (доступен open-source Studio).
Когда переходить на Pro
- Проект нужен 24/7 (Free паузит после 7 дней без запросов)
- База приближается к 500 MB
- Нужны ежедневные бэкапы
- Больше 200 одновременных Realtime-подключений
Branching — preview-среды для команды
Branching создаёт отдельный Supabase-инстанс под каждый pull request: своя база, свои API credentials, свои Edge Functions. Production-данные не попадают в бранч по умолчанию.
Два типа бранчей:
- Preview branch — создаётся автоматически при открытии PR, удаляется при закрытии
- Persistent branch — долгоживущий, для staging/QA/dev, не засыпает от неактивности
Branching 2.0 (2025) убрал обязательную привязку к GitHub — бранчами можно управлять прямо из Dashboard (public alpha).
Workflow для стартапа:
main→ production project- Feature branch → preview branch с автоматическими миграциями из PR
- Seed-файл без PII — тестовые данные без production-секретов
- Проверяй RLS, grants, Edge Functions до merge
Branching доступен начиная с Pro-плана.
Оптимизация расходов
- Индексы — без них PostgreSQL сканирует всю таблицу. Один правильный индекс экономит десятки долларов на compute
- Connection pooling — используй Supavisor (встроен). Не открывай прямые подключения из serverless
- RLS-политики — плохо написанная политика с подзапросом замедляет каждый запрос. Тестируй через
EXPLAIN ANALYZE - Edge Functions — кэшируй результаты, не вызывай на каждый запрос. Для CRUD используй PostgREST
- Storage — включи кэширование (cacheControl), используй трансформации вместо хранения нескольких размеров
Лучшие практики для стартапов
Миграции
Не редактируй схему через Dashboard в production. Используй миграции:
# Создание миграции
supabase migration new add_projects_table
# Редактирование: supabase/migrations/20260409_add_projects_table.sql
-- supabase/migrations/20260409_add_projects_table.sql
create table projects (
id uuid default gen_random_uuid() primary key,
name text not null,
owner_id uuid references auth.users(id) not null,
created_at timestamptz default now()
);
-- С мая 2026: явный GRANT обязателен — без него таблица не видна через Data API
grant select, insert, update, delete
on table projects
to authenticated;
alter table projects enable row level security;
create policy "Owner access"
on projects for all
using (auth.uid() = owner_id)
with check (auth.uid() = owner_id);
# Применение миграции к локальной базе
supabase db push
# Применение к production
supabase db push --linked
Миграции коммитятся в git. Каждый разработчик видит историю изменений схемы. Откат — через обратную миграцию.
RLS с первого дня
Добавлять RLS в уже работающее приложение — боль. Нужно проверить каждую таблицу, каждый запрос. Включай RLS сразу при создании таблицы.
Чек-лист:
-
grant select, insert, update, delete on table X to authenticated— явный GRANT (обязателен с мая 2026) -
alter table X enable row level securityна каждой новой таблице - Политики для SELECT, INSERT, UPDATE, DELETE — отдельно
-
with checkдля INSERT и UPDATE - Тест: попробуй получить чужие данные через Supabase client
-
service_role/ secret key только в Edge Functions и серверном коде
TypeScript-типы
Supabase CLI генерирует типы из схемы базы:
supabase gen types typescript --linked > src/types/database.ts
import { createClient } from '@supabase/supabase-js'
import type { Database } from './types/database'
const supabase = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
)
// Теперь TypeScript знает структуру таблиц
const { data } = await supabase
.from('todos')
.select('id, title, is_complete')
// data: { id: number; title: string; is_complete: boolean }[] | null
Запускай генерацию типов в CI/CD после каждой миграции.
Мониторинг
- Dashboard → Reports → медленные запросы, использование ресурсов
- pg_stat_statements — расширение PostgreSQL для мониторинга производительности запросов
- Supabase Logs — логи всех сервисов: Auth, PostgREST, Storage, Edge Functions
- Alerts — настрой уведомления на превышение лимитов (база > 80%, CPU высокий)
- Log Drains (Pro+) — экспорт логов в Datadog, Loki, Sentry, S3 или любой OTLP-совместимый коллектор. Связывает Supabase с существующим observability-стеком
- Supabase MCP — AI-агенты (Claude, Cursor, Windsurf и другие) могут читать схему, выполнять SQL, применять миграции, просматривать логи и деплоить Edge Functions напрямую из IDE. Не подключай MCP к production-проекту с real data — используй отдельный dev-проект
-- Топ-10 медленных запросов
select
query,
calls,
mean_exec_time::numeric(10,2) as avg_ms,
total_exec_time::numeric(10,2) as total_ms
from pg_stat_statements
order by mean_exec_time desc
limit 10;
Бэкапы
- Free — нет автоматических бэкапов. Делай
pg_dumpвручную - Pro — ежедневные бэкапы, хранятся 7 дней
- Team — ежедневные бэкапы + PITR (Point-in-Time Recovery) — восстановление на любую секунду
- Self-hosted — настраивай через pg_basebackup или WAL-G
Для критичных данных на Free-плане:
# Бэкап через CLI
supabase db dump -f backup.sql --linked
# Или через pg_dump напрямую
pg_dump postgresql://postgres:[password]@db.[project].supabase.co:5432/postgres > backup.sql
Edge Functions vs PostgreSQL Functions
| Edge Functions | PostgreSQL Functions | |
|---|---|---|
| Runtime | Deno (TypeScript) | PL/pgSQL, PL/v8 |
| Cold start | Да (значительно сокращён в 2025, до ~97%) | Нет |
| Доступ к сети | Да (fetch) | Через pg_net |
| AI API | Нативно | Через pg_net (сложнее) |
| Скорость для CRUD | Медленнее (сетевой hop) | Быстрее (внутри БД) |
| Отладка | Логи в Dashboard | RAISE NOTICE |
Правило: логика работает с данными в базе и не вызывает внешние API — PostgreSQL function. Нужен HTTP-запрос наружу (AI, платежи, email) — Edge Function.
Supabase закрывает 90% backend-потребностей стартапа одним инструментом. PostgreSQL под капотом — это преимущество: полноценный SQL, RLS, extensions, проверенная технология. Начни с Free-плана, перейди на Pro когда проект дорастёт до production — вместе с branching для безопасного деплоя. Self-hosted вариант всегда доступен как страховка от vendor lock-in.