Skip to main content
User Groups in Evaly allow you to organize test participants by email address into logical collections. This makes it easier to manage large numbers of participants and distribute tests to specific audiences.

What are User Groups?

User Groups are collections of participant email addresses within an organization. They help you:
  • Organize Participants: Group students, employees, or test-takers by class, department, or any other category
  • Simplify Test Distribution: Send tests to entire groups instead of individual emails
  • Manage at Scale: Handle hundreds or thousands of participants efficiently
  • Reuse Groups: Create once and use across multiple tests
User Groups are organization-scoped. Each organization has its own set of user groups that only members of that organization can access.

Creating a User Group

Any member of an organization can create user groups:
1

Provide Group Details

Give your group a name and optional description:
await createUserGroup({
  name: "Spring 2024 Biology Class",
  description: "Students enrolled in Biology 101",
  members: [
    "[email protected]",
    "[email protected]",
    "[email protected]"
  ]
});
2

Add Member Emails

Include at least one participant email address. Emails are automatically normalized to lowercase.
3

Group Created

The user group is created and associated with your currently selected organization.

Validation Rules

  • Name: Required and cannot be empty
  • Members: At least one email address is required
  • Email Format: Emails are normalized (lowercase and trimmed)
  • Organization: You must have a selected organization

Managing User Groups

Viewing All Groups

Retrieve all user groups for your current organization:
const groups = await getUserGroups();

// Returns:
// [
//   {
//     _id: "userGroup_123",
//     name: "Spring 2024 Biology Class",
//     description: "Students enrolled in Biology 101",
//     organizationId: "org_456",
//     memberCount: 45,
//     _creationTime: 1234567890
//   },
//   ...
// ]
Each group includes a memberCount showing how many participants are in the group.

Viewing Group Details

Get detailed information about a specific group, including all member emails:
const group = await getUserGroupById({ userGroupId: "userGroup_123" });

// Returns:
// {
//   _id: "userGroup_123",
//   name: "Spring 2024 Biology Class",
//   description: "Students enrolled in Biology 101",
//   organizationId: "org_456",
//   members: [
//     "[email protected]",
//     "[email protected]",
//     "[email protected]"
//   ]
// }

Updating User Groups

Modify group details and membership:
await updateUserGroup({
  userGroupId: "userGroup_123",
  name: "Updated Group Name",
  description: "New description",
  members: [
    "[email protected]",
    "[email protected]",
    "[email protected]" // new member
    // student3 removed
  ]
});
Update Behavior:
  • Removed Members: Soft-deleted from the group
  • New Members: Added to the group
  • Existing Members: No change if still in the list
  • Restored Members: If a previously removed member is re-added, their record is restored
When updating members, the system compares the new list with existing members and efficiently handles additions, removals, and restorations.

Adding Members

Add a Single Member

const result = await addMemberToUserGroup({
  userGroupId: "userGroup_123",
  email: "[email protected]"
});

// Returns:
// { restored: false, memberId: "member_789" }
// or
// { restored: true, memberId: "member_456" } // if previously removed
Behavior:
  • If the email is new, it’s added to the group
  • If the email was previously in the group but removed, it’s restored
  • If the email is already an active member, an error is thrown

Add Multiple Members

Bulk-add participants to a group:
const results = await addMembersToUserGroup({
  userGroupId: "userGroup_123",
  emails: [
    "[email protected]",
    "[email protected]",
    "[email protected]" // already exists
  ]
});

// Returns:
// [
//   { email: "[email protected]", status: "added" },
//   { email: "[email protected]", status: "added" },
//   { email: "[email protected]", status: "duplicate" }
// ]
Status Types:
  • added: Successfully added new member
  • restored: Previously removed member was restored
  • duplicate: Email already exists as active member

Removing Members

Remove a participant from a group (soft delete):
await removeMemberFromUserGroup({
  userGroupId: "userGroup_123",
  email: "[email protected]"
});
Members are soft-deleted, not permanently removed. This allows for restoration if they’re re-added later and maintains data integrity.

Updating Member Emails

Change a participant’s email address within a group:
await updateMemberEmail({
  userGroupId: "userGroup_123",
  oldEmail: "[email protected]",
  newEmail: "[email protected]"
});
Validation:
  • Old email must exist in the group
  • New email cannot already exist in the group
  • Both emails are normalized before processing

Deleting User Groups

Permanently remove a user group (soft delete):
await deleteUserGroup({ userGroupId: "userGroup_123" });
Effects:
  • The user group is soft-deleted
  • All members in the group are soft-deleted
  • The group no longer appears in listings
  • Data is preserved for potential recovery
Deleting a user group soft-deletes all its members. Make sure you want to remove the entire group before proceeding.

Ownership and Permissions

User groups are organization-scoped with automatic ownership checks:
const { isOwner, userGroup } = await checkUserGroupOwnership(
  ctx,
  userGroupId
);

if (!isOwner || !userGroup) {
  throw new ConvexError({
    message: "You are not allowed to modify this user group"
  });
}
Permission Rules:
  • Users can only access groups from their currently selected organization
  • Users must be a member of the organization to view or modify its groups
  • The system validates organization membership on every operation

Database Schema

User groups use two related tables:

userGroup Table

{
  name: string,
  description?: string,
  organizationId: Id<"organization">,
  createdByOrganizerId: Id<"organizer">,
  deletedAt?: number
}

userGroupMember Table

{
  userGroupId: Id<"userGroup">,
  email: string, // lowercase and trimmed
  deletedAt?: number
}

Use Cases

Educational Institutions

Class Sections

Create groups for each class section:
  • “Biology 101 - Section A”
  • “Biology 101 - Section B”
  • “Chemistry 201 - Lab Group 1”

Grade Levels

Organize students by grade:
  • “Freshman - Fall 2024”
  • “Sophomore - Spring 2024”
  • “Graduate Students”

Corporate Training

Departments

Group employees by department:
  • “Sales Team”
  • “Engineering - Frontend”
  • “Customer Support”

Training Cohorts

Organize by training program:
  • “New Hire Onboarding - Q1 2024”
  • “Leadership Development”
  • “Compliance Training”

Best Practices

1

Use Descriptive Names

Choose clear, specific names that indicate the group’s purpose and time period.
2

Add Descriptions

Include helpful descriptions to remind yourself and team members what each group represents.
3

Keep Groups Current

Regularly update membership as participants join or leave your organization.
4

Organize Hierarchically

Consider using naming conventions like prefixes to group related groups together:
  • “2024-Spring-Biology”
  • “2024-Spring-Chemistry”
5

Bulk Operations

Use bulk add operations when importing large lists of participants to save time.
6

Audit Periodically

Review and clean up unused groups to keep your organization tidy.

Common Patterns

Importing from CSV

When importing participants from a spreadsheet:
const csvEmails = [
  "[email protected]",
  "[email protected]",
  // ... many more
];

await createUserGroup({
  name: "CSV Import - March 2024",
  description: "Imported from student_list.csv",
  members: csvEmails
});

Combining Multiple Groups

To send a test to participants from multiple groups, retrieve members from each:
const group1 = await getUserGroupById({ userGroupId: "group_1" });
const group2 = await getUserGroupById({ userGroupId: "group_2" });

const allEmails = [...group1.members, ...group2.members];
const uniqueEmails = [...new Set(allEmails)];

Archiving Old Groups

Instead of deleting, consider adding a prefix or description:
await updateUserGroup({
  userGroupId: "old_group",
  name: "[ARCHIVED] Spring 2023 Biology",
  description: "Archived - No longer in use",
  members: existingMembers // keep members intact
});

Build docs developers (and LLMs) love