or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced.mdauth.mddatabase.mdindex.mdnextjs.mdreact.mdschema.mdserver-functions.mdvalues-validators.md
tile.json

server-functions.mddocs/

Server Functions

Function Types

Query - Read-only, Reactive

function query<ArgsValidator, Output>(func: {
  args?: ArgsValidator;
  handler: (ctx: QueryCtx, args: ObjectType<ArgsValidator>) => Promise<Output>;
}): RegisteredQuery<'public', ArgsValidator, Output>;

interface QueryCtx {
  db: GenericDatabaseReader;  // Read-only
  auth: Auth;
  storage: StorageReader;
  runQuery<Q extends FunctionReference<'query'>>(query: Q, ...args): Promise<ReturnType<Q>>;
}

Usage:

export const list = query({
  args: {},
  handler: async (ctx) => await ctx.db.query('messages').collect(),
});

export const getByAuthor = query({
  args: { author: v.string() },
  handler: async (ctx, args) =>
    await ctx.db.query('messages')
      .withIndex('by_author', q => q.eq('author', args.author))
      .collect(),
});

// With auth
export const myPosts = query({
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return [];
    return await ctx.db.query('posts')
      .withIndex('by_author', q => q.eq('author', identity.subject))
      .collect();
  },
});

Mutation - Atomic Writes

function mutation<ArgsValidator, Output>(func: {
  args?: ArgsValidator;
  handler: (ctx: MutationCtx, args: ObjectType<ArgsValidator>) => Promise<Output>;
}): RegisteredMutation<'public', ArgsValidator, Output>;

interface MutationCtx {
  db: GenericDatabaseWriter;  // Read-write (insert/patch/replace/delete)
  auth: Auth;
  storage: StorageWriter;
  scheduler: Scheduler;
  runQuery<Q>(query: Q, ...args): Promise<ReturnType<Q>>;
  runMutation<M>(mutation: M, ...args): Promise<ReturnType<M>>;
}

Usage:

export const create = mutation({
  args: { author: v.string(), body: v.string() },
  handler: async (ctx, args) =>
    await ctx.db.insert('messages', { author: args.author, body: args.body }),
});

export const update = mutation({
  args: { id: v.id('messages'), body: v.string() },
  handler: async (ctx, args) => {
    await ctx.db.patch(args.id, { body: args.body });
  },
});

export const like = mutation({
  args: { id: v.id('messages') },
  handler: async (ctx, args) => {
    const msg = await ctx.db.get(args.id);
    if (!msg) throw new Error('Not found');
    await ctx.db.patch(args.id, { likes: msg.likes + 1 });
  },
});

// Atomic multi-operation
export const transfer = mutation({
  args: { fromId: v.id('accounts'), toId: v.id('accounts'), amount: v.number() },
  handler: async (ctx, args) => {
    const from = await ctx.db.get(args.fromId);
    const to = await ctx.db.get(args.toId);
    // Both succeed or both fail
    await ctx.db.patch(args.fromId, { balance: from.balance - args.amount });
    await ctx.db.patch(args.toId, { balance: to.balance + args.amount });
  },
});

Action - External APIs, Non-transactional

function action<ArgsValidator, Output>(func: {
  args?: ArgsValidator;
  handler: (ctx: ActionCtx, args: ObjectType<ArgsValidator>) => Promise<Output>;
}): RegisteredAction<'public', ArgsValidator, Output>;

interface ActionCtx {
  runQuery<Q>(query: Q, ...args): Promise<ReturnType<Q>>;
  runMutation<M>(mutation: M, ...args): Promise<ReturnType<M>>;
  runAction<A>(action: A, ...args): Promise<ReturnType<A>>;
  scheduler: Scheduler;
  auth: Auth;
  storage: StorageActionWriter;  // get/store files
  vectorSearch(table: string, index: string): VectorSearch;
}

Usage:

export const sendEmail = action({
  args: { to: v.string(), subject: v.string(), body: v.string() },
  handler: async (ctx, args) => {
    await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${process.env.SENDGRID_KEY}` },
      body: JSON.stringify({ to: args.to, subject: args.subject, html: args.body }),
    });
    await ctx.runMutation(api.logs.record, { event: 'email_sent', to: args.to });
  },
});

export const generateSummary = action({
  args: { text: v.string() },
  handler: async (ctx, args) => {
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.OPENAI_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'gpt-4',
        messages: [{ role: 'user', content: `Summarize: ${args.text}` }],
      }),
    });
    const data = await response.json();
    return data.choices[0].message.content;
  },
});

// Vector search
export const semanticSearch = action({
  args: { query: v.string() },
  handler: async (ctx, args) => {
    // Get embedding from external API
    const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${process.env.OPENAI_KEY}` },
      body: JSON.stringify({ model: 'text-embedding-ada-002', input: args.query }),
    });
    const { data } = await embeddingResponse.json();

    // Search in Convex
    return await ctx.vectorSearch('documents', 'by_embedding', {
      vector: data[0].embedding,
      limit: 10,
    });
  },
});

HTTP Action - Webhooks & Custom Endpoints

function httpAction(
  handler: (ctx: ActionCtx, request: Request) => Promise<Response>
): PublicHttpAction;

Usage:

// convex/http.ts
import { httpRouter, httpAction } from 'convex/server';

const http = httpRouter();

http.route({
  path: '/webhook',
  method: 'POST',
  handler: httpAction(async (ctx, request) => {
    const signature = request.headers.get('X-Signature');
    if (!verifySignature(signature)) {
      return new Response('Unauthorized', { status: 401 });
    }

    const body = await request.json();
    await ctx.runMutation(api.webhooks.process, body);

    return new Response(JSON.stringify({ success: true }), {
      headers: { 'Content-Type': 'application/json' },
    });
  }),
});

http.route({
  path: '/api/users',
  method: 'GET',
  handler: httpAction(async (ctx, request) => {
    const url = new URL(request.url);
    const id = url.searchParams.get('id');

    const user = await ctx.runQuery(api.users.get, { id });
    return new Response(JSON.stringify(user), {
      headers: { 'Content-Type': 'application/json' },
    });
  }),
});

export default http;

Internal Functions

For server-only functions not exposed to clients:

// Internal query
export const _getSecret = internalQuery({
  args: { id: v.id('secrets') },
  handler: async (ctx, args) => await ctx.db.get(args.id),
});

// Internal mutation
export const _cleanup = internalMutation({
  args: { olderThan: v.number() },
  handler: async (ctx, args) => {
    const old = await ctx.db.query('logs')
      .filter(q => q.lt(q.field('_creationTime'), args.olderThan))
      .collect();
    for (const log of old) await ctx.db.delete(log._id);
  },
});

// Internal action
export const _processWebhook = internalAction({
  args: { data: v.any() },
  handler: async (ctx, args) => {
    await fetch('https://external-service.com/process', {
      method: 'POST',
      body: JSON.stringify(args.data),
    });
  },
});

// Call internal functions
import { internal } from './_generated/api';

export const scheduleCleanup = mutation({
  handler: async (ctx) => {
    await ctx.scheduler.runAfter(0, internal.logs._cleanup, {
      olderThan: Date.now() - 7 * 24 * 60 * 60 * 1000,
    });
  },
});

Common Patterns

Auth Check

const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error('Not authenticated');

// Check role
const role = identity.role as string | undefined;
if (role !== 'admin') throw new Error('Not authorized');

Error Handling

import { ConvexError } from 'convex/values';

// String error
throw new ConvexError('User not found');

// Structured error
throw new ConvexError({
  code: 'VALIDATION_ERROR',
  field: 'email',
  message: 'Invalid email format',
});

Calling Other Functions

// In queries: only runQuery
const data = await ctx.runQuery(api.module.otherQuery, { arg: 'value' });

// In mutations: runQuery + runMutation
const data = await ctx.runQuery(api.module.query, {});
await ctx.runMutation(api.module.otherMutation, {});

// In actions: runQuery + runMutation + runAction
const data = await ctx.runQuery(api.module.query, {});
await ctx.runMutation(api.module.mutation, {});
await ctx.runAction(api.module.otherAction, {});

Optional Arguments

export const search = query({
  args: {
    query: v.string(),
    limit: v.optional(v.number()),
    category: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 10;
    let q = ctx.db.query('items');
    if (args.category) {
      q = q.withIndex('by_category', q => q.eq('category', args.category));
    }
    return await q.take(limit);
  },
});