Туториалы

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: честное сравнение

КритерийSupabaseFirebase
База данныхPostgreSQL (реляционная, SQL)Firestore (NoSQL, документы)
ЗапросыПолный SQL, JOIN, оконные функцииОграниченные запросы, нет JOIN
AuthGoTrue: email, OAuth, magic link, phone, SSOFirebase Auth: email, OAuth, phone
StorageS3-совместимое, RLS на уровне бакетовCloud Storage for Firebase
FunctionsEdge Functions (Deno/TypeScript)Cloud Functions (Node.js)
RealtimeWebSocket-подписки на изменения в БДВстроенный realtime sync
Vector/AIpgvector — нативный 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.

Быстрый старт: от нуля до рабочего приложения

Создание проекта

  1. Регистрация на supabase.com
  2. «New Project» → название, пароль базы данных, регион (выбирай ближайший к пользователям)
  3. Проект поднимается за 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_rolesb_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 keysb_publishable_*Публичный клиентский код, браузер, мобильное приложение
service_role keysb_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.

Безопасность: лучшие практики

  1. Включай RLS на каждой таблице — без исключений. Забытая таблица без RLS = утечка данных
  2. Явный GRANT после создания таблицы — начиная с мая 2026 таблицы не открываются в Data API без явных грантов
  3. Никогда не используй service_role / secret key в клиентском коде — он обходит все RLS-политики
  4. Проверяй with checkusing фильтрует чтение, with check валидирует запись. Нужны оба
  5. Тестируй политики — SQL Editor в Dashboard позволяет проверить RLS от имени конкретного пользователя
  6. Используй auth.uid() вместо передачи user_id от клиента — клиент не может подделать auth.uid()
  7. Auth Hooks — кастомные обработчики для access token, email/SMS отправки, MFA-верификации. Позволяют добавлять кастомные claims в JWT без хаоса в user_metadata
  8. 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)

FreeProTeamEnterprise
Цена$0$25/мес$599/месПо запросу
Проекты2Без лимитаБез лимитаБез лимита
База данных500 MB8 GB8 GBКастом
Storage1 GB100 GB100 GBКастом
Egress (БД)5 GB250 GB250 GBКастом
Auth MAU50K100K100KКастом
Edge Functions500K вызовов2M вызовов2M вызововКастом
Realtime200 подключений500 подключений500 подключенийКастом
БэкапыНетЕжедневные (7 дней)Ежедневные (14 дней) + PITRКастом
BranchingНетДа (preview + persistent)ДаДа
Пауза проектаЧерез 7 дней неактивностиНетНетНет
SOC2 / ISO 27001НетНетДаДа
SSO/SAMLНетНетДаДа
SLAНетНет99.9%99.95%+
ПоддержкаCommunityEmailПриоритетный emailDedicated

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 для стартапа:

  1. main → production project
  2. Feature branch → preview branch с автоматическими миграциями из PR
  3. Seed-файл без PII — тестовые данные без production-секретов
  4. Проверяй RLS, grants, Edge Functions до merge

Branching доступен начиная с Pro-плана.

Оптимизация расходов

  1. Индексы — без них PostgreSQL сканирует всю таблицу. Один правильный индекс экономит десятки долларов на compute
  2. Connection pooling — используй Supavisor (встроен). Не открывай прямые подключения из serverless
  3. RLS-политики — плохо написанная политика с подзапросом замедляет каждый запрос. Тестируй через EXPLAIN ANALYZE
  4. Edge Functions — кэшируй результаты, не вызывай на каждый запрос. Для CRUD используй PostgREST
  5. 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 FunctionsPostgreSQL Functions
RuntimeDeno (TypeScript)PL/pgSQL, PL/v8
Cold startДа (значительно сокращён в 2025, до ~97%)Нет
Доступ к сетиДа (fetch)Через pg_net
AI APIНативноЧерез pg_net (сложнее)
Скорость для CRUDМедленнее (сетевой hop)Быстрее (внутри БД)
ОтладкаЛоги в DashboardRAISE 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.