CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-convex

TypeScript backend SDK, client library, and CLI for building real-time applications on the Convex platform

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

Convex

TypeScript-first backend platform: real-time database + serverless functions + reactive queries.

Package: npm install convex (v1.29.3)

Quick Start

// Schema (convex/schema.ts)
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';

export default defineSchema({
  messages: defineTable({
    author: v.string(),
    body: v.string(),
  }).index('by_author', ['author']),
});

// Backend (convex/messages.ts)
import { query, mutation } from './_generated/server';
import { v } from 'convex/values';

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

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

// React (src/App.tsx)
import { ConvexProvider, ConvexReactClient, useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

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

function Messages() {
  const messages = useQuery(api.messages.list);
  const send = useMutation(api.messages.send);
  return messages?.map(m => <div key={m._id}>{m.body}</div>);
}

function App() {
  return <ConvexProvider client={convex}><Messages /></ConvexProvider>;
}

Core Imports

// Server functions
import { query, mutation, action, internalQuery, internalMutation, internalAction, httpAction } from 'convex/server';

// Schema & validation
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';

// React
import { ConvexProvider, ConvexReactClient, useQuery, useMutation, useAction, usePaginatedQuery } from 'convex/react';

// Browser (non-React)
import { ConvexClient, ConvexHttpClient } from 'convex/browser';

// Next.js
import { preloadQuery, fetchQuery, fetchMutation, fetchAction } from 'convex/nextjs';

// Auth providers
import { ConvexProviderWithAuth0 } from 'convex/react-auth0';
import { ConvexProviderWithClerk } from 'convex/react-clerk';

Function Types

Query (Read-only, Reactive)

export const get = query({
  args: { id: v.id('table') },
  handler: async (ctx, args) => {
    // ctx.db: read-only, ctx.auth, ctx.storage.getUrl()
    return await ctx.db.get(args.id);
  },
});

Mutation (Atomic Writes)

export const create = mutation({
  args: { text: v.string() },
  handler: async (ctx, args) => {
    // ctx.db: read-write (insert/patch/replace/delete)
    // ctx.scheduler: schedule functions, ctx.storage: generateUploadUrl()
    return await ctx.db.insert('table', { text: args.text });
  },
});

Action (External APIs, Non-transactional)

export const sendEmail = action({
  args: { to: v.string(), body: v.string() },
  handler: async (ctx, args) => {
    // ctx.runQuery/runMutation/runAction, ctx.vectorSearch, ctx.storage.get/store()
    await fetch('https://api.email.com/send', { method: 'POST', body: JSON.stringify(args) });
    await ctx.runMutation(api.logs.record, { event: 'email_sent' });
  },
});

Server Functions | Database Operations | Schema

Validators

// Primitives
v.string(), v.number(), v.bigint(), v.boolean(), v.null(), v.bytes()

// Structures
v.array(v.string())
v.object({ name: v.string(), age: v.number() })
v.record(v.string(), v.any())

// Special
v.id('tableName')  // Document ID
v.optional(v.string())  // Optional field
v.union(v.literal('a'), v.literal('b'))  // Union type
v.nullable(v.string())  // string | null

// Usage
const userValidator = v.object({
  name: v.string(),
  age: v.number(),
  email: v.optional(v.string()),
});
type User = Infer<typeof userValidator>;  // { name: string; age: number; email?: string }

Values & Validators

Database

// Fetch by ID
const doc = await ctx.db.get(id);

// Query
await ctx.db.query('table').collect();  // All
await ctx.db.query('table').order('desc').take(10);  // First 10
await ctx.db.query('table').filter(q => q.gt(q.field('age'), 18)).collect();

// Index query
await ctx.db.query('table')
  .withIndex('by_author', q => q.eq('author', 'Alice'))
  .collect();

// Range query
await ctx.db.query('table')
  .withIndex('by_time', q => q.gte('_creationTime', startTime).lt('_creationTime', endTime))
  .collect();

// Write (mutations only)
const id = await ctx.db.insert('table', { field: 'value' });
await ctx.db.patch(id, { field: 'newValue' });
await ctx.db.replace(id, { field: 'completelyNew' });
await ctx.db.delete(id);

Database Operations

React Hooks

// Query (reactive)
const data = useQuery(api.module.queryFn, { arg: 'value' });
if (data === undefined) return <div>Loading...</div>;

// Skip conditionally
const data = useQuery(api.module.queryFn, shouldLoad ? { arg: 'value' } : 'skip');

// Mutation
const mutateFn = useMutation(api.module.mutationFn);
await mutateFn({ arg: 'value' });

// Optimistic update
const like = useMutation(api.posts.like).withOptimisticUpdate((store, args) => {
  const posts = store.getQuery(api.posts.list);
  if (posts) {
    store.setQuery(api.posts.list, {}, posts.map(p =>
      p._id === args.postId ? { ...p, likes: p.likes + 1 } : p
    ));
  }
});

// Action
const actionFn = useAction(api.module.actionFn);
await actionFn({ arg: 'value' });

// Pagination
const { results, status, loadMore } = usePaginatedQuery(
  api.module.list,
  { filter: 'value' },
  { initialNumItems: 20 }
);

React Integration

Next.js Integration

// Server Component
import { preloadQuery } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';

export default async function Page() {
  const preloaded = await preloadQuery(api.messages.list);
  return <ClientComponent preloaded={preloaded} />;
}

// Client Component
'use client';
import { usePreloadedQuery } from 'convex/react';

function ClientComponent({ preloaded }) {
  const messages = usePreloadedQuery(preloaded);  // Reactive
  return <div>{messages.map(m => <p key={m._id}>{m.body}</p>)}</div>;
}

// Server Action
import { fetchMutation } from 'convex/nextjs';

export async function createPost(formData: FormData) {
  'use server';
  const title = formData.get('title') as string;
  await fetchMutation(api.posts.create, { title });
  revalidatePath('/posts');
}

// With auth (Clerk)
import { auth } from '@clerk/nextjs';
const { getToken } = auth();
const token = await getToken({ template: 'convex' });
const data = await fetchQuery(api.users.getCurrent, {}, { token });

Next.js Integration

Authentication

// Server-side
export const getCurrentUser = query({
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return null;
    return {
      id: identity.tokenIdentifier,
      name: identity.name,
      email: identity.email,
    };
  },
});

// React with Auth0
import { ConvexProviderWithAuth0 } from 'convex/react-auth0';
import { Auth0Provider } from '@auth0/auth0-react';

<Auth0Provider domain="..." clientId="...">
  <ConvexProviderWithAuth0 client={convex}>
    <App />
  </ConvexProviderWithAuth0>
</Auth0Provider>

// React with Clerk
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ClerkProvider, useAuth } from '@clerk/clerk-react';

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

// Conditional rendering
import { Authenticated, Unauthenticated } from 'convex/react';

<Authenticated><Dashboard /></Authenticated>
<Unauthenticated><Login /></Unauthenticated>

Authentication

Advanced Features

Pagination

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

// React
const { results, status, loadMore } = usePaginatedQuery(api.module.list, {}, { initialNumItems: 20 });

File Storage

// Upload
export const generateUploadUrl = mutation(async (ctx) => await ctx.storage.generateUploadUrl());

// Client
const url = await convex.mutation(api.files.generateUploadUrl);
const result = await fetch(url, { method: 'POST', body: file });
const { storageId } = await result.json();

// Download
export const getUrl = query({
  args: { storageId: v.string() },
  handler: async (ctx, args) => await ctx.storage.getUrl(args.storageId),
});

Scheduling

// Schedule function
export const schedule = mutation({
  handler: async (ctx) => {
    await ctx.scheduler.runAfter(60000, internal.task.run, { data: 'value' });
    await ctx.scheduler.runAt(timestamp, internal.task.run, { data: 'value' });
  },
});

// Cron (convex/crons.ts)
import { cronJobs } from 'convex/server';
const crons = cronJobs();
crons.daily('cleanup', { hourUTC: 2, minuteUTC: 0 }, internal.tasks.cleanup);
export default crons;

HTTP Actions

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

const http = httpRouter();
http.route({
  path: '/webhook',
  method: 'POST',
  handler: httpAction(async (ctx, request) => {
    const body = await request.json();
    await ctx.runMutation(api.webhooks.process, body);
    return new Response(JSON.stringify({ success: true }), {
      headers: { 'Content-Type': 'application/json' },
    });
  }),
});
export default http;

Vector Search

export const search = action({
  args: { embedding: v.array(v.float64()) },
  handler: async (ctx, args) => {
    const results = await ctx.vectorSearch('documents', 'by_embedding', {
      vector: args.embedding,
      limit: 10,
      filter: q => q.eq('category', 'news'),
    });
    return results;
  },
});

Advanced Features

Type System

// Document types from schema
type Message = Doc<'messages'>;  // Includes _id, _creationTime
type MessageInput = WithoutSystemFields<Message>;  // For inserts

// Function references
api.module.functionName  // Type-safe reference
internal.module.functionName  // Internal functions

// Extract types from validators
type User = Infer<typeof userValidator>;

// Field paths
type MessageFieldPaths = FieldPaths<'messages'>;  // 'author' | 'body' | '_id' | '_creationTime'

Common Patterns

Authentication Check

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

Error Handling

import { ConvexError } from 'convex/values';

throw new ConvexError({ code: 'NOT_FOUND', id: args.id });

Unique Field Lookup

const user = await ctx.db.query('users')
  .withIndex('by_email', q => q.eq('email', args.email))
  .unique();

Transaction Pattern

export const transfer = mutation({
  handler: async (ctx, args) => {
    const from = await ctx.db.get(args.fromId);
    const to = await ctx.db.get(args.toId);
    // All succeed or all fail
    await ctx.db.patch(args.fromId, { balance: from.balance - args.amount });
    await ctx.db.patch(args.toId, { balance: to.balance + args.amount });
  },
});

Links

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/convex@1.29.x
Publish Source
CLI
Badge
tessl/npm-convex badge