Мы уже разобрались с тем, что такое 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
Для своей альтернативы 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
Как я уже писал выше, для создания аналога Firebase нам понадобится четыре компонента и один вспомогательный инструмент:
-
PostgreSQL;
-
PostgREST;
-
Keycloak;
-
Node.js Realtime сервис;
-
pgAdmin (для работы с PostgreSQL, опционально, можно использовать свои решения).
Каждый из них без проблем и максимально быстро можно развернуть в сервисе Amvera. Сервис создан для быстрого развертывания IT-приложений и предоставляет managed PostgreSQL и преднастроенные сервисы PostgREST, Keycloak, pgAdmin и многие другие. То есть фактически вам не нужно будет задумываться о настройке сервера, фаерволла и прочей начальной рутины.
Первое время сервисом можно пользоваться бесплатно: сразу после регистрации вам будут начислены 111 рублей для тестов.
Amvera — это лишь один из сотен вариантов для развертывания каждого из компонентов, вы можете использовать любой. В случае, если вы будете развертывать компоненты на любом другом сервисе или локально — вы можете пропускать развертывание, специально для вас я вынес настройку в отдельный этап.
PostgreSQL
Начнём с самого важного: с базы данных. Как я написал выше: PostgreSQL предоставляется как managed-сервис.
Для создания СУБД в Amvera регистрируемся в сервисе, на главной странице ЛК открываем раздел “PostgreSQL” и жмём “Создать базу данных”.
В открывшемся окне выбираем имя проекта, тариф и объем хранилища, после чего жмем “Далее”. На втором этапе создания СУБД заполняем поля с кредами:
-
Имя создаваемой БД;
-
Имя пользователя;
-
Пароль пользователя;
-
Пароль для superuser;
-
при необходимости заполняем “Расширенные параметры”.
и завершаем создание.
Теперь, в течение 1-3 минут после завершения, СУБД будет полностью готова к работе.
pgAdmin
Для удобной работы с БД и SQL, я рекомендую использовать pgAdmin.
При необходимости его можно развернуть локально, на своем ПК, но для этого понадобится создавать внешнее доменное имя.
Сейчас необходимости в подключении извне нет, соответственно быстрее будет развернуть pgAdmin в Amvera.
Для этого, снова открываем главную страницу ЛК и на этот раз переходим в раздел “Преднастроенные сервисы” -> нажимаем кнопку “Создать преднастроенный сервис” и выбираем следующий сервис:
По аналогии с PostgreSQL, выбираем название проекта, тариф и жмём “Далее”. В следующем окне вводим креды для аккаунта администратора:
-
PGADMIN_DEFAULT_EMAIL— почта; -
PGADMIN_DEFAULT_PASSWORD— пароль; -
третий параметр лучше не трогать.
Завершаем создание, открываем и проект и переходим во вкладку “Домены”. Так как pgAdmin — это не один из необходимых компонентов, которые будут общаться друг с другом по внутренней сети, а вспомогательный инструмент для работы с базой данных, нам необходим доступ извне.
В Amvera есть возможность создать бесплатное внешнее доменное имя с HTTPS. Обычно, для преднастроенных проектов внешний домен создается автоматически, но и ручное создание совсем не хитрое:
Если проект собрался, а домен открывает интерфейс pgAdmin, мы можем переходить к следующему компоненту — PostgREST.
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
Не уверен, нужно ли рассказывать, что такое 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— внутренний домен СУБД.
Настройка компонентов нашей альтернативы Firebase
Теперь нам необходимо связать все компоненты.
1.0 Keycloak
Для начала открываем интерфейс Keycloak по полученной нами ранее ссылке и заходим в аккаунт временного администратора, креды которого мы указывали при создании проекта.
1.1 Настройка Realm
В левом верхнем углу открываем список Realm и создаем новый с другим именем. Например: ourbaas.
Жмём “Create” и ожидаем создания. После успешного создания realm, вы автоматически переместитесь в него.
1.2 Создание клиента для фронтенда
Теперь создадим 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.
1.3 Доп. настройка для PostgREST
Изначально, Keycloak не возвращает необходимое нам поле role в JWT токене, поэтому нам необходимо его добавить вручную.
Делается это не сложно.
Открываем созданный ранее клиент и переходим в раздел Client scopes -> ourbaas-web-dedicated.
В открывшейся вкладке “Mappers” жмём “Configure a new mapper” и ищем “Hardcoded claim”.
Теперь выбираем название claim’а — можно указать любое, название поля role и его захаркоженое значение authenticated. Claim JSON Type необходимо указать в значении String
1.4 Создание тестового пользователя
Создадим тестового пользователя.
Переходим в Users -> Create new user, выбираем произвольный Username и обязательно жмём Email verified вверху.
После того, как User будет создан, откройте вкладку “Credentials” юзера и создайте новый пароль. При этом обязательно нужно убрать Temporary.
Если все сделали — пользователь готов.
1.5 Отключаем обязательные user actions
В левом блоке навигации, внизу ищем раздел “Auhentication”, открываем его и переходим во вкладку “Required actions”. Пока мы только тестим BaaS, можно просто отключить все Actions как на скриншоте ниже. При необходимости можете вернуть нужные вам действия.
Все, на этом настройка Keycloak завершена.
2.0 PostgREST
Первое и самое важное действие, которое нужно сейчас сделать — это научить PostgREST проверять токены.
2.1 Проверка токенов
Изначально 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 поменяются ключи подписи, то и сам файлик тоже нужно будет обновить.
3.0 PostgreSQL
Теперь мы переходим к самому важному: настройке базы.
Открываем ранее созданный pgAdmin, создаем подключение к нашей PostgreSQL и открываем Query tool под superuser.
3.1 Роли
Сначала создадим ранееописанные роли. Делается это всего в несколько SQL-запросов:
create role anon nologin;create role authenticated nologin;
Теперь необходимо разрешить пользователю, под которым подключается PostgREST, переключать эти роли:
grant anon to "<имя пользователя>";grant authenticated to "<имя пользователя>";
3.2 Таблица messages
Теперь создадим таблицу сообщений.
Эта таблица создается лишь для примера, вы в реальном проекте можете использовать совершенно любую таблицу с практически любой структурой.
Для генерации 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).
3.3 Права для ролей
Теперь выдаем права для ролей:
grant usage on schema public to anon, authenticated;grant select, insert, update, delete on public.messages to authenticated;
Для роли anon права специально не выдаем.
3.4 Включение безопасности на уровне строк
Для этого вводим следующий запрос:
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.messagesfor selectto authenticatedusing ( user_id = ( (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')::uuid ));
INSERT:
create policy "messages_insert_own"on public.messagesfor insertto authenticatedwith 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.messagesfor updateto authenticatedusing ( 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.messagesfor deleteto authenticatedusing ( user_id = ( (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')::uuid ));
4.0 Realtime
Базовый CRUD (Create, Read, Update, Delete) у нас уже есть. Теперь добавим Realtime.
Это будет работать так: в PostgreSQL мы создадим триггер на изменения в таблице, который будет отправлять notify, который Node.JS Gateway будет ловить и отправлять фронтенду по WebSocket соединению.
4.1 Функция для отправки событий
Выполняем в pgAdmin следующий SQL:
create or replace function public.notify_messages_changes()returns triggerlanguage plpgsqlas $$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;$$;
4.2 Триггер для таблицы messages
Теперь создадим триггер для таблицы:
create trigger messages_realtime_triggerafter insert or update or deleteon public.messagesfor each rowexecute function public.notify_messages_changes();
Теперь любое изменение в таблице будет отправлять событие в канал realtime_changes, на который можно будет подписаться в Gateway.
5.0 Node.js Realtime сервис
Если вы можете обойтись без 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— <пароль-БД>
Помимо этого, нужно будет создать внешнее доменное имя во вкладке “Домены”.
6.0 JavaScript SDK
Теперь нужно сделать один файл 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", }, });
6.1 Небольшая справка по SDK
Перед тем, как авторизовать пользователя, необходимо инициализировать 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
По итогам статьи мы получили Firebase-подобный BaaS, который позволяет работать с CRUD и получением realtime обновлений прям в коде фронтенда.
Дальше эту систему можно развивать в любом удобном для вас направлении.
ссылка на оригинал статьи https://habr.com/ru/articles/1050034/