# Supabase для стартапа: полный гайд — от нуля до production

> Исчерпывающее руководство по Supabase: архитектура, аутентификация, RLS, Edge Functions, pgvector, branching. Сравнение с Firebase, тарифы, лучшие практики для стартапов на середину 2026 года.
> Author: Roman Belov · Published: 2026-06-26 · Source: https://futurecraft.pro/ru/blog/supabase-startup-guide/

Supabase — open-source платформа на PostgreSQL. Полноценный Postgres с надстройками: аутентификация, realtime-подписки, хранилище файлов, serverless-функции, vector search. Всё из коробки, с единым SDK.

За 2025–2026 годы Supabase вырос из «Firebase-альтернативы» в самостоятельную платформу. 100K+ звёзд на GitHub, оценка $10.5B после [раунда Series F на $500M](https://supabase.com/blog/supabase-series-f) в июне 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](https://github.com/orgs/supabase/discussions/41288) (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](https://supabase.com)
2. «New Project» → название, пароль базы данных, регион (выбирай ближайший к пользователям)
3. Проект поднимается за 1–2 минуты. Supabase выделяет отдельный PostgreSQL-инстанс

### Настройка схемы

Через SQL Editor в Dashboard:

```sql
-- Таблица задач
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);
```

### Подключение клиента

Установка:

```bash
npm install @supabase/supabase-js
```

Инициализация:

```typescript
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:

```typescript
// Регистрация пользователя
const { data: authData, error: authError } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'secure-password-123'
})

// Вход
const { data: signInData, error: loginError } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  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](https://supabase.com/changelog/46458-passkeys-for-supabase-auth-beta) — вход по биометрии (Face ID, Touch ID) или аппаратному ключу через WebAuthn. Пользователи регистрируют passkeys через Dashboard, CLI или Management API. API стабилизирован, но всё ещё beta:

```typescript
// 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() при желании зарегистрироваться
```

### Управление сессией

```typescript
// Текущая сессия
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 — закрыта по умолчанию. Доступ открывается через политики.

```sql
-- Включить 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);
```

Продвинутые политики:

```sql
-- Доступ на основе ролей (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:

```sql
-- После создания таблицы — явно открываем доступ для нужных ролей
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](https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically). Если таблица не видна через API, проверь grants через Security Advisor в Dashboard.

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

1. **Включай RLS на каждой таблице** — без исключений. Забытая таблица без RLS = утечка данных
2. **Явный GRANT после создания таблицы** — начиная с мая 2026 таблицы не открываются в Data API без явных грантов
3. **Никогда не используй `service_role` / secret key в клиентском коде** — он обходит все RLS-политики
4. **Проверяй `with check`** — `using` фильтрует чтение, `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-операции

```typescript
// 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:

```typescript
// Получить посты с автором и комментариями
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:

```typescript
// 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:

```sql
-- Функция для подсчёта статистики пользователя
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;
$$;
```

Вызов из клиента:

```typescript
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 не нужен:

```sql
-- Добавляем колонку для поиска
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);
```

```typescript
// Поиск из клиента
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

### Создание и деплой

```bash
# Установка Supabase CLI
npm install -g supabase

# Инициализация проекта (если ещё не сделана)
supabase init

# Создание функции
supabase functions new process-webhook

# Структура:
# supabase/functions/process-webhook/index.ts
```

### Пример: webhook-обработчик

```typescript
// 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

```typescript
// 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

```bash
# Установка секретов (не коммитятся в 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

### Загрузка файлов

```typescript
// Создание бакета (через 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-параметры:

```typescript
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`:

```sql
-- Пользователь загружает только в свою папку
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 хранятся рядом с остальными данными.

```sql
-- Включение расширения
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 и поиска:

```typescript
// 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:

```sql
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

```typescript
// 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 | Приоритетный 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:

```bash
# Клонирование
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. Используй миграции:

```bash
# Создание миграции
supabase migration new add_projects_table

# Редактирование: supabase/migrations/20260409_add_projects_table.sql
```

```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);
```

```bash
# Применение миграции к локальной базе
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 генерирует типы из схемы базы:

```bash
supabase gen types typescript --linked > src/types/database.ts
```

```typescript
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-проект

```sql
-- Топ-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-плане:

```bash
# Бэкап через 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.
