Skip to main content

API Organization

Evaly’s backend API is organized by feature domain in the convex/ directory. All functions are written in TypeScript and automatically type-checked.

Directory Structure

convex/
├── organizer/          # Organizer-facing API
│   ├── test.ts         # Test CRUD operations
│   ├── question.ts     # Question management
│   ├── testSection.ts  # Section management
│   ├── organization.ts # Organization management
│   ├── invitation.ts   # Invitation system
│   ├── grading.ts      # Manual grading
│   ├── testResult.ts   # Results and analytics
│   ├── testMonitoring.ts # Live test monitoring
│   ├── testAccess.ts   # Access control
│   ├── testActivity.ts # Activity logs
│   ├── userGroup.ts    # Participant groups
│   ├── questionLibrary.ts # Question banks
│   ├── dashboard.ts    # Dashboard stats
│   ├── notifications.ts # Notification preferences
│   ├── profile.ts      # Organizer profile
│   ├── billing.ts      # Subscription management
│   ├── subscription.ts # Plan management
│   ├── aiQuestions.ts  # AI question generation
│   └── editorMedia.ts  # Rich text editor uploads
├── participant/        # Participant-facing API
├── office/            # Internal admin API
├── ai/                # AI integration functions
├── common/            # Shared utilities
├── internal/          # Scheduled/internal functions
└── schemas/           # Database schemas

Function Types

Queries are read-only functions that fetch data.
export const getTests = query({
  args: { /* ... */ },
  handler: async (ctx, args) => {
    // Read data from database
    return data;
  }
});
Characteristics:
  • Automatically cached
  • Real-time reactive updates
  • Cannot modify database
  • Can be called from frontend with useQuery()

Organizer API

Test Management

Queries:
  • getTests({ paginationOpts, status?, search?, sort? }) - List tests with filtering and pagination
  • getTestById({ testId }) - Get single test by ID
Mutations:
  • createTest() - Create new draft test
  • updateTest({ testId, data }) - Update test metadata
  • deleteTest({ testId }) - Soft delete test
  • duplicateTest({ testId }) - Duplicate test with all sections/questions
  • publishTest({ testId, startOption, scheduledStartAt?, scheduledEndAt? }) - Publish test
  • stopTest({ testId, reason? }) - Stop/unpublish test
  • pauseTest({ testId }) - Pause active test
  • resumeTest({ testId }) - Resume paused test
  • extendTestTime({ testId, additionalMinutes?, newEndTime? }) - Extend test duration
  • updateTestSchedule({ testId, scheduledEndAt? }) - Update test schedule
  • toggleUseSectionDurations({ testId, useSectionDurations }) - Toggle duration mode
  • toggleResultsReleased({ testId, resultsReleased }) - Toggle results visibility
Helpers:
  • checkTestOwnership(ctx, testId) - Verify user owns test
Queries:
  • getTestSections({ testId }) - Get all sections for a test
  • getSectionById({ sectionId }) - Get single section
Mutations:
  • createSection({ testId, title, description?, order }) - Create new section
  • updateSection({ sectionId, title?, description?, duration? }) - Update section
  • deleteSection({ sectionId }) - Delete section
  • reorderSections({ testId, sectionIds }) - Reorder sections
  • updateSectionDuration({ sectionId, duration }) - Set section time limit
Helpers:
  • validateSectionDurationsAgainstTestWindow(ctx, testId) - Validate durations fit schedule
Queries:
  • getQuestions({ referenceId }) - Get questions for section/library
  • getQuestionById({ questionId }) - Get single question
Mutations:
  • createQuestion({ referenceId, question, type, options?, ... }) - Create question
  • updateQuestion({ questionId, question?, type?, options?, ... }) - Update question
  • deleteQuestion({ questionId }) - Delete question
  • duplicateQuestion({ questionId }) - Duplicate question
  • reorderQuestions({ referenceId, questionIds }) - Reorder questions
  • duplicateQuestionsFromLibrary({ sourceLibraryId, targetSectionId, questionIds }) - Copy from library
Supported Question Types:
  • multiple-choice - Traditional multiple choice
  • yes-or-no - Boolean questions
  • image-choice - Image-based options
  • audio-choice - Audio-based options
  • text-field - Short/long text answers
  • file-upload - File submission
  • fill-the-blank - Fill-in-the-blank
  • audio-response - Audio recording
  • video-response - Video recording
  • matching-pairs - Match items
  • slider-scale - Numeric slider
  • likert-scale - Agreement scale
  • ranking - Rank items in order
Queries:
  • getLibraries({ paginationOpts, search? }) - List question libraries
  • getLibraryById({ libraryId }) - Get single library
Mutations:
  • createLibrary({ name, description? }) - Create question bank
  • updateLibrary({ libraryId, name?, description? }) - Update library
  • deleteLibrary({ libraryId }) - Delete library

Test Access & Monitoring

Queries:
  • getTestParticipants({ testId }) - Get allowed participants
  • getTestParticipantGroups({ testId }) - Get allowed user groups
Mutations:
  • addParticipant({ testId, email }) - Allow individual participant
  • removeParticipant({ testId, participantId }) - Remove participant
  • addParticipantGroup({ testId, userGroupId }) - Allow user group
  • removeParticipantGroup({ testId, groupId }) - Remove user group
  • updateAccessControl({ testId, password?, allowedEmailDomains?, allowedIpAddresses? }) - Set access restrictions
Queries:
  • getLiveTestStatus({ testId }) - Real-time test status
  • getActiveParticipants({ testId }) - Currently active participants
  • getParticipantProgress({ testId, participantId }) - Individual progress
  • getTestPresence({ testId }) - Presence/heartbeat data
Features:
  • Real-time participant join/leave
  • Live progress tracking
  • Section completion monitoring
  • Heartbeat-based presence
Queries:
  • getActivityLog({ testId, paginationOpts }) - Get test activity history
Event Types:
  • test_started - Test began
  • test_ended - Test finished
  • test_paused - Test paused
  • test_resumed - Test resumed
  • time_extended - Duration extended
  • participant_joined - Participant started test
  • participant_submitted - Participant submitted answers

Results & Grading

Queries:
  • getTestResults({ testId, paginationOpts }) - Get all participant results
  • getParticipantResult({ testId, participantId }) - Individual result
  • getResultsAnalytics({ testId }) - Aggregate statistics
  • exportResults({ testId, format }) - Export data
Analytics:
  • Average scores
  • Completion rates
  • Question difficulty analysis
  • Time-to-complete metrics
  • Score distribution
Queries:
  • getParticipantSubmission({ testId, participantId }) - Get submission with answers
  • getUngradedSubmissions({ testId }) - Find submissions needing grading
Mutations:
  • gradeAnswer({ testAttemptId, questionId, pointsAwarded, feedback? }) - Grade single answer
  • bulkGradeAnswers({ grades }) - Grade multiple answers at once
Use Cases:
  • Text field responses
  • File uploads
  • Audio/video responses
  • Subjective questions

Organization Management

Queries:
  • getOrganization({ organizationId }) - Get organization details
  • getUserOrganizations() - Get user’s organizations
Mutations:
  • createOrganization({ name, type, image? }) - Create new organization
  • updateOrganization({ organizationId, name?, image? }) - Update org
  • switchOrganization({ organizationId }) - Switch active organization
  • deleteOrganization({ organizationId }) - Delete organization
Queries:
  • getPendingInvitations({ organizationId }) - List pending invites
  • getInvitationByToken({ token }) - Verify invitation token
Mutations:
  • createInvitation({ organizationId, email, role }) - Invite member
  • acceptInvitation({ token }) - Accept invitation
  • revokeInvitation({ invitationId }) - Cancel invitation
  • resendInvitation({ invitationId }) - Resend email
Features:
  • 7-day token expiration
  • Email notifications via Plunk
  • Role-based invitations (owner, admin, member)
Queries:
  • getUserGroups({ paginationOpts, search? }) - List groups
  • getGroupById({ groupId }) - Get single group
  • getGroupMembers({ groupId }) - Get group members
Mutations:
  • createGroup({ name }) - Create user group
  • updateGroup({ groupId, name }) - Update group
  • deleteGroup({ groupId }) - Delete group
  • addGroupMember({ groupId, email }) - Add member
  • removeGroupMember({ groupId, memberId }) - Remove member
  • bulkAddMembers({ groupId, emails }) - Add multiple members

Subscription & Billing

Queries:
  • getCurrentPlan({ organizationId }) - Get active plan
  • getUsage({ organizationId, month? }) - Get current usage
  • getAvailablePlans() - List available plans
Plan Types:
  • free - Free tier with basic limits
  • pro - Professional tier with higher limits
  • max - Maximum tier with unlimited features
Limits:
  • AI question generation (monthly)
  • AI translation (monthly)
  • AI options generation (monthly)
  • AI analysis (monthly)
  • Test results (monthly)
  • Team members (real-time)
  • Active tests (real-time)
  • Concurrent participants (real-time)
Mutations:
  • upgradePlan({ organizationId, plan }) - Upgrade subscription
  • downgradePlan({ organizationId, plan }) - Downgrade subscription
  • setCustomLimits({ organizationId, customLimits }) - Override plan limits (admin)
Integrations:
  • Polar.sh for payment processing
  • Usage tracking and enforcement
  • Automatic limit checks

AI Features

Queries:
  • getAIThread({ referenceId }) - Get conversation thread
  • getAIUsage({ organizationId }) - Get AI usage stats
Actions:
  • generateQuestions({ referenceId, prompt, count, type }) - Generate questions with AI
  • suggestOptions({ questionText, count }) - Generate answer options
  • translateQuestion({ questionId, targetLanguage }) - Translate question
  • analyzeResults({ testId }) - AI analysis of test results
Features:
  • Google Gemini integration
  • Conversation context preservation
  • Usage tracking and limits
  • Multiple question types supported
Mutations:
  • acceptGeneratedQuestion({ threadId, questionData }) - Accept AI suggestion
  • rejectGeneratedQuestion({ threadId, questionId }) - Reject suggestion
  • refineQuestion({ questionId, refinementPrompt }) - Refine with AI

Dashboard & Profile

Queries:
  • getDashboardStats() - Overview statistics
    • Total tests (draft, active, scheduled, finished)
    • Total participants
    • Total questions
    • Recent activity
  • getRecentTests({ limit }) - Recent test list
  • getActiveTestsSummary() - Active tests overview
  • getTrendData({ metric, period }) - Trend analysis
Queries:
  • getProfile() - Get user profile
Mutations:
  • updateProfile({ name?, email?, avatar? }) - Update profile
  • updatePreferences({ preferences }) - Update user preferences
Queries:
  • getNotifications({ paginationOpts }) - Get notification batches
  • getUnreadCount() - Get unread notification count
  • getNotificationPreferences() - Get notification settings
Mutations:
  • markAsRead({ batchKey }) - Mark notification batch as read
  • markAllAsRead() - Mark all as read
  • updatePreferences({ preferences }) - Update notification preferences
Notification Types:
  • Participant joined test
  • Participant completed test
  • Test results available
  • Manual grading needed
  • Test limit reached
  • Member joined organization

Editor & Media

Mutations:
  • generateUploadUrl({ filename, contentType }) - Get upload URL for rich text images
  • deleteMedia({ url }) - Delete uploaded media
Supported:
  • Images in question text
  • Images in answer options
  • Audio files
  • Video files
  • Documents

Participant API

Located in convex/participant/ directory.
Queries:
  • getTest({ testId, accessToken? }) - Get test for participant
  • getTestSections({ testId }) - Get sections for participant
  • getCurrentAttempt({ testId, sectionId }) - Get active attempt
Mutations:
  • startTest({ testId, accessToken? }) - Start test attempt
  • startSection({ testId, sectionId }) - Start section attempt
  • submitAnswer({ attemptId, questionId, answer }) - Submit answer
  • flagQuestion({ attemptId, questionId }) - Flag for review
  • finishSection({ attemptId }) - Complete section
  • finishTest({ testId }) - Complete test
Queries:
  • getMyResults({ testId }) - Get participant’s results (if released)
  • getMyAnswers({ testId }) - Get participant’s submitted answers

Internal API

Located in convex/internal/ directory. Only callable by backend.
Internal Mutations:
  • activateTest({ testId }) - Activate scheduled test
  • finishTest({ testId }) - Finish test at scheduled time
  • cleanupStalePresence() - Remove stale presence records
  • resetMonthlyUsage() - Reset usage counters (monthly cron)
Internal Actions:
  • sendInvitationEmail({ email, token, organizationName }) - Send invite
  • sendTestReminderEmail({ testId, participantEmail }) - Send reminder
Internal Mutations:
  • createNotification({ organizationId, type, ... }) - Create notification event
  • batchNotifications({ organizationId }) - Group notifications

Common Utilities

Located in convex/common/ directory.
// Helpers for authorization
await requireAuth(ctx);  // Throws if not authenticated
await requireOrganizationMember(ctx, organizationId);  // Throws if not member
await requireOrganizationOwner(ctx, organizationId);  // Throws if not owner
await requireCanManageOrganization(ctx, organizationId);  // Owner or admin
// R2 storage helpers
const url = keyToPublicUrl(key);  // Convert key to CDN URL
const key = publicUrlToKey(url);  // Extract key from URL
// Plan limit helpers
await checkActiveTestsLimit(ctx, organizationId);  // Check if can create test
await incrementUsage(ctx, organizationId, 'aiQuestionGeneration');  // Track usage
await checkUsageLimit(ctx, organizationId, 'aiQuestionGeneration');  // Enforce limits
// Email utilities (Plunk integration)
await sendEmail({ to, subject, html });  // Send transactional email

API Conventions

Input Validation

All function arguments are validated using Convex validators:
import { v } from "convex/values";

export const myFunction = mutation({
  args: {
    testId: v.id("test"),
    title: v.string(),
    isPublished: v.boolean(),
    tags: v.optional(v.array(v.string())),
  },
  handler: async (ctx, args) => {
    // args are type-safe and validated
  }
});

Error Handling

Always use ConvexError for throwing errors:
import { ConvexError } from "convex/values";

throw new ConvexError({
  message: "Not authorized",
  code: "UNAUTHORIZED",
  display: "dialog",
});

Pagination

Use Convex’s pagination pattern:
import { paginationOptsValidator } from "convex/server";

export const getItems = query({
  args: {
    paginationOpts: paginationOptsValidator,
  },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("items")
      .paginate(args.paginationOpts);
  }
});

Soft Deletes

Always filter out soft-deleted records:
const items = await ctx.db
  .query("items")
  .filter((q) => q.lte(q.field("deletedAt"), 0))
  .collect();

Type Safety

All Convex functions are fully type-safe:
  • Arguments are validated at runtime
  • Return types are inferred automatically
  • Frontend hooks are type-safe
  • No manual type definitions needed
// Backend
export const getTest = query({
  args: { testId: v.id("test") },
  handler: async (ctx, args) => {
    return await ctx.db.get(args.testId);
  }
});

// Frontend (auto-typed)
const test = useQuery(api.organizer.test.getTest, { testId });
//    ^? test is typed as Test | null | undefined

Rate Limiting

Plan limits are enforced automatically:
  • AI usage tracked per organization per month
  • Active tests limited by plan
  • Concurrent participants limited by plan
  • Results generation limited by plan
Exceeding limits throws a ConvexError with code: "USAGE_LIMIT_EXCEEDED" which displays a dialog prompting upgrade.

Real-Time Updates

All queries automatically subscribe to real-time updates:
// Frontend
const participants = useQuery(
  api.organizer.testMonitoring.getActiveParticipants,
  { testId }
);
// participants updates automatically when data changes
No manual WebSocket management required!

Build docs developers (and LLMs) love