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
- references/schema-and-validators.md — Schema, validators, tipos, indexes, migrations
- references/functions-deep-dive.md — Queries, mutations, actions, HTTP, internal functions
- references/database-operations.md — Leitura, escrita, paginação, OCC
- references/authentication.md — Convex Auth, Clerk, Auth0, custom JWT
- references/file-storage-and-search.md — Upload/serve, full-text search, vector search
- references/scheduling-and-crons.md — runAfter/runAt, cron patterns
- references/react-and-nextjs.md — Hooks, SSR, App Router, TanStack
- references/ai-agents.md — Agent framework, workflows, RAG
- references/testing-and-production.md — convex-test, deploy, CI/CD
- references/components-and-advanced.md — Components, self-hosting, CLI, limites
Templates e Assets
- assets/convex-schema-template.ts — Schema starter com tabelas, indexes, search
- assets/convex-functions-template.ts — Query + mutation + action + internal starter
- assets/convex-auth-setup.ts — Auth config + user storage pattern
- assets/convex-nextjs-provider.tsx — Provider para Next.js App Router
- assets/convex-react-patterns.tsx — Loading, forms, pagination, upload, optimistic
Regras
- rules/convex-conventions.md — Naming, estrutura, anti-patterns
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
Repository
fernando-august…e-skillsFirst Seen
Feb 26, 2026
Security Audits
Installed on
mcpjam2
iflow-cli2
claude-code2
junie2
windsurf2
zencoder2