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?

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 Team Usage

Пример проекта на Convex

Как пример проекта на Convex можно считать мой пет-проект — Монополия онлайн, который я разрабатывал в команде с другом. Исходной код доступен — GitHub

Итог

Convex — это хороший инструмент для веб-разработчиков, который позволяет хранить и обрабатывать определенные данные. Для React разработчиков он гораздо удобнее, чем Firebase или полноценная база данных. Безусловно, это решение не дает всех возможностей и забирает полностью контроль над сервером и базой данных, поэтому для крупных проектов Convex не подходит, но для маленьких проектов он идеально подходит.

© Habrahabr.ru