Skip to main content
Evaly provides comprehensive access control features to ensure only authorized participants can take your tests. Combine multiple layers of security to protect your assessments.

Access Modes

Public Access

Anyone with the test link can participate:
test.access = "public"
Public tests can still have additional restrictions like password protection or email domain filtering.

Private Access

Only invited participants can access the test:
test.access = "private"
When a test is set to private, participants must be on the allowlist (individual emails or groups) to access it.

Access Restriction Methods

Password Protection

Require a password to access the test:
1

Set Password

await updateAccessSettings({
  testId,
  password: "SecurePass123"
});
2

Participant Verification

Participants must enter the password before starting the test. The password is validated on the server and stored in their test session.
3

Remove Password

await updateAccessSettings({
  testId,
  password: null // Clears password
});

Email Domain Restrictions

Limit access to specific email domains (e.g., company or university domains):
await updateAccessSettings({
  testId,
  allowedEmailDomains: ["company.com", "university.edu"]
});
How it works:
  • Participant email is checked against the allowed domains list
  • Subdomains are supported (e.g., “mit.edu” allows “student.mit.edu”)
  • Case-insensitive matching
// Only allow university emails
await updateAccessSettings({
  testId,
  allowedEmailDomains: [
    "stanford.edu",
    "mit.edu",
    "harvard.edu"
  ]
});
Valid: [email protected], [email protected]Invalid: [email protected], [email protected]

IP Address Whitelisting

Restrict test access to specific IP addresses or ranges:
await updateAccessSettings({
  testId,
  allowedIpAddresses: [
    "192.168.1.100",
    "10.0.0.0/24"
  ]
});
Use cases:
  • Campus-only testing (restrict to university network)
  • Office-only assessments
  • Proctored testing centers

Participant Management

Individual Participants

Add specific participants by email:
1

Add Single Participant

await addParticipant({
  testId,
  email: "[email protected]"
});
Email validation is performed server-side using regex pattern:
/^[^\s@]+@[^\s@]+\.[^\s@]+$/
2

Bulk Add Participants

await addParticipants({
  testId,
  emails: [
    "[email protected]",
    "[email protected]",
    "[email protected]"
  ]
});
Returns results array:
[
  { email: "student1@...", success: true, id: "abc123" },
  { email: "student2@...", success: false, error: "Already exists" },
  { email: "invalid", success: false, error: "Invalid email format" }
]
3

Remove Participant

await removeParticipant({
  participantId: "jh7..."
});
Performs soft delete by setting deletedAt timestamp.

Participant Groups

Manage access for groups of users:
1

Create User Group

First, create a user group in your organization:
// Groups are managed separately
const groupId = await createUserGroup({
  name: "Spring 2025 Students",
  organizationId
});
2

Add Members to Group

Populate the group with members:
await addGroupMember({
  groupId,
  email: "[email protected]"
});
3

Assign Group to Test

await addParticipantGroup({
  testId,
  userGroupId: groupId
});
Group must belong to the same organization as the test.
4

Remove Group Assignment

await removeParticipantGroup({
  participantGroupId
});

Querying Access Settings

Retrieve current access configuration:
const settings = await getAccessSettings({ testId });

// Returns:
{
  access: "public" | "private",
  password?: string,
  allowedEmailDomains: string[],
  allowedIpAddresses: string[],
  scheduledStartAt?: number,
  scheduledEndAt?: number
}

Database Schema

Test Participants

// testParticipant table
{
  testId: Id<"test">,
  email: string,
  addedAt: number,
  deletedAt?: number  // Soft delete
}

// Indexes:
// - by_test_id
// - by_email
// - by_test_id_email (for uniqueness checks)

Participant Groups

// testParticipantGroup table
{
  testId: Id<"test">,
  userGroupId: Id<"userGroup">,
  addedAt: number,
  deletedAt?: number
}

// Indexes:
// - by_test_id
// - by_user_group_id

Access Validation Flow

When a participant attempts to access a test:
1

Check Test Status

  • Is test published?
  • Is test within scheduled time window?
  • Has test finished?
2

Check Access Mode

  • If private: Is participant on allowlist (individual or group)?
3

Check Password

  • If password is set: Does participant provide correct password?
4

Check Email Domain

  • If domains are restricted: Does participant’s email match allowed domains?
5

Check IP Address

  • If IP whitelist exists: Does participant’s IP match allowed addresses?
6

Grant Access

  • Create test session
  • Allow participant to start

Combining Restrictions

You can layer multiple access controls:
// Secure university final exam
await updateAccessSettings({
  testId,
  access: "private",                    // Require allowlist
  password: "FinalExam2025",             // Require password
  allowedEmailDomains: ["university.edu"], // University emails only
  allowedIpAddresses: ["10.50.0.0/16"], // Campus network only
  scheduledStartAt: examStartTime,       // Available during exam window
  scheduledEndAt: examEndTime
});

// Add enrolled students
await addParticipantGroup({
  testId,
  userGroupId: "CS101_Students"
});

Schedule Integration

Access settings include scheduling fields:
await updateAccessSettings({
  testId,
  scheduledStartAt: 1735689600000,
  scheduledEndAt: 1735693200000
});
Schedule Validation:When using section-based durations, the system validates that total section duration fits within the test window:
if (totalSectionDuration > testWindowMinutes) {
  throw new ConvexError({
    message: `Cannot update schedule:\n\nTotal section duration: ${totalSectionDuration} min\nTest window: ${testWindowMinutes} min`
  });
}

Soft Delete Pattern

Participants and groups use soft deletion:
// Delete
await ctx.db.patch(participantId, { 
  deletedAt: Date.now() 
});

// Restore (when re-adding)
if (existing.deletedAt && existing.deletedAt > 0) {
  await ctx.db.patch(existing._id, {
    deletedAt: undefined,
    addedAt: Date.now()
  });
}

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

Best Practices

  1. Use private mode for sensitive or graded assessments
  2. Set password protection for public tests shared via link
  3. Restrict email domains to prevent unauthorized access from external users
  4. Combine with scheduling to limit when tests are accessible
  5. Use groups for recurring classes or cohorts instead of managing individual emails
  6. Test access controls before the actual exam to ensure participants can enter
  7. Document password distribution method to participants (email, LMS, etc.)

Build docs developers (and LLMs) love