Spaces:
Sleeping
User Invitation System - Implementation Plan
Overview
Implement a secure user invitation system that allows platform admins to invite users to organizations (clients/contractors) via WhatsApp (primary) or Email (fallback). Users are created in Supabase Auth ONLY after accepting the invitation.
1. Database Schema Changes
Important Note on User Status
The existing user_status enum (invited, pending_setup, active, suspended) is KEPT.
user_invitationstable tracks PRE-acceptance state (before user exists)users.statustracks POST-acceptance state (after user is created)- Flow: Invitation sent β User accepts β User created with status
activeβ User can besuspendedlater
New Table: user_invitations
CREATE TYPE invitation_status AS ENUM (
'pending',
'accepted',
'expired',
'cancelled'
);
CREATE TYPE invitation_method AS ENUM (
'whatsapp',
'email',
'both'
);
CREATE TABLE user_invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Invitation Details
email TEXT NOT NULL,
phone TEXT,
invited_role app_role NOT NULL,
-- Organization Link (one must be set based on role)
client_id UUID REFERENCES clients(id) ON DELETE CASCADE,
contractor_id UUID REFERENCES contractors(id) ON DELETE CASCADE,
-- Invitation Token & Status
token TEXT NOT NULL UNIQUE,
status invitation_status DEFAULT 'pending',
invitation_method invitation_method DEFAULT 'whatsapp',
-- Lifecycle
invited_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
invited_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
accepted_at TIMESTAMP WITH TIME ZONE,
-- Delivery Tracking
whatsapp_sent BOOLEAN DEFAULT FALSE,
whatsapp_sent_at TIMESTAMP WITH TIME ZONE,
email_sent BOOLEAN DEFAULT FALSE,
email_sent_at TIMESTAMP WITH TIME ZONE,
-- Metadata
invitation_metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
-- Constraints
CONSTRAINT chk_invitation_org_link CHECK (
(client_id IS NOT NULL AND contractor_id IS NULL) OR
(client_id IS NULL AND contractor_id IS NOT NULL) OR
(invited_role = 'platform_admin' AND client_id IS NULL AND contractor_id IS NULL)
),
CONSTRAINT chk_invitation_contact CHECK (
email IS NOT NULL OR phone IS NOT NULL
)
);
-- Indexes
CREATE INDEX idx_invitations_token ON user_invitations(token) WHERE status = 'pending';
CREATE INDEX idx_invitations_email ON user_invitations(email, status);
CREATE INDEX idx_invitations_status ON user_invitations(status, expires_at);
CREATE INDEX idx_invitations_client ON user_invitations(client_id) WHERE client_id IS NOT NULL;
CREATE INDEX idx_invitations_contractor ON user_invitations(contractor_id) WHERE contractor_id IS NOT NULL;
-- Prevent duplicate pending invitations
CREATE UNIQUE INDEX idx_invitations_unique_pending ON user_invitations(email, client_id, contractor_id)
WHERE status = 'pending';
COMMENT ON TABLE user_invitations IS 'Tracks user invitations before Supabase Auth account creation';
2. Environment Variables
Add to .env:
# Base URL for invitation links
BASE_URL=https://swiftops.atomio.tech
# Invitation Configuration
INVITATION_TOKEN_EXPIRY_HOURS=72
# Notification Services (already exist)
RESEND_API_KEY=re_xxx
WASENDER_API_KEY=xxx
WASENDER_PHONE_NUMBER=+254xxx
3. Project Structure
src/app/
βββ services/
β βββ __init__.py
β βββ invitation_service.py # Core invitation logic
β βββ notification_service.py # WhatsApp/Email delivery
β βββ token_service.py # Token generation/validation
βββ templates/
β βββ emails/
β β βββ base.html # Base email template
β β βββ invitation.html # User invitation email
β β βββ invitation_reminder.html
β βββ whatsapp/
β βββ invitation.txt # WhatsApp invitation message
β βββ invitation_reminder.txt
βββ models/
β βββ invitation.py # Invitation model
βββ schemas/
β βββ invitation.py # Invitation schemas
βββ api/v1/
βββ invitations.py # Invitation endpoints
4. Implementation Steps
Step 1: Database Migration
- Create migration file for
user_invitationstable - Add
invitation_statusandinvitation_methodenums - Run migration
Step 2: Models & Schemas
- Create
src/app/models/invitation.py - Create
src/app/schemas/invitation.pywith:InvitationCreateInvitationResponseInvitationAcceptInvitationValidate
Step 3: Core Services
A. Token Service (src/app/services/token_service.py)
-
generate_invitation_token()- Secure random token (32 chars) -
validate_invitation_token(token)- Check validity & expiry -
invalidate_token(token)- Mark as used/expired
B. Notification Service (src/app/services/notification_service.py)
-
send_whatsapp_invitation(phone, name, invitation_url)- WaSender integration -
send_email_invitation(email, name, invitation_url)- Resend integration -
send_invitation(invitation, method)- Smart routing (WhatsApp β Email fallback) - Template rendering with Jinja2
C. Invitation Service (src/app/services/invitation_service.py)
-
create_invitation(email, phone, role, org_id, invited_by, method) -
validate_invitation(token)- Check token validity -
accept_invitation(token, user_data)- Create Supabase user + local profile -
cancel_invitation(invitation_id)- Cancel pending invitation -
resend_invitation(invitation_id)- Resend notification -
cleanup_expired_invitations()- Background job
Step 4: Email Templates
src/app/templates/emails/base.html
- Create responsive HTML base template
- Include SwiftOps branding
- Header, footer, styling
src/app/templates/emails/invitation.html
- Personalized greeting
- Organization name
- Role information
- CTA button with invitation URL
- Expiry notice
- Support contact
Step 5: WhatsApp Templates
src/app/templates/whatsapp/invitation.txt
Hi {{name}},
You've been invited to join {{organization_name}} on SwiftOps as a {{role}}.
Click here to accept: {{invitation_url}}
This invitation expires in {{expiry_hours}} hours.
Need help? Contact support@atomio.tech
Step 6: API Endpoints
src/app/api/v1/invitations.py
-
POST /invitations- Create invitation (platform_admin, client_admin, contractor_admin) -
GET /invitations- List invitations (filtered by org) -
GET /invitations/{id}- Get invitation details -
POST /invitations/{id}/resend- Resend invitation -
DELETE /invitations/{id}- Cancel invitation -
POST /invitations/validate- Validate token (public endpoint) -
POST /invitations/accept- Accept invitation & create user (public endpoint)
Step 7: Update Existing Endpoints
src/app/api/v1/clients.py
- Add existence check in
create_client() - Return existing client if found (with flag
already_exists: true)
src/app/api/v1/contractors.py
- Add existence check in
create_contractor() - Return existing contractor if found (with flag
already_exists: true)
src/app/api/v1/auth.py
- Remove self-registration endpoint OR restrict to invitation-only
- Update
register()to require invitation token - Link new user to invitation record
Step 8: Authorization Rules
Who can invite whom:
platform_adminβ Can invite anyone to any organizationclient_adminβ Can invite users to their client organization onlycontractor_adminβ Can invite users to their contractor organization only- Other roles β Cannot invite users
Validation:
- Check inviter has permission for target organization
- Validate role is appropriate for organization type
- Prevent duplicate pending invitations
Step 9: Frontend Integration Points
Invitation Flow:
- User receives WhatsApp/Email with link:
https://swiftops.atomio.tech/accept-invitation?token=xxx - Frontend validates token via
POST /invitations/validate - Shows registration form pre-filled with email/phone
- User completes profile (name, password)
- Frontend submits to
POST /invitations/accept - Backend creates Supabase user + local profile
- Returns auth token
- User is logged in
5. Security Considerations
- Tokens are cryptographically secure (secrets.token_urlsafe)
- Tokens expire after 72 hours (configurable)
- Rate limiting on invitation endpoints
- Validate email/phone format
- Prevent invitation spam (max 3 pending per email)
- Log all invitation activities
- HTTPS only for invitation URLs
6. Testing Checklist
- Unit tests for token generation/validation
- Integration tests for invitation flow
- Test WhatsApp delivery (WaSender)
- Test email delivery (Resend)
- Test fallback mechanism (WhatsApp β Email)
- Test expiry handling
- Test duplicate invitation prevention
- Test authorization rules
- Test Supabase user creation on acceptance
7. Background Jobs (Future Enhancement)
- Cleanup expired invitations (daily cron)
- Send reminder for pending invitations (24h before expiry)
- Generate invitation analytics
8. API Response Examples
Create Invitation
POST /api/v1/invitations
{
"email": "john@example.com",
"phone": "+254712345678",
"role": "field_agent",
"contractor_id": "uuid",
"invitation_method": "whatsapp"
}
Response:
{
"id": "uuid",
"email": "john@example.com",
"status": "pending",
"invitation_url": "https://swiftops.atomio.tech/accept-invitation?token=xxx",
"expires_at": "2025-11-19T12:00:00Z",
"whatsapp_sent": true,
"email_sent": false
}
Accept Invitation
POST /api/v1/invitations/accept
{
"token": "xxx",
"first_name": "John",
"last_name": "Doe",
"password": "SecurePass123!"
}
Response:
{
"access_token": "jwt_token",
"token_type": "bearer",
"user": {
"id": "uuid",
"email": "john@example.com",
"full_name": "John Doe",
"role": "field_agent",
"contractor_id": "uuid"
}
}
9. Implementation Order
Phase 1: Foundation (Day 1)
- Database migration
- Models & schemas
- Token service
Phase 2: Core Logic (Day 2)
- Invitation service
- Notification service
- Templates (email + WhatsApp)
Phase 3: API (Day 3)
- Invitation endpoints
- Update client/contractor endpoints
- Authorization logic
Phase 4: Integration (Day 4)
- Update auth flow
- Frontend integration
- Testing
Phase 5: Polish (Day 5)
- Error handling
- Logging
- Documentation
10. Success Criteria
β Platform admin can invite users to any organization β Org admins can invite users to their organization β Invitations sent via WhatsApp (primary) or Email (fallback) β Secure token-based invitation links β Users created in Supabase ONLY after acceptance β Duplicate invitation prevention β Expiry handling β Audit trail for all invitations β Client/contractor existence checks before creation
Notes
- WhatsApp Priority: Default to WhatsApp to conserve Resend credits
- Fallback Logic: If WhatsApp fails, automatically try email
- Token Security: Use
secrets.token_urlsafe(32)for tokens - Expiry: 72 hours default, configurable via env
- Email Domain: swiftops@atomio.tech (configured in Resend)
- Base URL: https://swiftops.atomio.tech (from env)