Skip to main content

Overview

Evaly is a test/exam management platform built on a modern, serverless architecture. The platform serves two primary user roles:
  • Organizers: Create and manage tests through the organizer dashboard
  • Participants: Take tests through the participant interface

Tech Stack

  • Framework: TanStack Start (React 19 meta-framework)
  • Routing: File-based routing with TanStack Router
  • Runtime: Bun (not Node.js)
  • Styling: Tailwind CSS v4 with custom design tokens
  • UI Components: shadcn/ui built on Radix UI primitives
  • Rich Text Editor: TipTap for question editing
  • Build Tool: Vite bundler

Architecture Patterns

Route Organization

Evaly uses file-based routing with clear separation of concerns:
src/routes/
├── (organizer)/app/*    # Protected organizer dashboard
├── (participant)/*      # Public-facing test interface
├── (auth)/*            # Authentication flows
└── office/*            # Internal admin panel
Route parameters follow TanStack Router conventions:
  • File naming: s.$testId.index.tsx
  • Parameters: $testId, $attemptId, $token

Data Flow Architecture

1

Backend Functions

All backend logic lives in Convex server functions within the convex/ directory. Functions are organized by feature domain.
2

Type-Safe Hooks

Frontend uses auto-generated Convex React hooks for type-safe data fetching and mutations.
3

Real-time Updates

Convex subscriptions provide automatic real-time updates without manual WebSocket management.
4

Query Integration

TanStack Query handles caching and synchronization for optimal performance.

Component Architecture

Components are organized by purpose and reusability:
components/pages/ - Full page components tied to specific routes. These are feature-complete views that compose multiple shared components.
components/shared/ - Reusable business components like TestCard, QuestionEditor, ParticipantList. These contain business logic and domain knowledge.
components/ui/ - Base UI primitives from shadcn/ui. These are generic, unstyled components like Button, Dialog, Input.
hooks/ - Custom React hooks for business logic abstraction and state management.

Authentication & Authorization

Authentication Flow

Evaly uses Convex Auth for secure, serverless authentication:
  • Multi-provider support (Google OAuth)
  • Secure session management
  • Protected routes with <Authenticated> wrapper
  • Automatic token refresh

Permission System

Role-based access control is enforced through permission helpers in convex/common/permissions.ts:
HelperPurpose
requireAuth()Ensures user is authenticated
requireOrganizationMember()Ensures user belongs to organization
requireOrganizationOwner()Ensures user is organization owner
requireCanManageOrganization()Ensures user is owner or admin

Ownership Checking

All mutations verify ownership before allowing operations:
const { isOwner, test } = await checkTestOwnership(ctx, testId);
if (!isOwner) {
  throw new ConvexError({ message: "Unauthorized" });
}

Multi-Tenancy Architecture

Evaly implements organization-based multi-tenancy:
  • Each user can belong to multiple organizations
  • Users have a selectedOrganizationId and selectedOrganizerId
  • All data is scoped to organizations
  • Invitations use token-based system with 7-day expiry
  • Email invitations sent via Plunk service

Organization Context

Every authenticated request operates within an organization context:
  1. User authenticates and selects an organization
  2. All queries filter by organizationId
  3. Mutations verify organization membership
  4. Data is isolated between organizations

Real-Time Features

Evaly leverages Convex’s real-time capabilities for live collaboration:

Test Presence Tracking

  • Heartbeat System: Tracks active participants during tests
  • Live Status Updates: Real-time participant status changes
  • Automatic Cleanup: Stale presence records are cleaned up

Live Monitoring

Organizers can monitor tests in real-time:
  • Participant join/leave events
  • Test submission notifications
  • Section completion tracking
  • Live attempt progress

Notification System

Insta-style notification batching for organizers:
  • Organization-level: Notifications created per organization
  • Organizer preferences: Each organizer customizes notification types
  • Read tracking: Per-organizer read status
  • Smart batching: Events grouped within time windows

Error Handling Pattern

Always use ConvexError from convex/values for throwing errors in Convex functions. Never use standard JavaScript throw new Error().

Simple Errors (Toast)

import { ConvexError } from "convex/values";

throw new ConvexError({ message: "Not authenticated" });

Rich Errors (Dialog with Actions)

throw new ConvexError({
  message: "Monthly AI question generation limit reached (5/5).",
  code: "USAGE_LIMIT_EXCEEDED",
  display: "dialog",
  title: "AI Generation Limit Reached",
  action: "Upgrade Plan",
  redirect: "/app/settings?tab=billing",
  current: 5,
  limit: 5,
  plan: "free",
});

Display Types

  • toast (default): Shows a toast notification with optional action button
  • dialog: Shows an alert dialog with title, message, and optional action button
  • banner: Falls back to toast (for now)
  • none: Silent error, only logs to console
Errors with codes containing LIMIT_EXCEEDED or LIMIT_REACHED automatically use dialog display.

File Upload & Storage

Cloudflare R2 provides scalable file storage:

Upload Flow

1

Generate Upload URL

Backend generates a signed upload URL using @convex-dev/r2
2

Client Upload

Frontend uploads file directly to R2 (no backend proxy)
3

Sync Metadata

Backend records file metadata in database
4

Cleanup

Old files are deleted automatically

Storage Utilities

Helper functions in convex/common/storage.ts:
  • keyToPublicUrl() - Convert R2 key to CDN URL
  • publicUrlToKey() - Extract key from CDN URL
  • Automatic CDN integration via R2_CDN_URL

Scheduled Functions

Convex scheduler handles time-based operations:

Test Scheduling

// Schedule test activation
const activationJobId = await ctx.scheduler.runAt(
  scheduledStartAt,
  internal.internal.test.activateTest,
  { testId }
);

// Store job ID for cancellation
await ctx.db.patch(testId, { activationJobId });

Job Management

  • Job IDs stored in test records (activationJobId, finishJobId)
  • Cancel existing jobs before scheduling new ones
  • Prevents duplicate scheduled operations

Async Operations

Email sending scheduled asynchronously:
await ctx.scheduler.runAfter(0, internal.internal.email.sendInvitation, {
  email: participant.email,
  testId,
});

Common Patterns

Soft Deletion

Most tables use deletedAt field for soft deletion:
// Delete (soft)
await ctx.db.patch(id, { deletedAt: Date.now() });

// Query (exclude deleted)
.filter((q) => q.lte(q.field("deletedAt"), 0))
// or
.filter((q) => q.eq(q.field("deletedAt"), undefined))

Order Management

Items with order field maintain manual sorting:
  • Update neighboring items when adding/removing
  • Maintain sequence integrity
  • Support drag-and-drop reordering

Question Duplication

Questions can be duplicated from libraries to tests:
  • referenceId - Points to section or library
  • originalReferenceId - Tracks source when duplicated
  • Enables question bank reusability

Testing Strategy

Testing infrastructure is configured using:
  • Test Runner: Vitest
  • Component Testing: React Testing Library
  • Test Files: *.test.tsx or *.spec.tsx
Testing infrastructure is configured but no test files currently exist in the codebase.

Deployment Architecture

Frontend Deployment

bun run deploy  # Build, generate sitemap, deploy to Cloudflare
  • TypeScript path aliases: @/*./src/*
  • Environment: .env.local for Convex deployment
  • Platform: Cloudflare Workers (edge deployment)

Backend Deployment

npx convex deploy  # Deploy backend to Convex Cloud
  • Fully managed by Convex
  • Automatic scaling
  • Built-in monitoring
  • Zero-downtime deployments

Environment Variables

  • CONVEX_DEPLOYMENT - Convex deployment name
  • Auth: AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET, SITE_URL
  • Storage: R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_ENDPOINT, R2_CDN_URL, R2_TOKEN
  • Email: PLUNK_SECRET_API_KEY, PLUNK_API_URL, PLUNK_FROM_EMAIL
  • AI: GOOGLE_GENERATIVE_AI_API_KEY
  • Auth Tokens: JWKS, JWT_PRIVATE_KEY (auto-generated)

Build docs developers (and LLMs) love