Group-Based Enrollment

Feature Owner (original Hand-Off): Patrick Babala (Patpatty19)
Updated by / current owner: Patrick Babala
Module: Organisation — [11.5] Group-Based Enrollment
Priority: P1
Status: Handoff — New (v1.0)
Date: 2026-10-01
Issue: wyzlab/WyzQuests #64 — [11.5] Group-Based Enrollment (closed by PR #761).
PRs covered by this revision: #761 — feat/group-based-enrollment (primary; merged 2026-08-12). Related/integrated work: #805 (group-assigned quests + completion rate in group reports), #833 / #836 (creator group & subgroup permissions), #869 (default cohorts). Security: #875 / issue #860 (enrollment-count triggers + group_enrollments / quest_enrollments RLS hardening).
Companion doc: Internal Technical Guide — Internal-Technical-Guide-Group-Based-Enrollment.md (architecture, DB schema, API specs, helpers, ops, troubleshooting). This Hand-Off stays product-facing; where engineering depth is needed it cross-references the ITG instead of duplicating it.


EXECUTIVE SUMMARY

What is this feature?

Group-Based Enrollment lets an agency creator enroll an entire learner cohort (a learner group) into a quest with a single action. Every current group member is enrolled, members added to the group later are auto-enrolled into the group’s active quests, and unenrolling a cohort is a soft delete that preserves learner progress.

Why does it matter?

Before this feature, enrolling a class, team, or department meant adding learners one at a time or via CSV, and there was no way to keep enrollment in sync as the cohort changed. Group-Based Enrollment turns a cohort into a single enrollable unit, closes the loop between “this quest belongs to this group” (metadata) and “these learners are actually enrolled” (the group_enrollments + quest_enrollments rows), and keeps cohort enrollment within the existing capacity rules.

MVP scope (current implementation):

  • Assign one or more learner groups to a quest from the quest’s Enrollment section (“Assign to Groups”).

  • Enroll every current group member idempotently (already-enrolled learners are skipped).

  • Cohort table on the quest: group, manager, member count, enrolled date, auto-sync state, completion rate.

  • Soft unenroll (archive) that preserves quest_enrollments and progress.

  • Auto-sync: enrolling a member added to a group after the group was assigned.

  • Welcome email + in-app GROUP_ENROLLED notification to newly enrolled learners.

  • Capacity-safe enrollment: respects quests.enrollment_limit / enrollment_count (reject-not-truncate).

Shipped scope (v1.0):

  • Database — new group_enrollments table (soft-delete aware) with agency-scoped RLS.

  • Service — enrollGroupInQuest, unenrollGroupFromQuest, autoEnrollNewGroupMember(s), listQuestGroupEnrollments.

  • API — enroll/unenroll routes, a quest-scoped listing route, and auto-sync wired into both member-add routes.

  • UI — AssignToGroupsModal, ConfirmEnrollmentModal, and GroupEnrollmentTable on the quest enrollment page.

Still deferred: per-member management from the cohort view, scheduled/drip enrollment, unenroll notifications/emails, hard delete, public/anonymous enrollment, and populating the adventure_id link.


1. USER PAIN POINT & SOLUTION

Current State (Without Feature)

Creators can only enroll learners individually or by CSV, one quest at a time. For a class or department this is repetitive, and newly added team members are never enrolled automatically.

Pain Point

  • Emotional: Frustration managing the same cohort across many quests by hand.

  • Functional: No cohort-level enrollment primitive; enrollment drifts out of sync with the group as membership changes.

  • Business Impact: Slow onboarding for cohorts, weak cohort-level reporting, and manual work that scales poorly for agencies.

Future State (With Feature)

Creators assign a group to a quest once. The whole cohort is enrolled, later joiners are auto-enrolled, and completion is visible per cohort. Unenrolling archives the cohort link without destroying learner progress.

Marketing Hook

“Enroll an entire team in one click — and every new member joins automatically.”


2. CODEBASE ASSESSMENT

Current Implementation Status

Group-Based Enrollment is implemented across a database table, a service layer, three API surfaces, and an enrollment-UI trio (picker, confirm dialog, table). It is agency-scoped throughout and enforces the same capacity rules as single/bulk enrollment.

Primary Files

  • lib/services/groupEnrollmentService.ts — core data access: enroll, unenroll, auto-sync, and the enriched listing.

  • app/api/agency/groups/[id]/quests/[questId]/enroll/route.ts — POST (enroll) and DELETE (unenroll).

  • app/api/agency/groups/quest-enrollments/route.ts — GET list of a quest’s cohort enrollments (+ canManage).

  • app/api/agency/groups/[id]/members/route.ts and .../members/bulk/route.ts — member-add paths that trigger auto-sync.

  • components/creator/enrollment/AssignToGroupsModal.tsx — searchable group multi-select with target-learner count.

  • components/creator/enrollment/ConfirmEnrollmentModal.tsx — confirmation summary dialog.

  • components/creator/enrollment/GroupEnrollmentTable.tsx — cohort table with unenroll (soft delete).

  • app/quest-editor/[questID]/(sections)/enrollment/page.tsx — hosts the card and buttons.

  • lib/learnerGroups/groupQuestScope.ts — merges learner_group_quests (metadata) with group_enrollments (actual) for reporting.

  • supabase/migrations/20260812_create_group_enrollments.sql — schema, indexes, RLS, updated_at trigger.

Current Behavior Summary

  • A manager (agency OWNER/ADMIN/CREATOR with subgroup edit access) assigns groups via “Assign to Groups”.

  • The service validates the group and quest both belong to the agency, then enrolls all current members idempotently (batched at 100).

  • Newly enrolled learners receive a welcome email and an in-app GROUP_ENROLLED notification (in-app honors quest_updates_in_app).

  • The cohort table shows group, manager, member count, enrolled date, auto-sync vs. manual state, and aggregate completion rate.

  • Unenroll archives the group_enrollments row (status = 'ARCHIVED'); learner progress is preserved.

  • Members added later are auto-enrolled into every active cohort quest, with bounded concurrency and capacity checks.

  • quests.enrollment_count is maintained by database triggers on every write path (not by the API).

Strengths Already Present

  • Soft-delete-aware schema: partial unique index on (group_id, quest_id) WHERE status = 'ACTIVE' allows re-enrollment while keeping history.

  • Idempotent, batched, per-learner enrollment.

  • Capacity enforced before any write; cohorts that exceed remaining slots are rejected, not silently truncated.

  • Compensating rollback restores both group_enrollments and the inserted quest_enrollments on mid-batch failure.

  • Concurrency guard: the partial unique index plus a 23505 → BadRequestError mapping (no 500 on duplicate POSTs).

  • Agency-scoped reads and writes; subgroup visibility enforced for non-managers; RLS on the table.

Gaps / Hardening Opportunities

  • No dedicated end-to-end test for the assign-to-groups flow (coverage is service-level).

  • adventure_id exists on group_enrollments but is never populated (dead column).

  • Auto-sync is fire-and-forget with no retry; a learner skipped because a quest was full is not re-evaluated when capacity frees up.

  • Capacity is application-level, not a database invariant — concurrent cohort enrollments can still theoretically oversubscribe.

  • Confirm dialog copy says auto-sync “can be toggled off anytime,” but there is no dedicated toggle — only unenroll stops future sync.

  • Welcome emails are sent regardless of quest_updates_email (consistent with the existing single/bulk routes; documented, not changed).


3. 4D FRAMEWORK MAPPING

Diagnose

Creators lack a cohort-level way to grant quest access, and enrollment drifts as group membership changes.

Design

Model enrollment as a relationship between a learner group and a quest (group_enrollments), separate from the per-learner records it produces (quest_enrollments), with soft-delete semantics and agency-scoped access.

Develop

Provide enroll/unenroll/list APIs and a manager-gated UI, with idempotent member enrollment, capacity enforcement, compensating rollback, and auto-sync on member-add.

Deliver

Creators enroll a whole cohort once, see cohort completion, and keep membership and enrollment in sync automatically.


4. USER FLOWS

Entry Point

A creator opens a quest in the editor → Enrollment section → Assign to Groups. (Learners do not operate this feature directly; they receive enrollments.)

Success Criteria

  • The selected groups are associated with the quest (group_enrollments row ACTIVE).

  • Every current group member has a quest_enrollments row (unless already enrolled).

  • The cohort appears in the Group Enrollment table with member count and completion rate.

  • Newly enrolled learners receive the welcome email + in-app notification.

  • New members added to an assigned group are auto-enrolled.

  • Unenrolling archives the cohort link while preserving learner progress.

Main Flow (Happy Path)

  • Creator opens the quest’s Enrollment section.

  • Creator clicks Assign to Groups and selects one or more ACTIVE groups (already-enrolled groups are disabled).

  • Creator confirms in the summary dialog.

  • The system validates the group and quest belong to the agency, then enrolls every current member not already enrolled (idempotent, batched).

  • Newly enrolled learners get a welcome email and an in-app GROUP_ENROLLED notification.

  • The cohort appears in the Group Enrollment table (member count, auto-sync state, completion rate).

  • Later, a member is added to the group → the system auto-enrolls them into the group’s active quests (capacity permitting).

  • To stop future enrollment, the creator unenrolls the group (soft delete); existing progress is preserved.

Edge Cases

  • Group already enrolled: Rejected with "Group is already enrolled in this quest" (also maps the concurrent-insert race).

  • Quest at capacity: "Enrollment limit reached for this quest".

  • Cohort larger than remaining slots: "Not enough enrollment slots…" — the whole cohort is rejected (never partially enrolled).

  • Archived group: "Cannot enroll an archived group" (archived groups are also filtered out of the picker).

  • Unpublished / cross-agency quest: "Quest is not published and cannot be enrolled" / "Quest does not belong to this agency".

  • Member added while a quest is full: Auto-sync skips the learner and logs it; the member is not enrolled.

  • Standalone (non-agency) creator: GET /api/agency/groups/quest-enrollments returns 403; the UI hides the card entirely (no dead feature, no error toast).

  • Non-manager agency member (REVIEWER/VIEW): Can read the table but sees no write controls (canManage false); mutations require a manager.

Decision Points

  • IF the caller has no agency → the Group Enrollment card is hidden.

  • IF the caller is an agency manager (OWNER/ADMIN/CREATOR) → the Assign and Unenroll actions are shown.

  • IF the quest is full or the cohort exceeds remaining slots → enrollment is rejected wholesale.

  • IF the group already has an archived enrollment → the existing row is reactivated instead of inserting a new one.

  • IF a member added later already has a quest_enrollments row → auto-sync skips them silently.


5. INFORMATION ARCHITECTURE

Primary Information (Always visible)

  • Group name

  • Member count

  • Enrolled date

  • Auto-sync status (Auto-sync / Manual)

  • Completion status (progress bar + N% · In Progress / Not Started)

Secondary Information

  • Group manager (“Managed by …”)

  • Cohort count badge and “Auto-sync ON” badge

  • Assign to Groups / Unenroll actions

Tertiary Information (Hidden until needed)

  • group_enrollments.id, group id, quest id

  • Archived state and archived_at

Actions

  • Primary CTA: Assign to Groups

  • Secondary Actions: Unenroll group (destructive, confirmation required)


6. WIREFRAMES

The feature is embedded in the quest editor’s Enrollment section, above the per-learner enrollment table.

Key Screens:

  • Group Enrollment card with the cohort table

  • Assign to Groups modal (searchable multi-select + live target-learner count)

  • Confirm Enrollment modal (auto-enroll / welcome email / future auto-sync summary)

  • Unenroll confirmation modal (destructive, type-to-confirm)

Annotations:

  • The card is hidden for standalone creators and read-only for non-manager agency members.

  • Each row represents one cohort, not one learner.

  • Unenroll archives the cohort link; existing learner progress is explicitly preserved.

The architecture/system diagram lives with the companion ITG (Internal-Technical-Guide-Group-Based-Enrollment.md), not here.


7. WIREFLOWS

Creator opens Enrollment → Assign to Groups → selects cohorts → confirms → system enrolls all current members (capacity-checked, idempotent) and notifies them → cohort appears in the table with completion → later member adds auto-enroll → creator monitors completion → unenroll archives the cohort when done.


8. PROTOTYPE

Figma Prototype Link: Not currently available for this feature.

How to test:

  • Open a quest owned by an agency in the editor and go to the Enrollment section.

  • Click Assign to Groups; select an active group with members; confirm.

  • Confirm the cohort appears in the table with the correct member count and an “Auto-sync” state.

  • Add a learner to that group (Groups page) and confirm they are auto-enrolled into the quest.

  • Click Unenroll and confirm the row switches to archived; verify learner progress is intact.


9. DATA MODEL

Core Table

The feature introduces group_enrollments (one row per group↔quest enrollment):

  • group_id — FK → learner_groups (ON DELETE CASCADE)

  • quest_id — FK → quests (ON DELETE CASCADE)

  • adventure_id — nullable FK (present but currently unused)

  • status — ACTIVE or ARCHIVED (soft delete)

  • enrolled_at, archived_at, created_at, updated_at

Key Constraint

A partial unique index on (group_id, quest_id) where status = 'ACTIVE' — archived rows do not block re-enrollment, and two concurrent active enrollments are impossible. The reactivation path upserts the existing archived row.

Relationship to Learner Enrollment

Enrolling a cohort writes one group_enrollments row and inserts a quest_enrollments row per newly enrolled member (status: 'ongoing', progress JSON). Unenroll only archives the group row; the per-learner rows are preserved.

Progress Shape

progress is a JSON object with percentage, visited_cards, last_visited_at, and completed_nodes. Completion rate shown per cohort is the average percentage across members that have an enrollment.

Full column types, indexes, RLS policies, and the updated_at trigger are in the ITG §4 (Data Model / Schema).


10. API CONTRACTS

Enroll a Group

  • Endpoint: POST /api/agency/groups/{id}/quests/{questId}/enroll

  • Auth: agency manager (OWNER/ADMIN/CREATOR) + subgroup edit access.

  • Behavior: validates agency scoping; creates/reactivates the group_enrollments row; idempotently inserts quest_enrollments for current members; enforces capacity before any write; sends welcome emails + GROUP_ENROLLED notifications.

  • Response: { group_id, quest_id, newly_enrolled_count, total_group_members, reactivated }.

Unenroll a Group (soft delete)

  • Endpoint: DELETE /api/agency/groups/{id}/quests/{questId}/enroll

  • Auth: agency manager + subgroup edit access.

  • Behavior: status → 'ARCHIVED', archived_at = now(); learner progress preserved; idempotent when already archived.

List a Quest’s Group Enrollments

  • Endpoint: GET /api/agency/groups/quest-enrollments?questId={questId}

  • Auth: any agency member (non-agency → 403).

  • Behavior: agency-scoped; non-managers see only subgroups they own or were granted; returns { enrollments: [...], canManage } with group name, manager, member count, enrolled date, status, and completion rate. Archived rows are included for history.

Member Add (auto-sync trigger)

  • Endpoint: POST /api/agency/groups/{id}/members and POST /api/agency/groups/{id}/members/bulk

  • Auth: agency manager + subgroup edit access.

  • Behavior: after members are added, the routes fire autoEnrollNewGroupMembers(...) unawaited, enrolling added learners into the group’s active quests (capacity-checked; failures logged, never thrown).

Auth checks, exact error strings, and full request/response shapes are in the ITG §4 (API Specification).


11. DATA REQUIREMENTS

Frontend Needs

  • The list of a quest’s cohort enrollments (group name, manager, member count, enrolled date, status, completion rate).

  • Whether the caller can manage cohort enrollment (canManage).

  • The agency’s active groups for the picker (with member counts) and the set of already-enrolled group ids.

  • The ability to enroll/unenroll a cohort.

Backend Needs

  • learner_groups + group_memberships (cohort definition).

  • quests (publishing_status, enrollment_limit, enrollment_count, creator_id).

  • quest_enrollments (per-learner rows + progress).

  • notifications and the email service for the newly enrolled.


12. SECURITY & AUTHORIZATION

Who can access this feature?

  • Agency OWNER/ADMIN: ✔ full (read + manage all subgroups)

  • Creator (agency): ✔ manage owned/assigned resources (subgroup edit access required)

  • Reviewer / View-only agency member: read-only (write controls hidden)

  • Standalone creator (no agency): ✘ (card hidden)

  • Learner: — (does not operate the feature; receives enrollments)

Authorization Logic

  • Writes require authenticateAgencyManager() (OWNER/ADMIN/CREATOR) and subgroup edit access (canEditSubGroupRole), so a member cannot enroll a subgroup they cannot edit.

  • Reads require authenticateAgencyMember(); non-managers are filtered to subgroups they own or hold a view/edit grant on (resolveListableSubGroupIds).

  • The service re-validates that the group and the quest both belong to the caller’s agency (the service-role client bypasses RLS).

  • group_enrollments has RLS enabled (read = agency member, write = agency manager + active group + agency quest) as defense-in-depth for the direct PostgREST path.

Note: authentication is Supabase Auth; the legacy-named app_users.clerk_id column stores the Supabase auth UUID. See ITG §3 and §4.


13. ERROR HANDLING

Common Errors

  • Group already enrolled ("Group is already enrolled in this quest").

  • Enrollment limit reached ("Enrollment limit reached for this quest").

  • Cohort exceeds remaining capacity ("Not enough enrollment slots: this group needs N slots, only M remain").

  • Quest not published ("Quest is not published and cannot be enrolled").

  • Cross-agency quest ("Quest does not belong to this agency").

  • Archived group ("Cannot enroll an archived group").

  • No subgroup edit access ("You do not have edit access to this subgroup").

  • Group / quest / enrollment not found (404).

Handling Guidance

  • The listing endpoint returns canManage so the UI can hide actions that would 403.

  • Treat 401/403 on the listing as “feature unavailable” and hide the card (standalone creators).

  • Surface the exact capacity error messages; do not retry with a truncated cohort.

  • Auto-sync failures are logged under [groupEnrollmentService] and never block the member-add response.


14. TESTING CHECKLIST

Happy Path

  • Assign one group → members are enrolled, table shows the cohort with member count.

  • Assign multiple groups → each is enrolled independently.

  • New member added to an assigned group → auto-enrolled into the group’s active quests.

  • Newly enrolled learners receive a welcome email + in-app notification.

  • Completion rate updates as members progress.

  • Unenroll archives the cohort; learner progress remains.

Edge Cases

  • Duplicate enroll attempt → "Group is already enrolled in this quest" (no duplicate row).

  • Quest full → enrollment rejected; no stuck ACTIVE cohort row.

  • Cohort larger than remaining slots → rejected, not partially enrolled.

  • Archived group → rejected and not selectable in the picker.

  • Unpublished or cross-agency quest → rejected.

  • Archived (previously enrolled) group re-enroll → row reactivated, enrolled_at refreshed.

  • Standalone creator → card hidden (no error toast).

  • Reviewer/View member → can read, cannot see write controls.

  • Member added while quest is full → skipped (logged), not enrolled.

  • Mid-batch failure → both tables rolled back (no half-enrolled cohort, no stuck row).


15. OPEN QUESTIONS

For Product

  • Should there be an explicit auto-sync toggle on a cohort (the Confirm dialog implies one)? — Still open. Today only unenroll stops future sync.

  • Should unenrolling a cohort notify or email the affected learners? — Still open. Only enrollment sends email/notification.

  • Should skipped (quest-full) members be re-evaluated automatically when capacity frees up? — Still open.

For Engineering

  • Should adventure_id be populated, or the column dropped? — Still open.

  • Should capacity become a database invariant (lock/serialization) rather than an application check? — Still open.

  • Should the assign-to-groups flow get a dedicated Playwright e2e spec? — Still open.


16. OUT OF SCOPE (v1.1+)

  • Per-member management directly from the cohort view — out of scope.

  • Scheduled / drip cohort enrollment — out of scope.

  • Unenroll notifications or emails — out of scope.

  • Hard delete of a group enrollment — out of scope (soft delete only).

  • Public / anonymous enrollment links — out of scope.

  • Paid / seat-based cohort gating — out of scope.

  • adventure_id linkage — out of scope (column unused).


17. SUCCESS METRICS

  • Cohort enrollment success rate.

  • Auto-sync coverage (share of new members enrolled into active cohort quests).

  • Capacity rejection rate (quest-full / not-enough-slots).

  • Cohort completion visibility (table reach + completion rate accuracy).

  • Reduced manual per-learner enrollment work for agency creators.


18. DEPENDENCIES

This feature depends on

  • Learner Group Management ([11.4]) — learner_groups + group_memberships.

  • Quest Management — quests (publishing_status, enrollment_limit, enrollment_count, ownership).

  • Enrollment — quest_enrollments and the shared progress shape.

  • Authentication (Supabase Auth) and the agency/subgroup permission helpers.

  • Email service (welcome emails) and the notifications store (GROUP_ENROLLED).

  • Enrollment-count database triggers (sync_quest_enrollment_count_*).

  • Default-cohort provisioning (creatorCohortService, PR #869).

These features depend on this

  • Group reporting / analytics ([11.6] quest assignment + completion rate — groupQuestScope merges this table).

  • Learner quest dashboards (enrollments created here are what learners see).

  • Agency cohort management and the default-cohort flow.


19. TIMELINE & OWNERSHIP

  • Original owner / Hand-Off author: Patrick Babala (Patpatty19).

  • Implementation updates & current owner: Patrick Babala.

  • Reviewers: Cole (@ybcole), Christian Denzon (@cdnzn), Clyde Timothy (@clydetims) — PR #761.

  • QA: Manual end-to-end verification (enroll/dedupe/reactivate/unenroll, negative cases, UI flows, notifications/emails) + tests/services/group-enrollment-service.test.ts.

  • Shipped: PR #761 merged 2026-08-12 (issue #64 closed).

  • Estimated Completion: core complete; remaining items are the Open Questions and Out of Scope sections above.


REFERENCES

  • Issue: wyzlab/WyzQuests #64 — [11.5] Group-Based Enrollment.

  • Primary PR: #761 — feat/group-based-enrollment (merged 2026-08-12).

  • Related PRs: #805 — group-assigned quests + completion rate; #833 / #836 — group & subgroup permissions; #869 — default cohorts.

  • Security: issue #860 / PR #875 — enrollment-count triggers + group_enrollments / quest_enrollments RLS hardening.

  • Migration: supabase/migrations/20260812_create_group_enrollments.sql.

  • Companion ITG: Internal-Technical-Guide-Group-Based-Enrollment.md.


VERSION HISTORY

  • 1.0 — 2026-10-01 — Patrick Babala. Initial Hand-Off for Group-Based Enrollment ([11.5], PR #761). Covers cohort enrollment, auto-sync, soft unenroll, capacity enforcement, agency/subgroup authorization, the cohort table, API contracts, testing checklist, and known limitations.


Was this article helpful?