Convex — альтернатива Firebase и Supabase
Всем привет!
В этой статье я расскажу про Convex — платформа для бэкенда.
На данный момент Convex нативно доступен на
JavaScript/TypeScript (React, React Native, Next, Node)
Python
Rust
Благодаря тому, что поддерживается Node, вероятнее всего, можно использовать его на разных фреймворках и библиотеках
История появления
В апреле 2022 года несколько разработчиков из Dropbox решили разработать свой масштабируемый сервис для бэкенда. В Dropbox они занимались переносом миллиардов гигабайт пользовательских файлов из облака Amazon во внутреннюю систему, которую они построили. В раунде финансирования при участии Netlify и Heo они смогли привлечь 25 миллионов долларов на развитие стартапа. (Источник)
Основатели Convex
Что такое Convex?
Convex — это платформа для создания бэкенда. В нее входит:
Server Functions
ACID Database
Vector Search
Scheduling and crons
File Storage
Все в Convex работает в режиме реального времени. Также написаны удобные интерфейсы для работы с Convex на React.
Сравнение с реляционными базами данных
Источник — статья от разработчиков Convex
Язык запросов
Запросы к базе данных Convex полностью написаны на Typescript, что избавляет от необходимости построения SQL-запросов или использования громоздких ORM систем.
Помимо этого индексированные запросы к базе данных Convex обязывают явно указывать, какой индекс необходимо использовать для запроса. Это отличается от планировщиков запросов в СУБД (например PostgreSQL), где СУБД пытается автоматически решить, какой индекс использовать, что иногда вызывает неожиданные результаты.
Кэширование
Для реализации кэширования в реляционных базах данных используются дополнительное хранилище данных, например, Redis или Memcached. Для разработчика в таком случае необходимо реализовать синхронизацию данных между реляционной базой данных и хранилищем кэша, что подвержено ошибкам.
В Convex кэширование полностью автоматическое. Он кэширует результаты ваших функций запроса и пересчитывает значения при изменении базовых данных.
Данные в реальном времени
Для получений данных в реальном времени в реляционных базах данных можно использовать два варианта:
Регулярный опрос сервера на наличие изменений. Проблема заключается в том, что частые опросы негативно влияют на сервер и могут привести к перегрузам базы данных или сервера.
Распространение обновлений. Для этого необходимо использовать кластеры или встроенные возможности Pub/Sub, а также соединение через WebSocket для передачи данных. Внедрение обновлений в реальном времени — это огромная задача, многие веб-разработчики даже не пытаются реализовать это.
В Convex все запросы по умолчанию являются реактивными, то есть передают данные в реальном времени. Для этого они используют систему, основанную на соединение через WebSocket. Convex понимает, какие функции запроса зависят от каких данных. Когда данные изменяются, Convex повторно запускает нужные функции и передает полученные данные.
Основная идея
Основная идея разработчиков Convex заключается в том, чтобы облегчить построение масштабируемых приложений без использования сложных и громоздких инструментов таких, как PostgreSQL, Redis и так далее.
Convex берет на себя заботу о серверах, кэшировании и реактивности, позволяя разработчикам сфокусироваться на продукте.
Сравнение с Firebase Cloud Firestore
Источник — статья разработчиков Convex
Convex и Firebase Cloud Firestore очень схожи между собой по функционалу:
Обе платформы хранят данные в определенном формате
Уведомляют об изменениях данных в реальном времени
Не требуют веб-разработчикам управлять инфраструктурой сервера
Но также есть множество отличий во внутренностях работы Convex и Firestore
Документы или функции?
В Firestore используются документы. Клиент взаимодействует со своими данными, загружая документы из базы данных. В Convex же используются функции, которые при необходимости возвращают данные. Напрямую изменить и получить информацию из базы данных нельзя.
Этот дополнительный уровень (функции) позволяет решить две проблемы:
Водопад последовательных запросов
Он возникает, когда для загрузки всех данных необходимо совершить несколько последовательных запросов на сервер. Это ухудшает производительность загрузки страницы.
Например, вот так можно получить всех пользователей, отправили сообщение в Firestore
const querySnapshot = await getDocs(collection(db, "messages"));
const userSnapshots = await Promise.all(
querySnapshot.docs().map(async messageSnapshot => {
return await getDoc(docSnapshot.data().creator);
})
);
Дело в том, что при таком коде будет совершено несколько последовательных запросов к базе данных Firestore. Каждый запрос будет идти отдельно.
В Convex же можно реализовать функцию, которая внутри получит все сообщение и всех пользователей, которые отправили сообщение.
export const list = query(async (ctx) => {
const messages = await ctx.db.query("messages").collect();
return Promise.all(
messages.map(async (message) => {
const user = await ctx.db.get(message.user);
return {
author: user!.name,
...message,
};
}),
);
});
В итоге при использовании Convex мы отправим на сервер один запрос, который вернет нам уже собранные в нужном нам варианте данные.
В Firestore можно избежать водопад запросов с помощью облачных функций, но облачные функции не поддерживают передачу данных в реальном времени.
Бизнес-логика
Функции в Convex служат естественным методом для размещения бизнес-логики. Как разработчик, вы можете использовать логику между всеми своими платформами.
Реактивность
Convex был разработан для использования с React, поэтому были разработаны удобные React хуки, которые позволяют загружать и редактировать данные.
Firestore не был разработан для использования с React. У него имеется JavaScript SDK, но передача данных в React приложениях остается на усмотрение разработчика, что создает проблемы и отличия между разными приложениями, реализованные с помощью Firestore.
Также, как это было описано выше, Convex функции по умолчанию реактивны, в отличие от Firestore, где не все методы получают данные в реальном времени.
Сравнение с Supabase
Источник — статья разработчиков Convex
И Convex, и Supabase:
Основаны на хранилищах данных, совместимых с ACID.
Интеграция с большинством современных TS/JS-фреймворков.
Предоставить пользовательский интерфейс редактора базы данных в браузере.
Поставляется с хранилищем файлов и собственной векторной базой данных.
Но есть несколько ключевых отличий:
Реактивность
Supabase предлагает возможности реального времени через свои серверные контейнеры Realtime , которые вы можете разворачивать и управлять ими одновременно с вашей установкой. Данные Supabase в реальном времени не доставляются последовательно по одному и тому же каналу, что ограничивает гарантии согласованности.
Внутреннее устройство базы данных
Supabase использует PostgreSQL в качестве резервного хранилища, в Convex же используется собственное хранилище транзакционных документов, журнал записей хранится в AWS RDS. Помимо этого в Convex есть встроенное кэширование.
PostgreSQL
Так как Supabase используют PostgreSQL, то и ответственность за SQL-скрипты и хранение данных берет на себя разработчик. Convex же все ответственность за сохранение данных и передачу их берет на себя.
Авторизация
Для авторизации в Supabase используется собственный поставщик аутентификации. Convex интегрируется со стандартным набором внешних поставщиков аутентификации.
Использование
У Convex есть классная документация
Быстрый старт React
Для начала создадим базовый проект с помощью Vite или Next
yarn create vite@latest
После этого нам необходимо установить пакет convex
yarn add convex
После чего мы можем запустить сервер Convex. При первом запуске Convex попросит авторизоваться в их системе, а также создать проект в Convex.
npx convex dev
После команды выше Convex создает файл .env
и в корневой папке новую папку convex
. В папке convex
необходимо будет писать функции и схему вашей базы данных.
Для создания схемы необходимо создать файл convex/schema.ts
. Для типизации используются специальный валидатор — v
(подробнее об этом ниже)
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
tasks: defineTable({
text: v.string(),
isCompleted: v.boolean(),
}),
});
После этого можно написать определенные функции, например query
. Их нужно описывать в файле внутри папке convex
. Название файла влияет на то, откуда мы будем вытаскивать в будущем эту функцию
// convex/tasks.ts
import { query } from "./_generated/server";
export const get = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("tasks").collect();
},
});
При запущенном сервере Convex (npx convex dev
), он анализирует папку convex
и сам подтягивает изменения на сервер. При получения файлов Convex использует esbuild
для объединения файлов. При вызове функции Convex запускает функции с помощью V8
Для работы Convex также необходимо создать клиент Convex и обернуть все приложение провайдером
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import { ConvexProvider, ConvexReactClient } from "convex/react";
// Создаем клиента Convex
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
ReactDOM.createRoot(document.getElementById("root")!).render(
// Оборачиваем провайдером
,
);
Далее мы можем использовать наши функции в приложении
import "./App.css";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function App() {
// получаем все задачи с помощью функции tasks.get
const tasks = useQuery(api.tasks.get);
return (
{tasks?.map(({ _id, text }) => {text})}
);
}
export default App;
Схема
Создание схемы
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
messages: defineTable({
body: v.string(),
user: v.id("users"),
}),
users: defineTable({
name: v.string(),
tokenIdentifier: v.string(),
}).index("by_token", ["tokenIdentifier"]),
});
Типы данных
v.id(tableName)
— используется для внешних ключейv.null()
v.int64()
— в JavaScript это будетBigInt
v.number()
— число с плавающей запятойv.boolean()
v.string()
v.bytes()
— в JavaScript это будетArrayBuffer
v.array(values)
v.object({property: value})
Дополнительные валидаторы
v.optional()
— необязательное поле, если в такое поле ничего не придет, то ячейка в базе окажется пустойv.union()
— объединение нескольких элементовv.literal()
— одиночные значения, напримерv.literal("admin")
v.any()
Индексы
Для создания индекса, необходимо после defineTable
указать название индекса и поля на которые распространяется индекс
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
messages: defineTable({
channel: v.id("channels"),
body: v.string(),
user: v.id("users"),
})
.index("by_channel", ["channel"])
.index("by_channel_user", ["channel", "user"]),
});
Использовать индекс можно вот так:
const messages = await ctx.db
.query("messages")
.withIndex("by_channel_user", (q) => q.eq("channel", channel))
.collect();
Таблица
Таблица по умолчанию имеет два поля
_id
— уникальный идентифкатор вставленного документа. Генерируется в форматеBase32
, используя алфавит Крокфорда. Хранит 14 байт случайных чисел и 2 байта, которые отвечают за временную метку_creationTime
— время в миллисекундах в формате UNIX создания документа. Для хранения используются 64-битные числа с плавающей запятой.
На клиенте есть возможность использовать дженерик типы
const [userId, setUserId] = useState>();
export const getPlayerAdmin = (game: Doc<"games">) => {};
Работа с данными
Получение одного документа
Получить один документ можно с помощью команды .get(id)
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getTask = query({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
},
});
Получение нескольких документов
Для этого необходимо использовать .query().collect()
import { query } from "./_generated/server";
export const listTasks = query({
handler: async (ctx) => {
const tasks = await ctx.db.query("tasks").collect();
},
});
Также есть поддержка фильтров и сортировки — подробнее читать здесь
Добавление данных
Для этого используется .insert(tableName, data)
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const createTask = mutation({
args: { text: v.string() },
handler: async (ctx, args) => {
const taskId = await ctx.db.insert("tasks", { text: args.text });
},
});
Также есть поддержка обновления данных, замена и удаление — подробнее читать здесь
Функции
Query
Это функции, которые предназначены для получения данных. Создаются с помощью query
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getTaskList = query({
args: { taskListId: v.id("taskLists") },
handler: async (ctx, args) => {
const tasks = await ctx.db
.query("tasks")
.filter((q) => q.eq(q.field("taskListId"), args.taskListId))
.order("desc")
.take(100);
return tasks;
},
});
Mutations
Используются для изменения данных. Создаются с помощью mutation
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const createTask = mutation({
args: { text: v.string() },
handler: async (ctx, args) => {
const newTaskId = await ctx.db.insert("tasks", { text: args.text });
return newTaskId;
},
});
Internal
Это очень похожие на Mutations
и Queries
функции, но которые нельзя вызывать из клиента. Они используются для вызова внутри других функций
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
export const markPlanAsProfessional = internalMutation({
args: { planId: v.id("plans") },
handler: async (ctx, args) => {
await ctx.db.patch(args.planId, { planType: "professional" });
},
});
Использование:
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const upgrade = action({
args: {
planId: v.id("plans"),
},
handler: async (ctx, args) => {
const response = await fetch("https://...");
if (response.ok) {
await ctx.runMutation(internal.plans.markPlanAsProfessional, {
planId: args.planId,
});
}
},
});
Дополнительный материал
В Convex также реализованы Actions, HTTPActions, Scheduled Functions, Cron Jobs, Authentication, File Storage, Full Text Search, Vector Search и так далее
Подробно прочитать про все возможности можно здесь
Dashboard
У Convex есть удобный dashboard, где можно смотреть вообще все, что связано с ним:
Главное меню
Таблицы из базы данных
Функции
Логи
Deploy
Деплой Convex веб-приложения максимально простой и удобный. В Vercel необходимо поменять команду build
на npx convex deploy --cmd 'npm run build'
, а также сгенерировать CONVEX_DEPLOY_KEY
в Convex Dashboard. Подробнее можно прочитать здесь
Ограничения и бесплатный план
Ограничения можно увидеть в настройках команд. Бесплатно может быть только две команды. В каждой команде может быть 5 проектов, в сумме бесплатно доступно 10 проектов.
Convex Team Usage
Пример проекта на Convex
Как пример проекта на Convex можно считать мой пет-проект — Монополия онлайн, который я разрабатывал в команде с другом. Исходной код доступен — GitHub
Итог
Convex — это хороший инструмент для веб-разработчиков, который позволяет хранить и обрабатывать определенные данные. Для React разработчиков он гораздо удобнее, чем Firebase или полноценная база данных. Безусловно, это решение не дает всех возможностей и забирает полностью контроль над сервером и базой данных, поэтому для крупных проектов Convex не подходит, но для маленьких проектов он идеально подходит.