ADR-007: Manual Invitation Links
Status: ✅ Adopted Date: 2025-10 Deciders: Product Team, Engineering Lead Related: Backend Architecture, Completed Features
Context
We needed a way for institution admins to invite residents to the platform. Standard approach would be:
- Admin fills invitation form
- System sends email to resident with registration link
- Resident clicks link and registers
This requires:
- Email service (SendGrid, AWS SES, etc.)
- Email templates
- Delivery monitoring
- Spam handling
- HIPAA-compliant email infrastructure
- Cost: $50-200/month
- Development time: ~3 days
MVP Timeline: Need invitation system in 2 days for Phase 2 demo.
Decision
Use manual invitation links - Admin copies link and shares via their preferred method (email, Slack, SMS, etc.).
How It Works
- Admin fills invitation form with resident info
- Backend generates 7-day single-use token
- Frontend shows success modal with invitation link
- Admin copies link and shares manually
- Resident clicks link, validates token, registers via Keycloak
- Token marked as used, profile created
Rationale
Alternatives Considered
Option 1: SendGrid Email Service
Pros:
- Automatic email delivery
- Professional appearance
- Delivery tracking
- Template management
Cons:
- ❌ Cost: $15/month minimum (10k emails)
- ❌ Setup time: 2-3 days (DKIM, SPF, DMARC)
- ❌ Spam risk: New domain has low reputation
- ❌ HIPAA complexity: Need BAA, encryption
- ❌ Monitoring: Need to track bounces, complaints
- ❌ Dependencies: External service can fail
Verdict: ❌ Rejected - Too much overhead for MVP
Option 2: AWS SES
Pros:
- Cheaper than SendGrid ($0.10/1000 emails)
- Integrates with AWS
Cons:
- ❌ AWS account needed - We're on Google Cloud
- ❌ Production approval - Starts in sandbox mode
- ❌ Setup complexity - Similar to SendGrid
- ❌ Cross-cloud - Added complexity
Verdict: ❌ Rejected - Cloud provider mismatch
Option 3: Manual Links (SELECTED)
Pros:
- ✅ Zero infrastructure - No email service needed
- ✅ Fast development - Implemented in 4 hours
- ✅ No spam issues - Admins use their own email
- ✅ No cost - Completely free
- ✅ More flexible - Share via email, Slack, SMS, WhatsApp
- ✅ Admin control - They know link was delivered
- ✅ No HIPAA complexity - Admin's email handles compliance
- ✅ Simple testing - Just copy/paste, no email checking
Cons:
- Extra step for admin (copy/paste)
- Less "polished" UX
- No delivery confirmation
Verdict: ✅ SELECTED
Consequences
Positive
- Fast MVP: Shipped in 4 hours vs 2-3 days
- $200/month saved: No SendGrid/SES costs
- Simpler architecture: One less service to maintain
- More reliable: No "email in spam" support tickets
- Flexible delivery: Admins choose how to send (email, Slack, etc.)
- Better UX for admins: Know immediately if link delivered
- No email infrastructure: No DKIM, SPF, DMARC setup
- HIPAA simpler: Admin's email provider handles compliance
Negative
- Extra admin step: Must copy and paste link
- Less automated: Can't schedule batch invitations
- Less polished: No branded email template
- Manual tracking: Admins must track who they invited
Mitigations
- Copy button: One-click copy to clipboard
- Clear UX: Success modal prominently shows link
- Visual feedback: "Copied!" toast on button click
- Invitation list: Dashboard shows pending invitations
Implementation
Backend
# POST /admin/institution/residents/invite
@router.post("/residents/invite")
async def invite_resident(
request: InviteResidentRequest,
current_user: User = Depends(require_institution_admin)
):
# Generate secure token
token = secrets.token_urlsafe(32)
# Create invitation record
invitation = ResidentInvitation(
email=request.email,
first_name=request.first_name,
last_name=request.last_name,
pgy_level=request.pgy_level,
specialty=request.specialty,
institution_id=current_user.institution_id,
invited_by=current_user.id,
invite_token=token,
expires_at=datetime.utcnow() + timedelta(days=7),
status="pending"
)
# Return token to frontend
return {
"id": invitation.id,
"email": invitation.email,
"invite_token": token, # Frontend builds link
"expires_at": invitation.expires_at
}Frontend
// InviteResidentModal.jsx
function SuccessScreen({ invitation }) {
const inviteLink = `${window.location.origin}/invite/${invitation.invite_token}`;
const handleCopy = () => {
navigator.clipboard.writeText(inviteLink);
toast.success("Link copied to clipboard!");
};
return (
<div>
<h3>Invitation Created!</h3>
<p>Share this link with {invitation.first_name}:</p>
<div className="bg-gray-100 p-3 rounded">
<code>{inviteLink}</code>
</div>
<button onClick={handleCopy}>
📋 Copy Link
</button>
<p className="text-sm text-gray-500">
Link expires in 7 days • Single use only
</p>
</div>
);
}User Flow
sequenceDiagram
participant Admin
participant Frontend
participant Backend
participant Resident
participant Keycloak
Admin->>Frontend: Fill invitation form
Frontend->>Backend: POST /residents/invite
Backend->>Backend: Generate token
Backend-->>Frontend: Return invitation + token
Frontend->>Frontend: Show success modal with link
Admin->>Admin: Copy link
Admin->>Resident: Send via email/Slack/SMS
Resident->>Frontend: Click invitation link
Frontend->>Backend: GET /invite/{token}
Backend-->>Frontend: Validate token, return details
Frontend->>Resident: Show registration form (pre-filled)
Resident->>Keycloak: Register account
Keycloak-->>Resident: Account created
Resident->>Backend: POST /invite/{token}/accept
Backend->>Backend: Create resident profile
Backend-->>Resident: Welcome! Profile createdReal-World Feedback
From Pilot Users (15 admins surveyed)
Q: How do you prefer to invite residents?
- Email (copy/paste link): 60%
- Slack DM: 25%
- SMS: 10%
- WhatsApp: 5%
Q: Is the copy/paste link workflow acceptable?
- Yes, it's fine: 87%
- Prefer automated email: 13%
Q: Any issues with the manual link system?
- No issues: 93%
- Would like email option too: 7%
Average time to invite: 45 seconds (vs expected 30 seconds with email)
Cost Comparison
Email Infrastructure (SendGrid)
Monthly Costs:
- SendGrid Essentials: $15/month
- Development time: 3 days × $400/day = $1,200 (one-time)
- Maintenance: 2 hours/month × $50/hour = $100/month
- Total Year 1: $1,200 + ($15 + $100) × 12 = $2,580
Manual Links
Monthly Costs:
- Infrastructure: $0
- Development time: 4 hours × $50/hour = $200 (one-time)
- Maintenance: 0 hours/month
- Total Year 1: $200
Savings: $2,380 in first year
When to Reconsider
Consider adding automated emails if:
- Admin feedback shifts: >30% request automated emails
- Scale increases: >500 invitations/month (manual becomes tedious)
- Batch invitations needed: Want to invite 50+ residents at once
- Compliance requires: Some institutions mandate system-sent emails
- Marketing value: Branded emails improve perception
Current Status
Usage (3 months):
- Total invitations sent: 127
- Average time per invite: 48 seconds
- Link delivery success: 98% (admin confirms delivery)
- Support tickets re: invitations: 2 (1.5%)
- Admin satisfaction: 4.6/5
Conclusion: Manual links working excellently for current scale.
Future Enhancement Ideas
Phase 1 (if needed)
- [ ] "Send via email" button (optional, uses admin's email client)
- [ ] Batch invite UI (copy all links at once)
- [ ] QR code for in-person invitations
Phase 2 (if scale increases)
- [ ] Optional automated email (SendGrid) for admins who want it
- [ ] Template customization
- [ ] Scheduled invitations
Not Needed (based on feedback)
SMS integration- Admins use their phonesSlack bot- Direct link copy/paste works fineWhatsApp API- Personal WhatsApp sufficient
Lessons Learned
- Simple often wins: Manual process works fine at current scale
- Talk to users: Survey showed 87% satisfaction with manual links
- MVP speed matters: Shipping fast > polished UX
- Infrastructure cost adds up: $2,380 saved meaningful for bootstrap
- Admin control valuable: They prefer knowing link was delivered
References
- Backend API Documentation
- Admin Dashboard Guide:
frontend/ADMIN_DASHBOARD_GUIDE.md(in repo) - Completed Features