Мы уже разобрались с тем, что такое BaaS, почему появились Firebase и Supabase, чем они отличаются от обычного backend и можно ли самому собрать что-то похожее.
Как я и говорил в прошлой статье, сегодня мы наконец попробуем самостоятельно собрать альтернативу Firebase с Realtime исключительно на open-source компонентах!
Но важная оговорка: мы не будем делать полный клон Firebase с его тонной функционала, но мы соберем минимальную рабочую альтернативу, которую уже можно подключить к frontend через SDK и использовать почти как Firebase.
По итогам статьи у нас будет:
Авторизация;
База данных;
CRUD;
Безопасность на уровне строк (RLS, пользователи могут получать/изменять только собственные строки);
Realtime обновления;
Собственный JS SDK.
Подключение и работа на фронтенде будет выглядеть примерно так:
const ourBaas = createBaasClient({
apiUrl: "https://<название-проекта>.<имя-пользователя>.amvera.io",
realtimeUrl: "wss://<название-проекта>.<имя-пользователя>.amvera.io/realtime",
keycloak: {
url: "https://<название-проекта>.<имя-пользователя>.amvera.io",
realm: "ourbaas",
clientId: "ourbaas-web",
},
})
await ourBaas.auth.init()
await ourBaas.auth.login() // Авторизация
const {data} = await ourBaas.from("messages").select("*").get() // Получение данных
await ourBaas.from("messages").insert({title: "Какой-то текст"}).single() // Ввод данных (INSERT)
ourBaas.channel("messages").on("INSERT", payload => {
console.log("Новое сообщение:", payload.new)
}).subscribe() // Подписка на обновления
Это все - лишь учебная Firebase-подобная система, а не полный клон Firebase.
Для своей альтернативы Firebase я взял такой стек:
PostgreSQL - основная база данных с RLS;
Keycloak - авторизация;
PostgREST - REST API поверх PostgreSQL;
Realtime через LISTEN/NOTIFY в PostgreSQL;
JavaScript SDK и небольшой Realtime-сервис.
Пользователь логинится через Keycloak (допустим, какая-то кнопка на фронте вызывает await ourBaas.auth.login()), после чего Keycloak возвращает специальный JWT токен. Этот токен мы куда-то сохраняем и впоследствии используем для общения с PostgREST. Фактически мы этот токен просто подставляем в Authorization заголовок.
PostgREST, получая запрос от нас с JWT токеном, этот токен “проверяет” и если все окей - ходит в PostgreSQL как авторизованный пользователь, при этом передавая дальше JWT.
В PostgreSQL мы настроим RLS Policy (политику безопасности на уровне строк), которая будет проверять JWT токен, доставать оттуда UUID и возвращать только те строки, которые безоговорочно принадлежат именно этому UUID.
Получается, безопасность будет не в SDK, не в нашем коде, а в самой базе данных.
Если сейчас что-то для вас непонятно, то, во-первых, убедитесь что вы прочитали прошлую статью, во-вторых, это не страшно: по ходу статьи я буду объяснять роль каждого из компонентов.
Как я уже писал выше, для создания аналога Firebase нам понадобится четыре компонента и один вспомогательный инструмент:
PostgreSQL;
PostgREST;
Keycloak;
Node.js Realtime сервис;
pgAdmin (для работы с PostgreSQL, опционально, можно использовать свои решения).
Каждый из них без проблем и максимально быстро можно развернуть в сервисе Amvera. Сервис создан для быстрого развертывания IT-приложений и предоставляет managed PostgreSQL и преднастроенные сервисы PostgREST, Keycloak, pgAdmin и многие другие. То есть фактически вам не нужно будет задумываться о настройке сервера, фаерволла и прочей начальной рутины.
Первое время сервисом можно пользоваться бесплатно: сразу после регистрации вам будут начислены 111 рублей для тестов.
Amvera - это лишь один из сотен вариантов для развертывания каждого из компонентов, вы можете использовать любой. В случае, если вы будете развертывать компоненты на любом другом сервисе или локально - вы можете пропускать развертывание, специально для вас я вынес настройку в отдельный этап.
Начнём с самого важного: с базы данных. Как я написал выше: PostgreSQL предоставляется как managed-сервис.
Для создания СУБД в Amvera регистрируемся в сервисе, на главной странице ЛК открываем раздел “PostgreSQL” и жмём “Создать базу данных”.


В открывшемся окне выбираем имя проекта, тариф и объем хранилища, после чего жмем “Далее”. На втором этапе создания СУБД заполняем поля с кредами:
Имя создаваемой БД;
Имя пользователя;
Пароль пользователя;
Пароль для superuser;
при необходимости заполняем “Расширенные параметры”.
и завершаем создание.
Теперь, в течение 1-3 минут после завершения, СУБД будет полностью готова к работе.
Для удобной работы с БД и SQL, я рекомендую использовать pgAdmin.
При необходимости его можно развернуть локально, на своем ПК, но для этого понадобится создавать внешнее доменное имя.
Сейчас необходимости в подключении извне нет, соответственно быстрее будет развернуть pgAdmin в Amvera.
Для этого, снова открываем главную страницу ЛК и на этот раз переходим в раздел “Преднастроенные сервисы” -> нажимаем кнопку “Создать преднастроенный сервис” и выбираем следующий сервис:

По аналогии с PostgreSQL, выбираем название проекта, тариф и жмём “Далее”. В следующем окне вводим креды для аккаунта администратора:
PGADMIN_DEFAULT_EMAIL - почта;
PGADMIN_DEFAULT_PASSWORD - пароль;
третий параметр лучше не трогать.
Завершаем создание, открываем и проект и переходим во вкладку “Домены”. Так как pgAdmin - это не один из необходимых компонентов, которые будут общаться друг с другом по внутренней сети, а вспомогательный инструмент для работы с базой данных, нам необходим доступ извне.
В Amvera есть возможность создать бесплатное внешнее доменное имя с HTTPS. Обычно, для преднастроенных проектов внешний домен создается автоматически, но и ручное создание совсем не хитрое:

Если проект собрался, а домен открывает интерфейс pgAdmin, мы можем переходить к следующему компоненту - PostgREST.
PostgREST - это сервис, который создает Rest API для работы с вашей базой данных PostgreSQL.
Если говорить просто, PostgREST смотрит на таблицы, функции, права и прочее в PostgreSQL, а потом отдает поверх них HTTP API.
Для развертывания снова открываем раздел “Преднастроенные сервисы”, нажимаем “Создать преднастроенный сервис” и выбираем PostgREST:

На этапе конфигурации понадобится прописать данные для подключения к БД и анонимную роль:
PGUSER - имя пользователя;
PGPASSWORD - пароль от БД;
PGHOST - внутреннее доменное имя PostgreSQL, которое можно получить во вкладке “Инфо” самой СУБД. Выглядит примерно так: amvera-<имя-пользователя>-cnpg-<название-проекта>-rw для региона Москва.
PGDATABASE - название БД;
PGRST_DB_ANON_ROLE - самое важное. Пока напишем anon.
Так же, как и в случае с pgAdmin, внешнее доменное имя будет автоматически создано во вкладке “Домены”.
Итак, что за anon и зачем вообще нужны роли?
Роль в PostgreSQL - это сущность, у которой есть какие-то права. Ей можно разрешить читать таблицу, запретить вставку, разрешить выполнение какой-то функции и так далее.
В нашем сценарии можно выделить две роли: anon и authenticated (все просто: пользователь может быть в аккаунте, а может не быть - третьего не дано).
Фактически мы можем выдать анонимной роли любые права, но, опять же, для нашего сценария у нее не должно быть прав, так как неавторизованный пользователь не должен видеть контент сайта.
Это работает просто: если PostgREST получил JWT токен и он валидный, то он ищет в в нем поле role, например:
{
"role": "authenticated"
}
Если же токена нет, он невалидный или поля role в токене нет - PostgREST работает с БД с анонимной ролью.
Не уверен, нужно ли рассказывать, что такое Keycloak. Но если совсем кратко, это сервис, который в нашем случае будет отвечать за пользователей: регистрацию, вход, выход, их хранение и выдачу JWT токенов.
Keycloak можно развернуть по аналогии с другими преднастроенными сервисами.

Из настроек тут тоже ничего необычного:
KC_BOOTSTRAP_ADMIN_USERNAME и KC_BOOTSTRAP_ADMIN_PASSWORD - креды для временного аккаунта администратора;
KB_DB_URL_HOST, KC_DB_USERNAME и KC_DB_URL_DATABASE - креды для подключения к БД;
KC_DB_URL_HOST - внутренний домен СУБД.
Теперь нам необходимо связать все компоненты.
Для начала открываем интерфейс Keycloak по полученной нами ранее ссылке и заходим в аккаунт временного администратора, креды которого мы указывали при создании проекта.
В левом верхнем углу открываем список Realm и создаем новый с другим именем. Например: ourbaas.

Жмём “Create” и ожидаем создания. После успешного создания realm, вы автоматически переместитесь в него.
Теперь создадим client, через который frontend будет логинить пользователей.
В левом блоке навигации жмём “Clients” --> “Create client” и поэтапно вводим данные.
1. General settings:
Client ID - вводим название клиента, например ourbaas-web;
при желании заполняем Name и Description.
2. Capability config:
Client authentication - Off;
Authorization - Off;
Authentication оставляем как есть.
3. Login settings: Здесь важно указать адреса, на которые Keycloak сможет возвращать пользователя после логина.
Для локальной разработки можно просто добавить локальный адрес фронтенда в виде: http://localhost:порт/*, а если уже известен внешний домен фронтенда, то добавляем и его: http://<домен>/*
Звездочки нужно добавлять во все параметры, кроме Web Origins.
Изначально, Keycloak не возвращает необходимое нам поле role в JWT токене, поэтому нам необходимо его добавить вручную.
Делается это не сложно.
Открываем созданный ранее клиент и переходим в раздел Client scopes -> ourbaas-web-dedicated.

В открывшейся вкладке “Mappers” жмём “Configure a new mapper” и ищем “Hardcoded claim”.

Теперь выбираем название claim’а - можно указать любое, название поля role и его захаркоженое значение authenticated. Claim JSON Type необходимо указать в значении String

Создадим тестового пользователя.
Переходим в Users -> Create new user, выбираем произвольный Username и обязательно жмём Email verified вверху.
После того, как User будет создан, откройте вкладку “Credentials” юзера и создайте новый пароль. При этом обязательно нужно убрать Temporary.
Если все сделали - пользователь готов.
В левом блоке навигации, внизу ищем раздел “Auhentication”, открываем его и переходим во вкладку “Required actions”. Пока мы только тестим BaaS, можно просто отключить все Actions как на скриншоте ниже. При необходимости можете вернуть нужные вам действия.

Все, на этом настройка Keycloak завершена.
Первое и самое важное действие, которое нужно сейчас сделать - это научить PostgREST проверять токены.
Изначально Keycloak подписывает все токены приватным ключом. Проверить подпись можно публичным ключом.
Этот ключ Keycloak отдает в формате JWKS и мы можем получить его прям в браузере. Для этого откройте в браузере: https://<домен-Keycloak>/realms/<ваш-реалм>/protocol/openid-connect/certs.
Вывод в JSON необходимо скопировать полностью и добавить в значение переменной окружения PGRST_JWT_SECRET.
Обычно это не проблема, если вы разворачиваете PostgREST где-нибудь на VPS. Но на других сервисах, как Amvera, при создании переменной необходимо учитывать ограничения ввода.
Так, например, мы не сможем вставить всю строку из-за запрета ввода кавычек в значениях переменных.
Но и это можно обойти довольно легко: можно просто сохранить весь вывод JSON в отдельный файлик и заставить PostgREST его читать.
Просто сохраняем значение в условный key.json и загружаем в Data во вкладке “Репозиторий” проекта PostgREST.

После корректной загрузки переходим во вкладку “Переменные” и добавляем/изменяем переменную PGRST_JWT_SECRET на следующее значение: @/data/key.json.
Важно учитывать, что если в Keycloak поменяются ключи подписи, то и сам файлик тоже нужно будет обновить.
Теперь мы переходим к самому важному: настройке базы.
Открываем ранее созданный pgAdmin, создаем подключение к нашей PostgreSQL и открываем Query tool под superuser.
Сначала создадим ранееописанные роли. Делается это всего в несколько SQL-запросов:
create role anon nologin;
create role authenticated nologin;
Теперь необходимо разрешить пользователю, под которым подключается PostgREST, переключать эти роли:
grant anon to "<имя пользователя>";
grant authenticated to "<имя пользователя>";
Теперь создадим таблицу сообщений.
Эта таблица создается лишь для примера, вы в реальном проекте можете использовать совершенно любую таблицу с практически любой структурой.
Для генерации UUID включим расширение pgcrypto:
create extension if not exists pgcrypto;
Создадим простенькую таблицу:
create table if not exists public.messages (
id uuid primary key default gen_random_uuid(),
user_id uuid not null default (
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')::uuid
),
title text not null,
created_at timestamptz not null default now()
);
Тут важное поле - user_id. Вместо того, чтобы передавать его с фронтенда руками, PostgreSQL сам достанет ID пользователя Keycloak из JWT токена.
PostgREST внутри запроса кладет JWT claims в специальную настройку: current_setting('request.jwt.claims', true).
Теперь выдаем права для ролей:
grant usage on schema public to anon, authenticated;
grant select, insert, update, delete on public.messages to authenticated;Для роли anon права специально не выдаем.
Для этого вводим следующий запрос:
alter table public.messages enable row level security;И создаем политики для каждого действия.
Policy в PostgreSQL - это правила, которым обязана следовать определенная роль. Например: можно создать политику, которая будет разрешать делать SELECT с таблицы public.messages только для тех строк, где user_id строго равно ID из запроса.
SELECT:
create policy "select only own rows"
on public.messages
for select
to authenticated
using (
user_id = (
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')::uuid
)
);
INSERT:
create policy "messages_insert_own"
on public.messages
for insert
to authenticated
with check (
user_id = (
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')::uuid
)
);
Важно: если вы смотрели внимательно, в SELECT используется using для проверки, а в INSERT уже with check.
Если коротко, using проверяет уже существующие строки, а with check проверяет новые данные, которые пытаются записать.
UPDATE:
create policy "messages_update_own"
on public.messages
for update
to authenticated
using (
user_id = (
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')::uuid
)
)
with check (
user_id = (
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')::uuid
)
);
DELETE:
create policy "messages_delete_own"
on public.messages
for delete
to authenticated
using (
user_id = (
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')::uuid
)
);
Базовый CRUD (Create, Read, Update, Delete) у нас уже есть. Теперь добавим Realtime.
Это будет работать так: в PostgreSQL мы создадим триггер на изменения в таблице, который будет отправлять notify, который Node.JS Gateway будет ловить и отправлять фронтенду по WebSocket соединению.
Выполняем в pgAdmin следующий SQL:
create or replace function public.notify_messages_changes()
returns trigger
language plpgsql
as $$
declare
payload jsonb;
begin
payload = jsonb_build_object(
'schema', TG_TABLE_SCHEMA,
'table', TG_TABLE_NAME,
'event', TG_OP,
'old', case
when TG_OP in ('UPDATE', 'DELETE') then to_jsonb(OLD)
else null
end,
'new', case
when TG_OP in ('INSERT', 'UPDATE') then to_jsonb(NEW)
else null
end
);
perform pg_notify('realtime_changes', payload::text);
return coalesce(NEW, OLD);
end;
$$;
Теперь создадим триггер для таблицы:
create trigger messages_realtime_trigger
after insert or update or delete
on public.messages
for each row
execute function public.notify_messages_changes();
Теперь любое изменение в таблице будет отправлять событие в канал realtime_changes, на который можно будет подписаться в Gateway.
Если вы можете обойтись без Realtime - этим сервисом можно пренебречь.
Он нужен только из-за того, что для нашего Realtime необходимо подключение к PostgreSQL, для которого соответственно нужны креды, которые мы не имеем права распространять в коде фронтенда или любых компонентов, которые можно прочитать с браузера.
Полностью код Realtime сервиса будет доступен на GitHub по ссылке, вам лишь необходимо скачать файлы и загрузить все три файла (package.json, server.js, amvera.yml в репозиторий нового проекта. В отличие от остальных компонентов, этот проект нужно создать как обычное приложение, не преднастроенное).
После загрузки кода создайте новые переменные окружения во вкладке “Переменные”:
KEYCLOAK_URL - https://<внешний-домен-Keycloak>
KEYCLOAK_REALM - ourbaas
PGHOST - <внутренний-домен-PostgreSQL>
PGPORT - 5432
PGDATABASE - <название-БД>
PGUSER - <пользователь-БД>
PGPASSWORD - <пароль-БД>
Помимо этого, нужно будет создать внешнее доменное имя во вкладке “Домены”.
Теперь нужно сделать один файл SDK для фронтенда.
Вообще, прямой необходимости в нем нет: он нужен для того, чтобы создать тот красивый код, который был предоставлен в начале. Фактически это просто очередная прослойка.
Код будет также доступен на GitHub по ссылке. Его нужно будет положить в проект и подключать так:
import {createBaasClient} from "./ourbaas.js";
А создавать клиент:
const ourBaas = createBaasClient({
apiUrl: "https://postgrestt-testamvera.amvera.io",
realtimeUrl: "wss://ourbaas-realtime-testamvera.amvera.io/realtime",
keycloak: {
url: "https://keycloackk-testamvera.amvera.io",
realm: "ourbaas",
clientId: "ourbaas-web",
},
});
Перед тем, как авторизовать пользователя, необходимо инициализировать Keycloak:
await ourBaas.auth.init();
После этого можно проверять, авторизован ли пользователь:
if (!ourBaas.auth.keycloak.authenticated) {
await ourBaas.auth.login();
}
Выход:
await ourBaas.auth.logout();
Получить ID текущего пользователя можно так:
const userId = ourBaas.auth.userId();
Теперь получение данных из таблицы:
const {data} = await ourBaas
.from("messages")
.select("*")
.get();
С сортировкой:
const {data} = await ourBaas
.from("messages")
.select("*")
.order("created_at", false)
.get();
Здесь false означает сортировку по убыванию, т.е. сначала будут новые сообщения.
Добавление строки:
const {data} = await ourBaas
.from("messages")
.insert({
title: "Привет!",
})
.single();
Обновление строки:
const {data} = await ourBaas
.from("messages")
.update({
title: "Обновленный текст",
})
.eq("id", "ID_СООБЩЕНИЯ")
.single();
Удаление строки:
const {data} = await ourBaas
.from("messages")
.delete()
.eq("id", "ID_СООБЩЕНИЯ")
.single();
И realtime-подписка:
const channel = ourBaas
.channel("messages")
.on("INSERT", payload => {
console.log("Новое сообщение:", payload.new);
})
.on("UPDATE", payload => {
console.log("Сообщение обновлено:", payload.new);
})
.on("DELETE", payload => {
console.log("Сообщение удалено:", payload.old);
})
.subscribe();
Отписаться можно так:
channel.unsubscribe();
В итоге работа с нашим BaaS на фронтенде выглядит примерно так:
await ourBaas.auth.init();
if (!ourBaas.auth.keycloak.authenticated) {
await ourBaas.auth.login();
}
await ourBaas
.from("messages")
.insert({title: "Работает!"})
.single();
ourBaas
.channel("messages")
.on("INSERT", payload => {
console.log(payload.new);
})
.subscribe();
Это, конечно, не полноценный аналог Firebase со всеми его функциями, но уже похоже, а главное - наша замена Firebase работает!
По итогам статьи мы получили Firebase-подобный BaaS, который позволяет работать с CRUD и получением realtime обновлений прям в коде фронтенда.
Дальше эту систему можно развивать в любом удобном для вас направлении.