- Регистрация
- 23 Авг 2023
- Сообщения
- 3,969
- Реакции
- 0
- Баллы
- 36
Ofline
TL;DR
Мигрировал продакшн базу с Supabase на VPS PostgreSQL прямо на работающем проекте — без остановки, без потери данных. Заодно перенёс авторизацию через strangler-подход и убрал Supabase из SSR read-path. Расскажу три инженерных решения с кодом.
Контекст
Строил маркетплейс недвижимости: карточки объектов, фильтры, личный кабинет, кастомная админка. Стартовал на Supabase — быстро, удобно, auth из коробки.
К середине проекта стало ясно:
Решение: мигрировать на VPS PostgreSQL. Но без big bang — прод работает, пользователи не должны ничего заметить.
Почему не pg_dump + переключение connection string
Классический подход:
Проблемы:
Нужен безопасный cutover с rollback на каждом шаге.
Фазы перехода
Фаза 1 — Dual-write. Все записи уходят одновременно в Supabase и VPS. Source of truth — Supabase.
Фаза 2 — Compare-job. Фоновый процесс регулярно сравнивает данные в обеих базах. Расхождения логируются — это даёт уверенность что VPS не отстаёт.
Фаза 3 — Soak period. Несколько дней dual-write + мониторинг. Если расхождений нет — переходим дальше.
Фаза 4 — Переключение source of truth. Читаем из VPS, пишем в обе. Supabase получает данные "на всякий случай".
Фаза 5 — Rollback gate. 48 часов мониторинга. Если метрики ок — убираем дублирующую запись в Supabase. Один флаг возвращает всё назад за 30 секунд.
Результат: нулевой downtime, нулевая потеря данных.
Почему Auth — самое страшное
Supabase Auth — это не просто таблица users. Это JWT-токены, magic links, RLS, активные сессии. "Выключить и включить свой" — все залогиненные пользователи получат 401 и потребуют перелогина. На проде — неприемлемо.
Решение: auth bridge
Идея strangler: новая система постепенно перехватывает трафик, старая умирает сама.
Шаг 1. Добавляем VPS session layer поверх Supabase Auth. Новые логины проходят через наш session manager, но валидация пароля делегируется в Supabase.
Шаг 2. Middleware переходит на проверку VPS-сессий. Supabase JWT больше не попадает к клиенту.
Шаг 3. Когда VPS-сессии стабильны — переносим хеши паролей, переключаем валидацию на bcrypt. Supabase Auth выключается.
Результат: ни один пользователь не получил 401. Bridge прожил 3 дня — потом тихо умер.
Проблема
Next.js SSR на каждый запрос должен отдать HTML с данными для индексации. Если SSR ходит в Supabase, а тот тормозит — страница не рендерится, поисковик видит пустой HTML.
Задача: убрать Supabase из SSR read-path полностью, не сломав sitemap и метаданные.
Решение: ReadRepository с переключаемым источником
Один env-флаг — переключение без редеплоя. Rollback за 30 секунд.
Результат: SSR latency снизился на 40ms (убрали сетевой хоп до Supabase). Sitemap читает напрямую из PostgreSQL. SEO-индексация без перебоев.
Ключевой вывод: большие миграции на проде — это не событие, а процесс. Dual-write + compare-job + rollback gate дают уверенность на каждом шаге.
Если интересно — в комментариях отвечу на вопросы по реализации.
Стек: Next.js 15 · TypeScript · PostgreSQL · Tailwind CSS · Vercel
Яков Радченко, Full-Stack разработчик
Мигрировал продакшн базу с Supabase на VPS PostgreSQL прямо на работающем проекте — без остановки, без потери данных. Заодно перенёс авторизацию через strangler-подход и убрал Supabase из SSR read-path. Расскажу три инженерных решения с кодом.
Контекст
Строил маркетплейс недвижимости: карточки объектов, фильтры, личный кабинет, кастомная админка. Стартовал на Supabase — быстро, удобно, auth из коробки.
К середине проекта стало ясно:
RLS политики начинают мешать по мере усложнения логики
Стоимость при масштабировании некомфортная
Контроль над базой — нулевой
152-ФЗ: хранение данных РФ-пользователей на зарубежных серверах — юридический риск
Решение: мигрировать на VPS PostgreSQL. Но без big bang — прод работает, пользователи не должны ничего заметить.
Часть 1: Dual-write миграция — переключение source of truth по сущностям
Почему не pg_dump + переключение connection string
Классический подход:
Код:
textpg_dump supabase → залить на VPS → поменять DATABASE_URL → молиться
pg_dump supabase → залить на VPS → поменять DATABASE_URL → молиться Проблемы:
Данные между дампом и переключением теряются
RLS политики Supabase не переносятся 1-в-1
Нет пути назад если что-то пошло не так
Нужен безопасный cutover с rollback на каждом шаге.
Фазы перехода
Фаза 1 — Dual-write. Все записи уходят одновременно в Supabase и VPS. Source of truth — Supabase.
Код:
typescriptasync function createListing(data: ListingInput) {
const [supabaseResult, vpsResult] = await Promise.allSettled([
supabaseClient.from('listings').insert(data),
vpsPool.query('INSERT INTO listings ...', [...values])
]);
if (supabaseResult.status !== vpsResult.status) {
await logMigrationDivergence('listings', data.id);
}
return supabaseResult; // source of truth пока Supabase
}
[B]async[/B] [B]function[/B] createListing(data: ListingInput) { [B]const[/B] [supabaseResult, vpsResult] = [B]await[/B] Promise.allSettled([ supabaseClient.from('listings').insert(data), vpsPool.query('INSERT INTO listings ...', [...values]) ]); [B]if[/B] (supabaseResult.status !== vpsResult.status) { [B]await[/B] logMigrationDivergence('listings', data.id); } [B]return[/B] supabaseResult; [I]// source of truth пока Supabase[/I] } Фаза 2 — Compare-job. Фоновый процесс регулярно сравнивает данные в обеих базах. Расхождения логируются — это даёт уверенность что VPS не отстаёт.
Фаза 3 — Soak period. Несколько дней dual-write + мониторинг. Если расхождений нет — переходим дальше.
Фаза 4 — Переключение source of truth. Читаем из VPS, пишем в обе. Supabase получает данные "на всякий случай".
Фаза 5 — Rollback gate. 48 часов мониторинга. Если метрики ок — убираем дублирующую запись в Supabase. Один флаг возвращает всё назад за 30 секунд.
Результат: нулевой downtime, нулевая потеря данных.
Часть 2: Strangler pattern для Auth — переезд без принудительного разлогина
Почему Auth — самое страшное
Supabase Auth — это не просто таблица users. Это JWT-токены, magic links, RLS, активные сессии. "Выключить и включить свой" — все залогиненные пользователи получат 401 и потребуют перелогина. На проде — неприемлемо.
Решение: auth bridge
Идея strangler: новая система постепенно перехватывает трафик, старая умирает сама.
Шаг 1. Добавляем VPS session layer поверх Supabase Auth. Новые логины проходят через наш session manager, но валидация пароля делегируется в Supabase.
Код:
typescriptexport async function authenticateUser(email: string, password: string) {
// Валидация пароля через Supabase (временно)
const { data, error } = await supabase.auth.signInWithPassword({
email, password
});
if (error) throw new AuthError('Invalid credentials');
// Создаём свою VPS-сессию
const sessionToken = await createVpsSession({
userId: data.user.id,
email: data.user.email,
});
// Supabase токен не покидает сервер
return { sessionToken, user: mapToVpsUser(data.user) };
}
[B]export[/B] [B]async[/B] [B]function[/B] authenticateUser(email: string, password: string) { [I]// Валидация пароля через Supabase (временно)[/I] [B]const[/B] { data, error } = [B]await[/B] supabase.auth.signInWithPassword({ email, password }); [B]if[/B] (error) [B]throw[/B] [B]new[/B] AuthError('Invalid credentials'); [I]// Создаём свою VPS-сессию[/I] [B]const[/B] sessionToken = [B]await[/B] createVpsSession({ userId: data.user.id, email: data.user.email, }); [I]// Supabase токен не покидает сервер[/I] [B]return[/B] { sessionToken, user: mapToVpsUser(data.user) }; } Шаг 2. Middleware переходит на проверку VPS-сессий. Supabase JWT больше не попадает к клиенту.
Код:
typescriptexport async function middleware(request: NextRequest) {
const sessionToken = request.cookies.get('vps_session');
const session = await validateVpsSession(sessionToken?.value);
if (!session) {
return NextResponse.redirect('/login');
}
return NextResponse.next();
}
[B]export[/B] [B]async[/B] [B]function[/B] middleware(request: NextRequest) { [B]const[/B] sessionToken = request.cookies.get('vps_session'); [B]const[/B] session = [B]await[/B] validateVpsSession(sessionToken?.value); [B]if[/B] (!session) { [B]return[/B] NextResponse.redirect('/login'); } [B]return[/B] NextResponse.next(); } Шаг 3. Когда VPS-сессии стабильны — переносим хеши паролей, переключаем валидацию на bcrypt. Supabase Auth выключается.
Результат: ни один пользователь не получил 401. Bridge прожил 3 дня — потом тихо умер.
Часть 3: SSR read-path без Supabase — стабильный рендеринг и SEO
Проблема
Next.js SSR на каждый запрос должен отдать HTML с данными для индексации. Если SSR ходит в Supabase, а тот тормозит — страница не рендерится, поисковик видит пустой HTML.
Задача: убрать Supabase из SSR read-path полностью, не сломав sitemap и метаданные.
Решение: ReadRepository с переключаемым источником
Код:
typescriptexport class ReadRepository {
private source: 'supabase' | 'vps';
constructor() {
// Переключается env-флагом без редеплоя
this.source = process.env.READ_SOURCE as 'supabase' | 'vps';
}
async getListing(id: string): Promise<Listing> {
return this.source === 'vps'
? this.vpsGetListing(id)
: this.supabaseGetListing(id);
}
async getListingsForSitemap(): Promise<SitemapEntry[]> {
return this.source === 'vps'
? this.vpsGetSitemapEntries()
: this.supabaseGetSitemapEntries();
}
}
[B]export[/B] [B]class[/B] ReadRepository { [B]private[/B] source: 'supabase' | 'vps'; constructor() { [I]// Переключается env-флагом без редеплоя[/I] [B]this[/B].source = process.env.READ_SOURCE [B]as[/B] 'supabase' | 'vps'; } [B]async[/B] getListing(id: string): Promise<Listing> { [B]return[/B] [B]this[/B].source === 'vps' ? [B]this[/B].vpsGetListing(id) : [B]this[/B].supabaseGetListing(id); } [B]async[/B] getListingsForSitemap(): Promise<SitemapEntry[]> { [B]return[/B] [B]this[/B].source === 'vps' ? [B]this[/B].vpsGetSitemapEntries() : [B]this[/B].supabaseGetSitemapEntries(); } } Один env-флаг — переключение без редеплоя. Rollback за 30 секунд.
Код:
typescriptexport async function generateMetadata({ params }: ListingPageProps) {
const listing = await readRepository.getListing(params.id);
return {
title: listing.title,
description: listing.description,
openGraph: { images: [listing.mainImage] }
};
}
[B]export[/B] [B]async[/B] [B]function[/B] generateMetadata({ params }: ListingPageProps) { [B]const[/B] listing = [B]await[/B] readRepository.getListing(params.id); [B]return[/B] { title: listing.title, description: listing.description, openGraph: { images: [listing.mainImage] } }; } Результат: SSR latency снизился на 40ms (убрали сетевой хоп до Supabase). Sitemap читает напрямую из PostgreSQL. SEO-индексация без перебоев.
Итоги
Метрика | Результат |
|---|---|
Downtime при миграции БД | 0 |
Потеря данных | 0 |
Принудительные разлогины | 0 |
SSR latency | −40ms |
Lighthouse mobile | 94+ |
Ключевой вывод: большие миграции на проде — это не событие, а процесс. Dual-write + compare-job + rollback gate дают уверенность на каждом шаге.
Если интересно — в комментариях отвечу на вопросы по реализации.
Стек: Next.js 15 · TypeScript · PostgreSQL · Tailwind CSS · Vercel
Яков Радченко, Full-Stack разработчик