Groups let instructors sell team/organization access to courses. An organization buys a block of seats, gets a group, and can invite members who automatically get access to all linked courses.
Key advantage over LifterLMS: A single group can grant access to multiple courses (LifterLMS limits to one).
Admin UI: Navigate to /admin/groups → New Group. Fields:
- Name / Description / Slug (auto-generated)
- Total Seats — max members allowed
- Visibility —
private(invite only),open(join link), orclosed(no new members) - Pricing Model —
per_seat(price × quantity) orfixed_tier(flat price includes N seats) - Stripe Price ID — links to a Stripe Price for checkout
Via API: POST /api/v1/groups with Bearer token authentication.
Via Stripe Purchase: A customer buys group access at /courses/:slug/group-purchase, picks seat count, names the group, and completes Stripe Checkout. The webhook automatically creates the group, makes the purchaser primary_admin, and links the course(s).
Admin UI: On the group show page (/admin/groups/:id), scroll to "Add Course" — it lists all courses not yet linked. Click "Add" to link one.
Via Stripe: The group purchase checkout passes course_ids in Stripe metadata. On successful payment, those courses are auto-linked.
Note: Only courses are linkable to groups. Events are not currently supported.
Four roles with hierarchical permissions:
| Role | Manage Members | Manage Managers | Manage Info | Manage Seats | View Reports | Delete Group |
|---|---|---|---|---|---|---|
| primary_admin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| admin | ✓ | ✓ | ✓ | ✓ | ✓ | |
| leader | ✓ | ✓ | ✓ | |||
| member |
- The group creator (or Stripe purchaser) is always
primary_admin primary_admincannot be demoted- Admins can change roles for leaders/members
- Leaders can invite/remove members but can't manage other leaders/admins
Two invitation types:
Email Invitations:
- Admin enters one or more emails (comma/space/newline separated) → batch invitations sent
- Each invited person receives an email with an "Accept Invitation" link
- The invitation is locked to that email address — only the matching account can accept
- Pending email invitations count toward seat usage (reserving the seat)
- Default expiry: 7 days
Open Invitations (Join Links):
- Generate a shareable URL at
/groups/join/:token - Anyone with the link can join (no email matching)
- One active open invitation per group at a time
- Open invitations do NOT count toward seat usage (seats are consumed on accept)
- Default expiry: 1 year
- Can be disabled/re-enabled by toggling
When a user visits /groups/join/:token:
- Token is looked up → shows error if not found, expired, or already used
- For email invitations: logged-in user's email must match the invitation email
- Checks if user is already a member → shows "Already a Member" if so
- Checks seat availability → shows "Group Full" if no seats left
- If everything checks out → shows group info + "Accept & Join" button
- On accept: creates an active seat for the user with
memberrole, redirects to leader dashboard
Route: /courses/:slug/group-purchase
- User selects seat count (or picks a tier) and names the group
- Creates a Stripe Checkout Session with metadata:
purchase_type: "group"group_name,group_seats,course_ids- Supports both
payment(one-time) andsubscriptionmodes
- On successful payment (webhook
checkout.session.completed):- Creates the group with specified seats
- Adds purchaser as
primary_admin - Links specified courses
- Stores
stripe_subscription_idif subscription mode
Route: /my/groups (index) and /my/groups/:slug (manage)
Available to users with primary_admin, admin, or leader role.
Index page shows:
- All groups where user has leader+ role
- Member count / total seats with progress bar
- Linked course count, pending invitation count
Show page has tabbed sections:
- Overview — group details, visibility, join link management (generate/copy/disable)
- Members — list with role badges, role change dropdowns, remove buttons
- Invitations — send batch invitations via textarea, list pending with revoke buttons
- Courses — linked courses with status badges
All endpoints require Bearer token authentication (Authorization: Bearer <token>).
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/groups |
List user's groups |
POST |
/api/v1/groups |
Create a group |
GET |
/api/v1/groups/:id |
Get group details |
PATCH |
/api/v1/groups/:id |
Update group |
DELETE |
/api/v1/groups/:id |
Delete group |
POST |
/api/v1/groups/:token/accept-invitation |
Accept invitation by token |
GET |
/api/v1/groups/:group_id/members |
List members |
GET |
/api/v1/groups/:group_id/members/:id |
Get member details |
PATCH |
/api/v1/groups/:group_id/members/:id |
Change member role |
DELETE |
/api/v1/groups/:group_id/members/:id |
Remove member |
GET |
/api/v1/groups/:group_id/invitations |
List invitations |
POST |
/api/v1/groups/:group_id/invitations |
Create invitation(s) |
DELETE |
/api/v1/groups/:group_id/invitations/:id |
Revoke invitation |
GET |
/api/v1/groups/:group_id/seats |
Get seat info (total/used/available) |
PUT |
/api/v1/groups/:group_id/seats |
Update total seats |
- Used seats = active seats + pending email invitations (open invitations don't reserve seats)
- Available seats = total_seats - used_seats
- A group is "full" when available_seats <= 0
- Changing
total_seatscan be done by users withmanage_seatspermission
| Area | Files |
|---|---|
| Schema | lib/pewpros/groups/group.ex, group_seat.ex, group_invitation.ex |
| Context | lib/pewpros/groups.ex |
lib/pewpros/emails/group_invitation_email.ex |
|
| Worker | lib/pewpros/workers/group_invitation_worker.ex |
| Join Page | lib/pewpros_web/live/group_live/join.ex + .html.heex |
| Purchase | lib/pewpros_web/live/group_live/purchase.ex + .html.heex |
| Dashboard | lib/pewpros_web/live/group_live/dashboard/{index,show}.ex + .html.heex |
| API | lib/pewpros_web/controllers/api/v1/group_{controller,member_controller,invitation_controller,seat_controller}.ex |
| Admin | lib/pewpros_web/live/admin/group_live/{show,form_component}.ex |
| Auth Plug | lib/pewpros_web/plugs/group_authorization.ex |
| Billing | lib/pewpros/billing.ex (group checkout + webhook handler) |