Invites Module
Overview
The invites module manages user invitations to businesses within the FlowPOS multi-tenant platform. A business admin creates an invite for a specific email address and role. The invitee receives a notification email and can then accept or reject the invite via the API.
Module path: apps/backend/src/invites/
Architecture
The module follows Hexagonal Architecture with strict layer boundaries.
invites/
├── invites.module.ts # NestJS module wiring
├── domain/
│ └── invites-repository.domain.ts # IInvitesRepository port + INVITES_REPOSITORY token
├── application/
│ ├── invites.service.ts # Use cases (CRUD + accept/reject/resend)
│ └── events/
│ ├── on-create-invite.event.ts # invite.create event payload
│ └── on-resend-invite.event.ts # invite.resend event payload
├── infrastructure/
│ └── invites.repository.ts # Kysely DB adapter (implements port)
└── interfaces/
├── invites.controller.ts # HTTP adapter
├── dtos/
│ ├── create-invite.dto.ts
│ └── update-invite.dto.ts
└── query/
└── paginate-invites.query.ts
Layer responsibilities
| Layer | Responsibility |
|---|---|
domain/ | Repository port (IInvitesRepository) and INVITES_REPOSITORY injection token. |
application/ | InvitesService — orchestrates CRUD, accept/reject/resend use cases, and event emission. |
infrastructure/ | InvitesRepository — Kysely queries against the invite table. Implements IInvitesRepository. |
interfaces/ | InvitesController — maps HTTP requests to service calls. Thin layer with no business logic. |
Dependency rule
interfaces → application → domain ← infrastructure
The service also depends on BusinessUsersService for the acceptInvite use case.
Domain Concepts
Invite
A pending invitation linking an email address to a business and a role.
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key (auto-generated) |
email | string | Invitee email address (required) |
name | string | Invitee display name (optional) |
phone | string | Invitee phone number (optional) |
invitedByName | string | Name of the person who sent the invite (optional) |
uniqueRoleName | string | Role to assign on acceptance |
businessId | UUID | FK to business |
status | InviteStatus | pending / accepted / rejected / cancelled |
isActive | boolean | Defaults to true |
resendCount | integer | How many times the invite has been resent |
lastResentAt | timestamptz | Timestamp of the most recent resend |
createdBy | UUID | FK to user |
updatedBy | UUID | FK to user (nullable) |
createdAt | timestamptz | Auto-set on creation |
updatedAt | timestamptz | Set on update |
Status transitions
pending ──accept──→ accepted
pending ──reject──→ rejected
pending ──cancel──→ cancelled
Only pending invites should be acted upon. The API does not enforce this guard today — callers are responsible for checking status before accepting or rejecting.
Use Cases
1. Create invite
Creates an invite record and emits invite.create to trigger the invitation email.
2. Accept invite
Authenticated user accepts the invite:
- Marks invite status as
accepted. - Creates a
business_userrecord linking the user to the business with the invited role. - Firebase custom claims are synced by
BusinessUsersService.
3. Reject invite
Marks invite status as rejected. No side effects.
4. Resend invite
Increments resendCount, sets lastResentAt, and emits invite.resend to re-trigger the email.
5. List / search invites
Paginated list of active invites, filterable by businessId and searchable by email.
6. Get invites by email
Returns all active invites for an email address, joined with businessName for display.
API Endpoints
All endpoints require Bearer token authentication and are scoped to PolicyResource.Invite for RBAC.
POST /invites
Create a new invite.
Body:
{
"email": "jane@example.com",
"name": "Jane Doe",
"invitedByName": "John Smith",
"uniqueRoleName": "manager",
"businessId": "<uuid>",
"status": "pending",
"isActive": true,
"createdBy": "<uuid>"
}
Responses: 201 Created | 401 Unauthorized | 422 Validation error
GET /invites
List invites with pagination, search, and sorting.
Query parameters:
| Param | Required | Description |
|---|---|---|
businessId | No | UUID of the business (filter) |
page | No | Page number (default: 1) |
size | No | Page size (default: 10) |
search | No | Case-insensitive email search |
orderBy | No | Sort field (email) |
order | No | asc or desc |
Responses: 200 Paginated list | 401 Unauthorized
GET /invites/by-email
Get all active invites for an email address, enriched with businessName.
Query parameters: email (required)
Responses: 200 List of invites | 400 Missing email | 401 Unauthorized
GET /invites/:id
Get a single invite by UUID.
Responses: 200 Found | 404 Not found | 401 Unauthorized
PATCH /invites/:id
Partially update an invite.
Body:
{
"isActive": false,
"updatedBy": "<uuid>"
}
Responses: 200 Updated | 404 Not found | 401 Unauthorized | 422 Validation error
DELETE /invites/:id
Hard-delete an invite record.
Responses: 200 Deleted | 404 Not found | 401 Unauthorized
POST /invites/:id/accept
Accept an invite. The authenticated user's identity is resolved from the Firebase token — no body required.
Responses: 201 Accepted | 404 Not found | 401 Unauthorized
PATCH /invites/:id/reject
Reject an invite. No body required.
Responses: 200 Rejected | 404 Not found | 401 Unauthorized
POST /invites/:id/resend
Resend the invitation email. Increments resend counter. Requires Firebase token.
Responses: 201 Resent (returns updated invite) | 404 Not found | 401 Unauthorized
Event System
The module emits two domain events via EventEmitter2:
| Event name | Class | Emitted on |
|---|---|---|
invite.create | OnCreateInviteEvent | Successful invite creation |
invite.resend | OnResendInviteEvent | Successful resend |
Listeners (email handlers) are defined in the communications module.
Design Decisions
Dependency inversion via Symbol token
The repository is injected through INVITES_REPOSITORY Symbol token, matching the project convention (brands, colors, sizes). The service depends on IInvitesRepository (port), not the concrete Kysely implementation.
RBAC enforcement
The controller uses @UseGuards(RolesGuard) with @PermissionResource(PolicyResource.Invite) and per-endpoint @PermissionAction(...) decorators to enforce role-based access control.
Accept is an application-layer use case
The acceptInvite flow updates the invite status and creates a business_user in a single service call. This preserves SRP and makes the use case testable independently of HTTP.
findByFilter joins business name
The GET /invites/by-email endpoint returns invites enriched with businessName via an INNER JOIN on the business table. The general paginated list does not include this join to keep the query lightweight.
Event-driven communication
The invites module does not know about email sending. The communications module listens to events and sends notifications asynchronously.
Bruno API Collection
Requests are located at:
api-client/flowpos/collections/invites/
| File | Description |
|---|---|
invites.yml | GET list (paginated, filterable by businessId) |
invites by email.yml | GET by email |
invite by Id.yml | GET by ID |
invite.yml | POST create |
invite_1.yml | PATCH update |
invite_2.yml | DELETE |
accept invite.yml | POST accept |
reject invite.yml | PATCH reject |
Resend an invite.yml | POST resend |
Related Modules
- BusinessUsers —
acceptInvitecreates abusiness_userrecord - Communications — listens to
invite.createandinvite.resendevents to send emails/SMS - Firebase — token verification for accept/resend endpoints
- Users — user lookup during accept/resend flows