convex

SKILL.md

Convex — Guia Completo

Convex é um backend-as-a-service reativo com banco de dados transacional, funções serverless TypeScript, sync em tempo real, e integração nativa com React/Next.js. O modelo mental: você escreve funções TypeScript que rodam no servidor, e os dados fluem reativamente para o cliente via WebSocket.

Referências Detalhadas

Templates e Assets

Regras

Quando Usar

  • Criando projeto com Convex como backend
  • Definindo schemas, tabelas, indexes ou validators
  • Escrevendo queries, mutations ou actions
  • Configurando autenticação (Convex Auth, Clerk, Auth0)
  • Integrando Convex com React ou Next.js
  • Configurando upload de arquivos ou busca (text/vector)
  • Implementando agendamento, cron jobs ou workflows
  • Construindo agentes de IA com @convex-dev/agent
  • Escrevendo testes com convex-test
  • Fazendo deploy ou configurando CI/CD

Diferenciadores do Convex

Aspecto Convex BaaS Tradicional
Reatividade Nativa (WebSocket) Polling manual
Consistência Transacional (ACID) Eventual
Tipagem End-to-end TypeScript Parcial
Functions Serverless com ctx tipado REST APIs
Caching Automático + invalidação Manual
Schema Validação runtime + compile Apenas compile

Quick Start

1. Instalar e inicializar

bun add convex
bunx convex dev

2. Definir schema

// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  tasks: defineTable({
    text: v.string(),
    completed: v.boolean(),
    userId: v.id("users"),
  })
    .index("by_user", ["userId"])
    .index("by_completed", ["completed"]),

  users: defineTable({
    name: v.string(),
    email: v.string(),
  }).index("by_email", ["email"]),
});

3. Escrever funções

// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";

export const list = query({
  args: { userId: v.id("users") },
  returns: v.array(v.object({
    _id: v.id("tasks"),
    _creationTime: v.number(),
    text: v.string(),
    completed: v.boolean(),
    userId: v.id("users"),
  })),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("tasks")
      .withIndex("by_user", (q) => q.eq("userId", args.userId))
      .order("desc")
      .collect();
  },
});

export const create = mutation({
  args: { text: v.string(), userId: v.id("users") },
  returns: v.id("tasks"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("tasks", {
      text: args.text,
      completed: false,
      userId: args.userId,
    });
  },
});

export const toggle = mutation({
  args: { taskId: v.id("tasks") },
  handler: async (ctx, args) => {
    const task = await ctx.db.get(args.taskId);
    if (!task) throw new Error("Task not found");
    await ctx.db.patch(args.taskId, { completed: !task.completed });
  },
});

4. Conectar ao React

// app/ConvexClientProvider.tsx
"use client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ReactNode } from "react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export default function ConvexClientProvider({ children }: { children: ReactNode }) {
  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
// app/layout.tsx
import ConvexClientProvider from "./ConvexClientProvider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR">
      <body>
        <ConvexClientProvider>{children}</ConvexClientProvider>
      </body>
    </html>
  );
}
// app/tasks/page.tsx
"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";

export default function TasksPage() {
  const tasks = useQuery(api.tasks.list, { userId: "user123" as any });
  const createTask = useMutation(api.tasks.create);
  const toggleTask = useMutation(api.tasks.toggle);

  if (!tasks) return <div>Carregando...</div>;

  return (
    <div>
      <button onClick={() => createTask({ text: "Nova tarefa", userId: "user123" as any })}>
        Adicionar
      </button>
      {tasks.map((task) => (
        <div key={task._id} onClick={() => toggleTask({ taskId: task._id })}>
          {task.completed ? "✓" : "○"} {task.text}
        </div>
      ))}
    </div>
  );
}

Schema Definition

Validators (v.*)

Validator Tipo TS Uso
v.string() string Textos
v.number() number Números (float64)
v.boolean() boolean Flags
v.null() null Nulo explícito
v.int64() bigint Inteiros grandes
v.bytes() ArrayBuffer Dados binários
v.id("table") Id<"table"> Referência a documento
v.array(v.X()) X[] Arrays (max 8192)
v.object({...}) {...} Objetos (max 1024 keys)
v.record(k, v) Record<K, V> Maps dinâmicos
v.union(a, b) A | B Unions
v.literal("x") "x" Constantes
v.optional(v.X()) X | undefined Campos opcionais
v.any() any Qualquer valor

Tipos TypeScript gerados

import { Doc, Id } from "./_generated/dataModel";
import { Infer } from "convex/values";

type User = Doc<"users">;       // tipo completo do documento
type UserId = Id<"users">;      // tipo do ID

// Custom validator → tipo
const statusValidator = v.union(v.literal("active"), v.literal("archived"));
type Status = Infer<typeof statusValidator>; // "active" | "archived"

Indexes

defineTable({ channel: v.id("channels"), body: v.string(), author: v.id("users") })
  .index("by_channel", ["channel"])                    // index simples
  .index("by_channel_author", ["channel", "author"])   // index composto
  .searchIndex("search_body", {                        // full-text search
    searchField: "body",
    filterFields: ["channel"],
  })
  .vectorIndex("by_embedding", {                       // vector search
    vectorField: "embedding",
    dimensions: 1536,
    filterFields: ["channel"],
  })

Functions Core

Contextos disponíveis

Tipo ctx.db read ctx.db write ctx.auth ctx.storage ctx.scheduler APIs externas
query Sim - Sim getUrl - -
mutation Sim Sim Sim Sim Sim -
action via runQuery via runMutation Sim Sim Sim Sim
httpAction via runQuery via runMutation - Sim - Sim

Error handling

import { ConvexError } from "convex/values";

// Lançar erro tipado
throw new ConvexError({ code: "NOT_FOUND", message: "Documento não encontrado" });

// Capturar no cliente
try {
  await createTask({ text: "" });
} catch (error) {
  if (error instanceof ConvexError) {
    const data = error.data; // { code: "NOT_FOUND", message: "..." }
  }
}

Internal functions

import { internalQuery, internalMutation, internalAction } from "./_generated/server";
import { internal } from "./_generated/api";

// Definir — não acessível por clientes
export const processJob = internalMutation({
  args: { jobId: v.id("jobs") },
  handler: async (ctx, args) => { /* ... */ },
});

// Chamar — usa `internal` (não `api`)
await ctx.scheduler.runAfter(0, internal.jobs.processJob, { jobId });
await ctx.runMutation(internal.jobs.processJob, { jobId }); // em actions

Database Operations

Leitura

// Por ID
const doc = await ctx.db.get(documentId);

// Query com index (mais eficiente)
const results = await ctx.db
  .query("messages")
  .withIndex("by_channel", (q) => q.eq("channel", channelId))
  .order("desc")
  .collect();

// Métodos de resultado: .collect(), .first(), .unique(), .take(n)

// Filtro pós-index
const active = await ctx.db
  .query("tasks")
  .withIndex("by_user", (q) => q.eq("userId", userId))
  .filter((q) => q.eq(q.field("completed"), false))
  .take(10);

Escrita

const id = await ctx.db.insert("tasks", { text: "Nova", completed: false });
await ctx.db.patch(id, { completed: true });           // merge parcial
await ctx.db.replace(id, { text: "Substituída", completed: true }); // substitui tudo
await ctx.db.delete(id);

React Hooks

// Query reativa — undefined enquanto carrega, depois dados
const tasks = useQuery(api.tasks.list, { userId });

// Skip condicional
const tasks = useQuery(api.tasks.list, userId ? { userId } : "skip");

// Mutation — retries automáticos
const create = useMutation(api.tasks.create);
await create({ text: "Nova tarefa", userId });

// Action — sem retry automático, pode falhar
const generate = useAction(api.ai.generate);
const result = await generate({ prompt: "..." });

// Paginação infinita
const { results, status, loadMore } = usePaginatedQuery(
  api.messages.list, {}, { initialNumItems: 25 }
);
// status: "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted"

Authentication Overview

Convex Auth (setup rápido)

bun add @convex-dev/auth
// convex/auth.ts
import { convexAuth } from "@convex-dev/auth/server";
import GitHub from "@auth/core/providers/github";
import { Password } from "@convex-dev/auth/providers/Password";

export const { auth, signIn, signOut, store } = convexAuth({
  providers: [GitHub, Password],
});
// Client — ConvexAuthProvider em vez de ConvexProvider
import { ConvexAuthProvider } from "@convex-dev/auth/react";
// Verificar autenticação no backend
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new ConvexError("Não autenticado");
// identity.tokenIdentifier, identity.name, identity.email

Clerk (resumo)

// convex/auth.config.ts
export default {
  providers: [{ domain: process.env.CLERK_JWT_ISSUER_DOMAIN, applicationID: "convex" }],
};
// Client
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { ClerkProvider, useAuth } from "@clerk/nextjs";

<ClerkProvider>
  <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
    <App />
  </ConvexProviderWithClerk>
</ClerkProvider>

Common Patterns

Relacionamentos (manual via IDs)

// Convex não tem JOINs — você faz "join manual"
export const getMessageWithAuthor = query({
  args: { messageId: v.id("messages") },
  handler: async (ctx, args) => {
    const message = await ctx.db.get(args.messageId);
    if (!message) return null;
    const author = await ctx.db.get(message.authorId);
    return { ...message, author };
  },
});

Quando usar cada tipo de função

Use Quando
query Leitura de dados (reativa, cacheada, determinística)
mutation Escrita no banco (transacional, sem APIs externas)
action APIs externas, LLMs, file processing, lógica não-determinística
httpAction Webhooks, APIs REST, servir arquivos
internal* Funções chamadas apenas pelo backend (scheduler, crons, actions)

Environment Variables

# Development
bunx convex env set OPENAI_API_KEY sk-...

# Production
bunx convex deploy --env-var OPENAI_API_KEY=sk-...
// Usar em actions (não em queries/mutations)
const key = process.env.OPENAI_API_KEY;

OCC (Optimistic Concurrency Control)

Mutations são transacionais. Se dois clientes modificam o mesmo documento simultaneamente, o Convex detecta o conflito e re-executa automaticamente a mutation que perdeu a corrida. Não precisa de locks manuais.

CLI Essencial

Comando Descrição
bunx convex dev Watch mode (sync + logs)
bunx convex deploy Deploy para produção
bunx convex run module:function '{"arg":"val"}' Executar função
bunx convex env set KEY value Definir env var
bunx convex import --table T data.jsonl Importar dados
bunx convex export --path backup.zip Exportar dados
bunx convex logs Tail dos logs
bunx convex dashboard Abrir dashboard
Weekly Installs
2
First Seen
Feb 26, 2026
Installed on
mcpjam2
iflow-cli2
claude-code2
junie2
windsurf2
zencoder2