Вы сделали приложение на Next.js. В нём есть каталог событий и кнопка «В избранное». Пока список хранится в браузере, всё работает. Но после входа с другого устройства сохранённые события не появляются.
Чтобы данные были доступны между устройствами, понадобится серверное хранилище. Можно разработать отдельный backend, подключить базу данных и написать API для каждого действия. А можно использовать Supabase, где база данных и API для работы с ней уже предоставляются как сервис. Разберём второй вариант на примере избранных событий.
Что Supabase берёт на себя
Supabase предоставляет базу PostgreSQL и интерфейс для работы с её таблицами. Приложение может читать и записывать данные через клиентскую библиотеку, не создавая собственный REST endpoint для каждого запроса. В сервисе также есть авторизация, хранение файлов и другие возможности. Но наличие готовых инструментов не отменяет проектирование данных и проверку доступа. Представим приложение с событиями. У каждого пользователя есть собственный список избранного. Нам нужна таблица, в которой одна запись связывает пользователя и выбранное событие.
Создаём таблицу избранного
Откройте SQL Editor в проекте Supabase. Создадим таблицу favorites с идентификатором пользователя и события.
create table public.favorites (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id),
event_id text not null,
created_at timestamptz not null default now(),
unique (user_id, event_id)
);
user_id указывает на учётную запись в Supabase Auth, а event_id хранит идентификатор события из каталога. Ограничение unique не позволяет одному пользователю добавить одно событие дважды. Не сохраняем в этой таблице название события, описание и дату проведения. Для начала достаточно идентификатора, по которому приложение сможет найти событие в основном каталоге.
Теперь нужно определить, кто вправе обращаться к этим записям.
Почему нельзя пропускать RLS
Если frontend работает напрямую с базой через клиент Supabase, проверка прав доступа должна выполняться на стороне базы данных. Недостаточно скрыть чужие записи в интерфейсе или добавить фильтр по текущему пользователю в JavaScript. Для этого в PostgreSQL используется Row Level Security, или RLS. Правила RLS определяют, какие строки разрешено читать и изменять.
Включим защиту таблицы и добавим политики для чтения, создания и удаления своих записей.
alter table public.favorites enable row level security;
create policy "Read own favorites"
on public.favorites
for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Add own favorites"
on public.favorites
for insert
to authenticated
with check ((select auth.uid()) = user_id);
create policy "Delete own favorites"
on public.favorites
for delete
to authenticated
using ((select auth.uid()) = user_id);
auth.uid() возвращает идентификатор пользователя из проверенного контекста авторизации. Когда пользователь не вошёл в систему, он не соответствует роли authenticated и эти политики не дают ему доступа. После этого один пользователь не сможет получить чужие записи простым изменением user_id в запросе. При условии, что приложение использует обычный клиентский ключ и не обходит RLS привилегированным серверным доступом.
Для обновления записей понадобилась бы отдельная политика. В нашем примере избранное можно только добавлять, читать и удалять.
Подключаем клиент Supabase
Для работы понадобятся адрес проекта и его publishable key. Их можно хранить в переменных окружения Next.js.
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
Публичный ключ не является секретным серверным ключом. Его можно использовать в браузере, потому что доступ к пользовательским строкам должен ограничиваться RLS.
Сервисный ключ с привилегированными правами нельзя передавать клиентскому приложению. Он способен обходить RLS и предназначен для отдельных доверенных серверных задач.
Установим библиотеку Supabase.
npm install @supabase/supabase-js
Для следующего примера предполагается, что клиент supabase уже создан и пользователь вошёл в приложение. Настройка входа через email отдельно разобрана в статье «Как добавить авторизацию в Next.js через Supabase».
Сохраняем событие в избранное
Представим, что пользователь нажал кнопку рядом с событием. Мы знаем его идентификатор и хотим добавить запись в таблицу.
async function addFavorite(
supabase: SupabaseClient,
eventId: string
) {
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
throw new Error("Сначала войдите в аккаунт");
}
const { error } = await supabase.from("favorites").insert({
user_id: user.id,
event_id: eventId,
});
if (error) {
throw new Error("Не удалось сохранить событие");
}
}
SupabaseClient импортируется из @supabase/supabase-js, а в функцию передаётся настроенный клиент с пользовательской сессией. Сначала приложение получает текущего пользователя, затем отправляет запрос на создание записи. Но окончательное решение о доступе принимает не функция JavaScript, а политика RLS. Если пользователь попытается подставить чужой user_id, база отклонит запись. Если событие уже сохранено, ограничение unique не позволит создать дубль.
В рабочем интерфейсе нужно отдельно обработать случай повторного добавления и показать пользователю актуальное состояние кнопки.
Читаем данные после повторного входа
Когда человек открывает приложение на другом устройстве, список избранного можно запросить из той же таблицы.
const { data, error } = await supabase
.from("favorites")
.select("event_id");
if (error) {
throw new Error("Не удалось загрузить избранное");
}
Здесь запрос не содержит фильтра по user_id. Тем не менее при правильно настроенной пользовательской сессии и действующей политике RLS база вернёт только разрешённые строки. В интерфейсе можно дополнительно фильтровать данные по конкретному пользователю, но такой фильтр не должен считаться механизмом защиты.
Полученные идентификаторы событий затем связываются с данными каталога. Для внешнего API иногда нужна дополнительная загрузка и обработка событий, которых больше нет в источнике.
Когда собственного backend всё-таки не хватает
Supabase позволяет не писать отдельный сервер для простых операций с таблицами. Но это не означает, что любой код приложения можно перенести в браузер. Если нужно обратиться к платному внешнему API с секретным ключом, выполнить сложную бизнес-операцию или проверить webhook, для этого потребуется доверенная серверная часть. В Next.js такую задачу могут выполнять Route Handlers или серверные функции. Другой вариант отдельный backend на Django.
В моей статье на Habr о Route Handlers в Next.js разобрано, какие серверные задачи можно решать внутри App Router. Здесь Supabase отвечает за хранение и контролируемый доступ к данным, а не за всю логику приложения.
От таблицы к пользовательскому продукту
В демонстрационном EventMap поиск событий сочетается с авторизацией и облачным избранным. Пользователь может сохранять интересующие события и получать список после входа в аккаунт.
Курс «Next.js III: SEO, i18n 2026» последовательно связывает внешний API, серверный поиск, Supabase Auth, RLS и пользовательские данные в проекте на Next.js.
Начать можно с одной таблицы и трёх операций. Сохранить событие, получить избранное и удалить запись. Если права доступа проверяются в базе, такой функциональности уже достаточно для следующего этапа развития приложения.