Skip to main content
Answer Overflow’s backend is built on Convex, organizing database functions into three access levels:
  • Public - Read-only queries accessible without authentication
  • Authenticated - Functions requiring user authentication
  • Private - Internal functions for bot and admin operations

Function Organization

All Convex functions are located in packages/database/convex/ and organized by access level:
convex/
├── public/          # Public queries
├── authenticated/   # Authenticated queries and mutations  
├── private/         # Private queries, mutations, and actions
└── client/          # Client-specific function wrappers

Public Functions

Public functions are read-only queries that don’t require authentication. They use the publicQuery wrapper which provides caching and rate limiting.

Custom Function Wrapper

All public functions use the publicQuery wrapper defined in /convex/public/custom_functions.ts:
import { publicQuery } from "../public/custom_functions";

export const getMessages = publicQuery({
  args: { channelId: v.int64() },
  handler: async (ctx, args) => {
    // Query implementation
  }
});
args
object
Automatically includes these fields in addition to your custom args:
ctx
QueryCtxWithCache
Enhanced context with caching capabilities:
  • ctx.cache.getMessage(id) - Get cached message
  • ctx.cache.getChannel(id) - Get cached channel
  • ctx.cache.getServer(id) - Get cached server
  • ctx.cache.getChannelSettings(id) - Get cached channel settings
  • Standard Convex ctx.db for database access

Message Queries

Defined in /convex/public/messages.ts

getMessages

Get paginated messages from a channel.
await ctx.runQuery(api.public.messages.getMessages, {
  channelId: 123456789n,
  after: 0n,
  paginationOpts: { numItems: 50 }
})
channelId
bigint
required
Discord channel ID
after
bigint
required
Message ID to start after (for pagination)
paginationOpts
PaginationOptions
required
Convex pagination options
page
EnrichedMessage[]
Array of enriched messages with author and attachment data
isDone
boolean
Whether pagination is complete
continueCursor
string
Cursor for next page

getMessagePageHeaderData

Get all data needed to render a message page header.
await ctx.runQuery(api.public.messages.getMessagePageHeaderData, {
  messageId: 123456789n
})
messageId
bigint
required
Discord message ID
canonicalId
bigint
The canonical ID for the thread/message
firstMessage
EnrichedMessage
The first message in the thread
solutionMessage
EnrichedMessage | null
The marked solution message, if any
server
object
Server information including name, icon, customDomain, etc.
channel
object
Channel information including name, type, availableTags
thread
object | null
Thread information if this is a thread
replyCount
number
Number of replies in the thread
threadTagIds
bigint[]
IDs of tags applied to the thread

getMessageAsSearchResult

Get a message formatted as a search result.
await ctx.runQuery(api.public.messages.getMessageAsSearchResult, {
  messageId: 123456789n
})
Returns message, channel, server, and thread data formatted for search result display.

Server Queries

Defined in /convex/public/servers.ts

getServerByDomain

Get server information by custom domain.
await ctx.runQuery(api.public.servers.getServerByDomain, {
  domain: "help.example.com"
})

getServerByDiscordId

Get server information by Discord server ID.
await ctx.runQuery(api.public.servers.getServerByDiscordId, {
  discordId: 393088095840370689n
})

Channel Queries

Defined in /convex/public/channels.ts

getChannelById

Get channel information and settings.
await ctx.runQuery(api.public.channels.getChannelById, {
  channelId: 123456789n
})
channel
object
Channel data including name, type, parent channel, and available tags
settings
object
Channel settings including indexingEnabled, markSolutionEnabled, etc.

Search Queries

Defined in /convex/public/search.ts

searchMessages

Search messages across indexed servers and channels.
await ctx.runQuery(api.public.search.searchMessages, {
  query: "how to setup webhooks",
  filters: {
    serverIds: [123456789n],
    channelIds: [987654321n]
  },
  limit: 20
})

Authenticated Functions

Authenticated functions require a valid user session. They use custom wrappers that validate authentication:
import { authenticatedQuery, authenticatedMutation } from "../client/authenticated";

Guild Manager Functions

Defined in /convex/client/guildManager.ts These functions are for users with “Manage Guild” permissions on a Discord server.

getThreadsForServer

Get paginated threads for a server.
await ctx.runQuery(api.authenticated.threads.getThreadsForServer, {
  serverId: 123456789n,
  paginationOpts: { numItems: 50 },
  sortOrder: "newest"
})
serverId
bigint
required
Discord server ID (auto-injected from auth)
paginationOpts
PaginationOptions
required
Pagination options
sortOrder
'newest' | 'oldest'
Sort order for threads (default: “newest”)
page
DashboardThreadItem[]
Array of threads with:
  • thread - Thread channel data
  • message - First message in thread
  • parentChannel - Parent channel info
  • tags - Applied forum tags

User Server Settings

Defined in /convex/authenticated/user_server_settings.ts

getUserServerSettings

Get user-specific settings for a server.
await ctx.runQuery(api.authenticated.user_server_settings.getUserServerSettings, {
  serverId: 123456789n
})
Returns privacy settings, notification preferences, etc.

updateUserServerSettings

Update user server settings.
await ctx.runMutation(api.authenticated.user_server_settings.updateUserServerSettings, {
  serverId: 123456789n,
  settings: {
    notificationsEnabled: false,
    hideFromLeaderboard: true
  }
})

Private Functions

Private functions are for internal use by the Discord bot, API keys, and admin operations. They provide full read/write access.

Message Functions

Defined in /convex/private/messages.ts

upsertMessage

Insert or update a message.
await ctx.runMutation(api.private.messages.upsertMessage, {
  serverId: 123456789n,
  channelId: 987654321n,
  id: 111222333n,
  authorId: 444555666n,
  content: "Message content",
  createdAt: new Date(),
  childThreadId: null,
  parentChannelId: null,
  solutionIds: []
})
serverId
bigint
required
Discord server ID
channelId
bigint
required
Discord channel ID
id
bigint
required
Discord message ID
authorId
bigint
required
Discord user ID of author
content
string
required
Message text content
ignoreChecks
boolean
Skip validation checks (default: false)

markMessageAsSolution

Mark a message as the solution to a question.
await ctx.runMutation(api.private.messages.markMessageAsSolution, {
  questionMessageId: 111222333n,
  solutionMessageId: 444555666n
})
questionMessageId
bigint
required
ID of the question message
solutionMessageId
bigint
required
ID of the solution message

unmarkSolution

Remove solution marking from a message.
await ctx.runMutation(api.private.messages.unmarkSolution, {
  questionMessageId: 111222333n
})

Server Functions

Defined in /convex/private/servers.ts

upsertServer

Insert or update a Discord server.
await ctx.runMutation(api.private.servers.upsertServer, {
  discordId: 123456789n,
  name: "My Discord Server",
  icon: "icon-hash",
  description: "A helpful community",
  approximateMemberCount: 5000
})

getServerByDiscordId

Get full server data by Discord ID.
await ctx.runQuery(api.private.servers.getServerByDiscordId, {
  discordId: 123456789n
})

Channel Functions

Defined in /convex/private/channels.ts

upsertChannel

Insert or update a channel.
await ctx.runMutation(api.private.channels.upsertChannel, {
  serverId: 123456789n,
  id: 987654321n,
  name: "help",
  type: 0, // ChannelType enum
  parentId: null,
  flags: {
    indexingEnabled: true,
    markSolutionEnabled: true
  }
})

updateChannelSettings

Update channel settings.
await ctx.runMutation(api.private.channels.updateChannelSettings, {
  channelId: 987654321n,
  flags: {
    indexingEnabled: false,
    solutionTagId: 111222333n
  }
})

Discord Account Functions

Defined in /convex/private/discord_accounts.ts

upsertDiscordAccount

Insert or update a Discord account.
await ctx.runMutation(api.private.discord_accounts.upsertDiscordAccount, {
  id: 123456789n,
  name: "username",
  avatar: "avatar-hash"
})

Data Access Patterns

Caching

Public queries include an enhanced context with caching:
export const myQuery = publicQuery({
  args: {},
  handler: async (ctx, args) => {
    // Cached access (recommended)
    const message = await ctx.cache.getMessage(messageId);
    const channel = await ctx.cache.getChannel(channelId);
    const server = await ctx.cache.getServer(serverId);
    
    // Direct database access (when needed)
    const messages = await ctx.db
      .query("messages")
      .withIndex("by_channelId", q => q.eq("channelId", channelId))
      .collect();
    
    return { message, channel, messages };
  }
});

Pagination

Use Convex’s built-in pagination:
const result = await ctx.db
  .query("messages")
  .withIndex("by_channelId_and_id", q => 
    q.eq("channelId", channelId).gt("id", afterId)
  )
  .order("asc")
  .paginate(paginationOpts);

return {
  page: result.page,
  isDone: result.isDone,
  continueCursor: result.continueCursor
};

Enrichment

Use helper functions to enrich data with relationships:
import { enrichMessages } from "../shared/dataAccess";

const messages = await ctx.db.query("messages").collect();
const enriched = await enrichMessages(ctx, messages);
// enriched includes author data, attachments, reactions, etc.

Access Control

Backend Access Tokens

For server-to-server API calls, use backend access tokens:
// In your API route or action
const result = await convex.query(api.public.messages.getMessages, {
  channelId: 123n,
  after: 0n,
  paginationOpts: { numItems: 50 },
  publicBackendAccessToken: process.env.PUBLIC_BACKEND_ACCESS_TOKEN
});

Guild Manager Permissions

Guild manager functions automatically check if the user has “Manage Guild” permission:
export const guildManagerQuery = customQuery(query, {
  args: {
    serverId: v.int64(),
    // User auth automatically validated
  },
  input: async (ctx, args) => {
    // Validates user has Manage Guild permission for serverId
    await validateGuildManagerPermission(ctx, args.serverId);
    return { ctx, args };
  }
});

API Key Access

API key functions validate the key before executing:
import { apiKeyMutation } from "../client/apiKey";

export const myFunction = apiKeyMutation({
  args: { /* ... */ },
  handler: async (ctx, args) => {
    // API key already validated
    // ctx includes apiKey info
  }
});

Database Schema

Core Tables

  • messages - Discord messages
  • channels - Discord channels and threads
  • servers - Discord servers/guilds
  • discord_accounts - Discord user accounts
  • channel_settings - Per-channel configuration
  • server_preferences - Per-server settings
  • user_server_settings - User-specific server settings
  • threadTags - Forum thread tags
  • attachments - Message attachments

Indexes

Key indexes for performance:
  • messages.by_channelId_and_id - Get messages in a channel
  • messages.by_serverId - Get all server messages
  • channels.by_serverId_and_id - Get server channels
  • threadTags.by_threadId - Get tags for a thread

Error Handling

Convex functions throw errors that propagate to the client:
export const myMutation = privateMutation({
  args: { messageId: v.int64() },
  handler: async (ctx, args) => {
    const message = await ctx.db.get(args.messageId);
    
    if (!message) {
      throw new Error("Message not found");
    }
    
    if (!message.channelId) {
      throw new Error("Message has no channel");
    }
    
    // Process message...
  }
});
Handle errors on the client:
try {
  await convex.mutation(api.private.messages.myMutation, {
    messageId: 123n
  });
} catch (error) {
  console.error("Mutation failed:", error.message);
}

Build docs developers (and LLMs) love