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_enrollmentsand progress.Auto-sync: enrolling a member added to a group after the group was assigned.
Welcome email + in-app
GROUP_ENROLLEDnotification to newly enrolled learners.Capacity-safe enrollment: respects
quests.enrollment_limit/enrollment_count(reject-not-truncate).
Shipped scope (v1.0):
Database — new
group_enrollmentstable (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, andGroupEnrollmentTableon 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) andDELETE(unenroll).app/api/agency/groups/quest-enrollments/route.ts—GETlist of a quest’s cohort enrollments (+canManage).app/api/agency/groups/[id]/members/route.tsand.../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— mergeslearner_group_quests(metadata) withgroup_enrollments(actual) for reporting.supabase/migrations/20260812_create_group_enrollments.sql— schema, indexes, RLS,updated_attrigger.
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_ENROLLEDnotification (in-app honorsquest_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_enrollmentsrow (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_countis 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_enrollmentsand the insertedquest_enrollmentson mid-batch failure.Concurrency guard: the partial unique index plus a
23505→BadRequestErrormapping (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_idexists ongroup_enrollmentsbut 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_enrollmentsrowACTIVE).Every current group member has a
quest_enrollmentsrow (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
ACTIVEgroups (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_ENROLLEDnotification.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-enrollmentsreturns403; 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 (
canManagefalse); 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_enrollmentsrow → 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 idArchived 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—ACTIVEorARCHIVED(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_attrigger are in the ITG §4 (Data Model / Schema).
10. API CONTRACTS
Enroll a Group
Endpoint:
POST /api/agency/groups/{id}/quests/{questId}/enrollAuth: agency manager (OWNER/ADMIN/CREATOR) + subgroup edit access.
Behavior: validates agency scoping; creates/reactivates the
group_enrollmentsrow; idempotently insertsquest_enrollmentsfor current members; enforces capacity before any write; sends welcome emails +GROUP_ENROLLEDnotifications.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}/enrollAuth: 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}/membersandPOST /api/agency/groups/{id}/members/bulkAuth: 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).notificationsand 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_enrollmentshas 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_idcolumn 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
canManageso the UI can hide actions that would403.Treat
401/403on 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
ACTIVEcohort 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_atrefreshed.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_idbe 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_idlinkage — 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_enrollmentsand 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 —groupQuestScopemerges 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.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_enrollmentsRLS 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.