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

advanced.mddocs/

Advanced Features

Pagination

Backend

import { query } from './_generated/server';
import { paginationOptsValidator } from 'convex/server';

export const listMessages = query({
  args: { paginationOpts: paginationOptsValidator },
  handler: async (ctx, args) =>
    await ctx.db.query('messages')
      .order('desc')
      .paginate(args.paginationOpts),
});

// Returns: { page: Item[], isDone: boolean, continueCursor: string }

// With filters
export const listByAuthor = query({
  args: {
    author: v.string(),
    paginationOpts: paginationOptsValidator,
  },
  handler: async (ctx, args) =>
    await ctx.db.query('messages')
      .withIndex('by_author', q => q.eq('author', args.author))
      .paginate(args.paginationOpts),
});

React

import { usePaginatedQuery } from 'convex/react';

function MessageList() {
  const { results, status, loadMore } = usePaginatedQuery(
    api.messages.listMessages,
    {},
    { initialNumItems: 20 }
  );

  return (
    <div>
      {results.map(msg => <div key={msg._id}>{msg.body}</div>)}

      {status === 'CanLoadMore' && (
        <button onClick={() => loadMore(20)}>Load More</button>
      )}
      {status === 'LoadingMore' && <div>Loading...</div>}
      {status === 'Exhausted' && <div>No more items</div>}
    </div>
  );
}

File Storage

Upload

// Backend: generate URL
export const generateUploadUrl = mutation(async (ctx) =>
  await ctx.storage.generateUploadUrl()
);

// Client: upload file
async function uploadFile(file: File) {
  // 1. Get upload URL
  const uploadUrl = await convex.mutation(api.files.generateUploadUrl);

  // 2. Upload to Convex
  const result = await fetch(uploadUrl, {
    method: 'POST',
    headers: { 'Content-Type': file.type },
    body: file,
  });

  const { storageId } = await result.json();

  // 3. Save reference in database
  await convex.mutation(api.files.save, {
    storageId,
    name: file.name,
    type: file.type,
    size: file.size,
  });
}

// Backend: save reference
export const save = mutation({
  args: {
    storageId: v.string(),
    name: v.string(),
    type: v.string(),
    size: v.number(),
  },
  handler: async (ctx, args) =>
    await ctx.db.insert('files', args),
});

Download

// Backend: get URL (valid 1 hour)
export const getUrl = query({
  args: { storageId: v.string() },
  handler: async (ctx, args) =>
    await ctx.storage.getUrl(args.storageId),
});

// Client
const url = useQuery(api.files.getUrl, { storageId });
if (url) {
  <img src={url} alt="Image" />
}

Delete

export const deleteFile = mutation({
  args: { fileId: v.id('files') },
  handler: async (ctx, args) => {
    const file = await ctx.db.get(args.fileId);
    if (!file) throw new Error('Not found');

    await ctx.storage.delete(file.storageId);
    await ctx.db.delete(args.fileId);
  },
});

Direct File Operations (Actions)

// Download in action
export const processFile = action({
  args: { storageId: v.string() },
  handler: async (ctx, args) => {
    const blob = await ctx.storage.get(args.storageId);
    if (!blob) throw new Error('Not found');

    const buffer = await blob.arrayBuffer();
    // Process file...
  },
});

// Upload in action
export const fetchAndStore = action({
  args: { url: v.string() },
  handler: async (ctx, args) => {
    const response = await fetch(args.url);
    const blob = await response.blob();

    const storageId = await ctx.storage.store(blob);

    await ctx.runMutation(api.files.save, {
      storageId,
      name: 'downloaded',
      type: blob.type,
    });
  },
});

Scheduled Functions

Scheduling

// Schedule for later
export const scheduleReminder = mutation({
  args: { userId: v.id('users'), message: v.string(), delayMinutes: v.number() },
  handler: async (ctx, args) => {
    const delayMs = args.delayMinutes * 60 * 1000;

    const jobId = await ctx.scheduler.runAfter(
      delayMs,
      internal.notifications.send,
      { userId: args.userId, message: args.message }
    );

    return jobId;  // Id<'_scheduled_functions'>
  },
});

// Schedule at specific time
export const schedulePublish = mutation({
  args: { postId: v.id('posts'), publishAt: v.number() },
  handler: async (ctx, args) => {
    const jobId = await ctx.scheduler.runAt(
      args.publishAt,
      internal.posts.publish,
      { postId: args.postId }
    );

    await ctx.db.patch(args.postId, {
      scheduledJobId: jobId,
      status: 'scheduled',
    });
  },
});

// Cancel scheduled job
export const cancelJob = mutation({
  args: { jobId: v.id('_scheduled_functions') },
  handler: async (ctx, args) => {
    await ctx.scheduler.cancel(args.jobId);
  },
});

// Internal function (scheduled target)
export const send = internalMutation({
  args: { userId: v.id('users'), message: v.string() },
  handler: async (ctx, args) => {
    await ctx.db.insert('notifications', {
      userId: args.userId,
      message: args.message,
      read: false,
    });
  },
});

Cron Jobs

// convex/crons.ts
import { cronJobs } from 'convex/server';
import { internal } from './_generated/api';

const crons = cronJobs();

// Every 5 minutes
crons.interval('cleanup', { minutes: 5 }, internal.tasks.cleanup);

// Hourly at minute 15
crons.hourly('send-digest', { minuteUTC: 15 }, internal.emails.sendDigest);

// Daily at 2 AM UTC
crons.daily('backup', { hourUTC: 2, minuteUTC: 0 }, internal.tasks.backup);

// Weekly on Monday at 9 AM UTC
crons.weekly('report', {
  dayOfWeek: 'monday',
  hourUTC: 9,
  minuteUTC: 0,
}, internal.reports.generate);

// Monthly on 1st at midnight UTC
crons.monthly('billing', {
  day: 1,
  hourUTC: 0,
  minuteUTC: 0,
}, internal.billing.process);

// Custom cron expression
crons.cron('health-check', '*/15 * * * *', internal.health.check);

// With arguments
crons.daily('summary', { hourUTC: 8, minuteUTC: 0 }, internal.emails.sendSummary, {
  type: 'daily',
});

export default crons;

HTTP Actions

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

const http = httpRouter();

// POST webhook
http.route({
  path: '/webhook',
  method: 'POST',
  handler: httpAction(async (ctx, request) => {
    const signature = request.headers.get('X-Signature');
    if (!verifySignature(signature, await request.text())) {
      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' },
    });
  }),
});

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

    if (!id) {
      return new Response('Missing id', { status: 400 });
    }

    const user = await ctx.runQuery(api.users.get, { id });

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

// File upload
http.route({
  path: '/upload',
  method: 'POST',
  handler: httpAction(async (ctx, request) => {
    const blob = await request.blob();
    const storageId = await ctx.storage.store(blob);

    await ctx.runMutation(api.files.save, {
      storageId,
      contentType: blob.type,
      size: blob.size,
    });

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

// Path prefix
http.route({
  pathPrefix: '/api/admin/',
  method: 'GET',
  handler: httpAction(async (ctx, request) => {
    const authHeader = request.headers.get('Authorization');
    if (!authHeader) {
      return new Response('Unauthorized', { status: 401 });
    }

    const url = new URL(request.url);
    // Handle /api/admin/* routes
    return new Response(`Admin route: ${url.pathname}`);
  }),
});

export default http;

Vector Search

Schema

defineTable({
  content: v.string(),
  embedding: v.array(v.float64()),
  category: v.string(),
}).vectorIndex('by_embedding', {
  vectorField: 'embedding',
  dimensions: 1536,  // OpenAI ada-002
  filterFields: ['category'],
})

Search (Actions only)

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

    const { data } = await embeddingRes.json();
    const embedding = data[0].embedding;

    // 2. Search in Convex
    const results = await ctx.vectorSearch('documents', 'by_embedding', {
      vector: embedding,
      limit: 10,
      filter: args.category
        ? q => q.eq('category', args.category)
        : undefined,
    });

    // 3. Fetch full documents
    const docs = await Promise.all(
      results.map(r => ctx.runQuery(api.documents.get, { id: r._id }))
    );

    return docs.map((doc, i) => ({
      ...doc,
      score: results[i]._score,
    }));
  },
});

Components (Beta)

Reusable modules with namespaced resources:

// Define component
import { defineComponent } from 'convex/server';

export default defineComponent('myComponent');

// Use in app
import { defineApp } from 'convex/server';
import myComponent from './myComponent/convex.config';

export default defineApp({
  myComponent: myComponent.use({ /* config */ }),
});

// Access from code
import { components } from './_generated/api';

await ctx.runQuery(components.myComponent.queries.getData, {});

API Utilities

// anyApi: for projects without codegen
import { anyApi } from 'convex/server';
const result = await anyApi.module.function();

// Function name utilities
import { getFunctionName, makeFunctionReference } from 'convex/server';

const name = getFunctionName(api.messages.list);  // "messages:list"
const ref = makeFunctionReference<'query'>('messages:list');

// Filter API
import { filterApi } from 'convex/server';

const queryOnlyApi = filterApi(
  api,
  (func): func is FunctionReference<'query'> => func._type === 'query'
);

Search Indexes (Full-text)

// Schema
defineTable({
  title: v.string(),
  body: v.string(),
  author: v.string(),
  category: v.string(),
}).searchIndex('search_body', {
  searchField: 'body',
  filterFields: ['author', 'category'],
})

// Query
export const search = query({
  args: {
    searchTerm: v.string(),
    author: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    let q = ctx.db.query('posts')
      .withSearchIndex('search_body', q => q.search('body', args.searchTerm));

    if (args.author) {
      q = q.withSearchIndex('search_body', q =>
        q.search('body', args.searchTerm).eq('author', args.author)
      );
    }

    return await q.collect();
  },
});

Query Journals (Debugging)

import { BaseConvexClient } from 'convex/browser';

const client = new BaseConvexClient(url, onTransition);

const watch = client.watch(api.query, {}, { journal: true });

watch.onUpdate(() => {
  const journal = watch.journal();
  console.log('Execution time:', journal?.executionTime, 'ms');
  console.log('Cached:', journal?.cached);
  console.log('Log:', journal?.lines);
});