convex
Database and Backend logic
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Menu
Database and Backend logic
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Basé sur la classification professionnelle SOC
Outseta is an all-in-one operating system for SaaS founders, combining CRM, authentication, billing, and customer support into a single platform. It is designed to handle the "business logic" of a SaaS product so developers can focus on the "functional logic". Use when the user needs to integrate authentication, billing, or CRM features using Outseta.
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
| name | convex |
| description | Database and Backend logic |
import { query } from "./_generated/server";
import { v } from "convex/values";
export const f = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
// Function body
},
});
convex/http.ts and require an httpAction decorator. For example:import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/echo",
method: "POST",
handler: httpAction(async (ctx, req) => {
const body = await req.bytes();
return new Response(body, { status: 200 });
}),
});
path field. For example, if you specify /api/someRoute, the endpoint will be registered at /api/someRoute.import { mutation } from "./_generated/server";
import { v } from "convex/values";
export default mutation({
args: {
simpleArray: v.array(v.union(v.string(), v.number())),
},
handler: async (ctx, args) => {
//...
},
});
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
results: defineTable(
v.union(
v.object({
kind: v.literal("error"),
errorMessage: v.string(),
}),
v.object({
kind: v.literal("success"),
value: v.number(),
}),
),
),
});
v.null() validator when returning a null value. Below is an example query that returns a null value:import { query } from "./_generated/server";
import { v } from "convex/values";
export const exampleQuery = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("This query returns a null value");
return null;
},
});
| Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes |
|---|---|---|---|---|
| Id | string | doc._id | v.id(tableName) | |
| Null | null | null | v.null() | JavaScript's undefined is not a valid Convex value. Functions the return undefined or do not return will return null when called from a client. Use null instead. |
| Int64 | bigint | 3n | v.int64() | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports bigints in most modern browsers. |
| Float64 | number | 3.1 | v.number() | Convex supports all IEEE-754 double-precision floating point numbers (such as NaNs). Inf and NaN are JSON serialized as strings. |
| Boolean | boolean | true | v.boolean() | |
| String | string | "abc" | v.string() | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. |
| Bytes | ArrayBuffer | new ArrayBuffer(8) | v.bytes() | Convex supports first class bytestrings, passed in as ArrayBuffers. Bytestrings must be smaller than the 1MB total size limit for Convex types. |
| Array | Array | [1, 3.2, "abc"] | v.array(values) | Arrays can have at most 8192 values. |
| Object | Object | {a: "abc"} | v.object({property: value}) | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". |
| Record | Record | {"a": "1", "b": "2"} | v.record(keys, values) | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "_". |
internalQuery, internalMutation, and internalAction to register internal functions. These functions are private and aren't part of an app's API. They can only be called by other Convex functions. These functions are always imported from ./_generated/server.query, mutation, and action to register public functions. These functions are part of the public API and are exposed to the public Internet. Do NOT use query, mutation, or action to register sensitive internal functions that should be kept private.api or internal objects.query, internalQuery, mutation, internalMutation, action, and internalAction. If a function doesn't return anything, include returns: v.null() as its output validator.null.ctx.runQuery to call a query from a query, mutation, or action.ctx.runMutation to call a mutation from a mutation or action.ctx.runAction to call an action from an action.FunctionReference. Do NOT try to pass the callee function directly into one of these calls.ctx.runQuery, ctx.runMutation, or ctx.runAction to call a function in the same file, specify a type annotation on the return value to work around TypeScript circularity limitations. For example,export const f = query({
args: { name: v.string() },
returns: v.string(),
handler: async (ctx, args) => {
return "Hello " + args.name;
},
});
export const g = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
return null;
},
});
api object defined by the framework in convex/_generated/api.ts to call public functions registered with query, mutation, or action.internal object defined by the framework in convex/_generated/api.ts to call internal (or private) functions registered with internalQuery, internalMutation, or internalAction.convex/example.ts named f has a function reference of api.example.f.convex/example.ts named g has a function reference of internal.example.g.convex/ folder. For example, a public function h defined in convex/messages/access.ts has a function reference of api.messages.access.h.convex/ directory.query, mutation, and action to define public functions.internalQuery, internalMutation, and internalAction to define private, internal functions.import { v } from "convex/values";
import { query, mutation } from "./_generated/server";
import { paginationOptsValidator } from "convex/server";
export const listWithExtraArg = query({
args: { paginationOpts: paginationOptsValidator, author: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.filter((q) => q.eq(q.field("author"), args.author))
.order("desc")
.paginate(args.paginationOpts);
},
});
Note: paginationOpts is an object with the following properties:
numItems: the maximum number of documents to return (the validator is v.number())cursor: the cursor to use to fetch the next page of documents (the validator is v.union(v.string(), v.null())).paginate() returns an object that has the following properties: - page (contains an array of documents that you fetches) - isDone (a boolean that represents whether or not this is the last page of documents) - continueCursor (a string that represents the cursor to use to fetch the next page of documents)v.bigint() is deprecated for representing signed 64-bit integers. Use v.int64() instead.v.record() for defining a record type. v.map() and v.set() are not supported.convex/schema.ts.convex/server._creationTime which has the validator v.number() and _id which has the validator v.id(tableName).["field1", "field2"], the index name should be "by_field1_and_field2".Id imported from './_generated/dataModel' to get the type of the id for a given table. For example if there is a table called 'users' you can use Id<'users'> to get the type of the id for that table.Record make sure that you correctly provide the type of the key and value in the type. For example a validator v.record(v.id('users'), v.string()) would have the type Record<Id<'users'>, string>. Below is an example of using Record with an Id type in a query:import { query } from "./_generated/server";
import { Doc, Id } from "./_generated/dataModel";
export const exampleQuery = query({
args: { userIds: v.array(v.id("users")) },
returns: v.record(v.id("users"), v.string()),
handler: async (ctx, args) => {
const idToUsername: Record<Id<"users">, string> = {};
for (const userId of args.userIds) {
const user = await ctx.db.get("users", userId);
if (user) {
idToUsername[user._id] = user.username;
}
}
return idToUsername;
},
});
Id<'users'> rather than string.as const for string literals in discriminated union types.Array type, make sure to always define your arrays as const array: Array<T> = [...];Record type, make sure to always define your records as const record: Record<KeyType, ValueType> = {...};@types/node to your package.json when using any Node.js built-in modules.const messages = await ctx.db .query("messages") .withSearchIndex("search_body", (q) => q.search("body", "hello hi").eq("channel", "#general"), ) .take(10);
filter in queries. Instead, define an index in the schema and use withIndex instead..delete(). Instead, .collect() the results, iterate over them, and call ctx.db.delete(row._id) on each result..unique() to get a single document from a query. This method will throw an error if there are multiple documents that match the query..collect() or .take(n) on the result of a query. Instead, use the for await (const row of query) syntax._creationTime order..order('asc') or .order('desc') to pick whether a query is in ascending or descending order. If the order isn't specified, it defaults to ascending.ctx.db.replace to fully replace an existing document. This method will throw an error if the document does not exist. Syntax: await ctx.db.replace('tasks', taskId, { name: 'Buy milk', completed: false })ctx.db.patch to shallow merge updates into an existing document. This method will throw an error if the document does not exist. Syntax: await ctx.db.patch('tasks', taskId, { completed: true })"use node"; to the top of files containing actions that use Node.js built-in modules.ctx.db inside of an action. Actions don't have access to the database.import { action } from "./_generated/server";
export const exampleAction = action({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("This action does not return anything");
return null;
},
});
crons.interval or crons.cron methods to schedule cron jobs. Do NOT use the crons.hourly, crons.daily, or crons.weekly helpers.crons object, calling some methods on it, and then exporting it as default. For example,import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
import { internalAction } from "./_generated/server";
const empty = internalAction({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
console.log("empty");
},
});
const crons = cronJobs();
// Run `internal.crons.empty` every two hours.
crons.interval("delete inactive users", { hours: 2 }, internal.crons.empty, {});
export default crons;
crons.ts just like any other file.internal object from '_generated/api', even if the internal function is registered in the same file.Convex includes file storage for large files like images, videos, and PDFs.
The ctx.storage.getUrl() method returns a signed URL for a given file. It returns null if the file doesn't exist.
Do NOT use the deprecated ctx.storage.getMetadata call for loading a file's metadata.
Instead, query the `_storage` system table. For example, you can use `ctx.db.system.get` to get an `Id<"_storage">`.
import { query } from "./_generated/server";
import { Id } from "./_generated/dataModel";
type FileMetadata = {
_id: Id<"_storage">;
_creationTime: number;
contentType?: string;
sha256: string;
size: number;
}
export const exampleQuery = query({
args: { fileId: v.id("_storage") },
returns: v.null(),
handler: async (ctx, args) => {
const metadata: FileMetadata | null = await ctx.db.system.get("_storage", args.fileId);
console.log(metadata);
return null;
},
});
Blob objects. You must convert all items to/from a Blob when using Convex storage.Create a real-time chat application backend with AI responses. The app should:
- Allow creating users with names
- Support multiple chat channels
- Enable users to send messages to channels
- Automatically generate AI responses to user messages
- Show recent message history
The backend should provide APIs for:
1. User management (creation)
2. Channel management (creation)
3. Message operations (sending, listing)
4. AI response generation using OpenAI's GPT-4
Messages should be stored with their channel, author, and content. The system should maintain message order
and limit history display to the 10 most recent messages per channel.
Public Queries:
Internal Functions:
{
"name": "chat-app",
"description": "This example shows how to build a chat app without authentication.",
"version": "1.0.0",
"dependencies": {
"convex": "^1.31.2",
"openai": "^4.79.0"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"jsx": "react-jsx"
},
"exclude": ["convex"],
"include": ["**/src/**/*.tsx", "**/src/**/*.ts", "vite.config.ts"]
}
import {
query,
mutation,
internalQuery,
internalMutation,
internalAction,
} from "./_generated/server";
import { v } from "convex/values";
import OpenAI from "openai";
import { internal } from "./_generated/api";
/**
* Create a user with a given name.
*/
export const createUser = mutation({
args: {
name: v.string(),
},
returns: v.id("users"),
handler: async (ctx, args) => {
return await ctx.db.insert("users", { name: args.name });
},
});
/**
* Create a channel with a given name.
*/
export const createChannel = mutation({
args: {
name: v.string(),
},
returns: v.id("channels"),
handler: async (ctx, args) => {
return await ctx.db.insert("channels", { name: args.name });
},
});
/**
* List the 10 most recent messages from a channel in descending creation order.
*/
export const listMessages = query({
args: {
channelId: v.id("channels"),
},
returns: v.array(
v.object({
_id: v.id("messages"),
_creationTime: v.number(),
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}),
),
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(10);
return messages;
},
});
/**
* Send a message to a channel and schedule a response from the AI.
*/
export const sendMessage = mutation({
args: {
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
if (!channel) {
throw new Error("Channel not found");
}
const user = await ctx.db.get(args.authorId);
if (!user) {
throw new Error("User not found");
}
await ctx.db.insert("messages", {
channelId: args.channelId,
authorId: args.authorId,
content: args.content,
});
await ctx.scheduler.runAfter(0, internal.index.generateResponse, {
channelId: args.channelId,
});
return null;
},
});
const openai = new OpenAI();
export const generateResponse = internalAction({
args: {
channelId: v.id("channels"),
},
returns: v.null(),
handler: async (ctx, args) => {
const context = await ctx.runQuery(internal.index.loadContext, {
channelId: args.channelId,
});
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: context,
});
const content = response.choices[0].message.content;
if (!content) {
throw new Error("No content in response");
}
await ctx.runMutation(internal.index.writeAgentResponse, {
channelId: args.channelId,
content,
});
return null;
},
});
export const loadContext = internalQuery({
args: {
channelId: v.id("channels"),
},
returns: v.array(
v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
content: v.string(),
}),
),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
if (!channel) {
throw new Error("Channel not found");
}
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(10);
const result = [];
for (const message of messages) {
if (message.authorId) {
const user = await ctx.db.get(message.authorId);
if (!user) {
throw new Error("User not found");
}
result.push({
role: "user" as const,
content: `${user.name}: ${message.content}`,
});
} else {
result.push({ role: "assistant" as const, content: message.content });
}
}
return result;
},
});
export const writeAgentResponse = internalMutation({
args: {
channelId: v.id("channels"),
content: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert("messages", {
channelId: args.channelId,
content: args.content,
});
return null;
},
});
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
channels: defineTable({
name: v.string(),
}),
users: defineTable({
name: v.string(),
}),
messages: defineTable({
channelId: v.id("channels"),
authorId: v.optional(v.id("users")),
content: v.string(),
}).index("by_channel", ["channelId"]),
});
{
/* This TypeScript project config describes the environment that
* Convex functions run in and is used to typecheck them.
* You can modify it, but some settings required to use Convex.
*/
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": true,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"isolatedModules": true,
"noEmit": true
},
"include": ["./**/*"],
"exclude": ["./_generated"]
}
export default function App() {
return <div>Hello World</div>;
}