Skip to main content

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​

LayerResponsibility
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.

FieldTypeDescription
idUUIDPrimary key (auto-generated)
emailstringInvitee email address (required)
namestringInvitee display name (optional)
phonestringInvitee phone number (optional)
invitedByNamestringName of the person who sent the invite (optional)
uniqueRoleNamestringRole to assign on acceptance
businessIdUUIDFK to business
statusInviteStatuspending / accepted / rejected / cancelled
isActivebooleanDefaults to true
resendCountintegerHow many times the invite has been resent
lastResentAttimestamptzTimestamp of the most recent resend
createdByUUIDFK to user
updatedByUUIDFK to user (nullable)
createdAttimestamptzAuto-set on creation
updatedAttimestamptzSet 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:

  1. Marks invite status as accepted.
  2. Creates a business_user record linking the user to the business with the invited role.
  3. 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:

ParamRequiredDescription
businessIdNoUUID of the business (filter)
pageNoPage number (default: 1)
sizeNoPage size (default: 10)
searchNoCase-insensitive email search
orderByNoSort field (email)
orderNoasc 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 nameClassEmitted on
invite.createOnCreateInviteEventSuccessful invite creation
invite.resendOnResendInviteEventSuccessful 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/
FileDescription
invites.ymlGET list (paginated, filterable by businessId)
invites by email.ymlGET by email
invite by Id.ymlGET by ID
invite.ymlPOST create
invite_1.ymlPATCH update
invite_2.ymlDELETE
accept invite.ymlPOST accept
reject invite.ymlPATCH reject
Resend an invite.ymlPOST resend

  • BusinessUsers — acceptInvite creates a business_user record
  • Communications — listens to invite.create and invite.resend events to send emails/SMS
  • Firebase — token verification for accept/resend endpoints
  • Users — user lookup during accept/resend flows