Mail System
The mail system provides encrypted messaging between users on the Fetch Quests platform.
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │────▶│ /api/mail │────▶│ Vercel KV │
│ (MailPage) │ │ (mail.js) │ │ (encrypted) │
└─────────────────┘ └─────────────────┘ └─────────────────┘Encryption
All mail content is encrypted at rest using AES-256-GCM:
javascript
// Encryption key derived from JWT_SECRET
const key = createHash('sha256').update(process.env.JWT_SECRET).digest();
// Encrypt
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
encrypted += cipher.final('base64');
const tag = cipher.getAuthTag();
// Format: iv:tag:ciphertext (all base64)
return `${iv.toString('base64')}:${tag.toString('base64')}:${encrypted}`;Encrypted Fields
subject— Message subject linebody— Message content
Non-Encrypted Fields
id— Message identifierfrom/to— Addresses (needed for routing)type— Message typesentAt/read— Metadata
Storage Structure
Each user has their own mailbox in KV:
mail:{address_lowercase} → [MailMessage, MailMessage, ...]
mail:broadcasts → [MailMessage, ...] // Platform announcementsWhen a message is sent:
- Encrypted copy added to sender's mailbox
- Encrypted copy added to recipient's mailbox
This ensures both parties can access sent/received mail independently.
API Endpoints
GET /api/mail
Retrieve authenticated user's mailbox.
bash
curl -H "Authorization: Bearer {token}" \
https://fetchquests.io/api/mailResponse:
json
[
{
"id": "mail-1234567890-abc",
"from": "0x123...",
"to": "0x456...",
"subject": "Quest completed!",
"body": "Great work on the frontend task...",
"type": "quest",
"sentAt": "2025-03-01T12:00:00.000Z",
"read": false
}
]POST /api/mail
Send a new message.
bash
curl -X POST \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"to": "0x456...", "subject": "Hello", "body": "Message content"}' \
https://fetchquests.io/api/mailPATCH /api/mail
Mark message as read.
bash
curl -X PATCH \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"messageId": "mail-1234567890-abc", "read": true}' \
https://fetchquests.io/api/mailDELETE /api/mail
Delete a message from user's mailbox.
bash
curl -X DELETE \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"messageId": "mail-1234567890-abc"}' \
https://fetchquests.io/api/mailMessage Types
| Type | Description |
|---|---|
direct | User-to-user message |
quest | Quest-related notification |
platform-update | System announcement (from platform wallet) |
dispute | Dispute-related message |
Broadcasts
Platform-wide announcements are sent to to: '*' and stored in mail:broadcasts. When fetching mail, broadcasts are merged with the user's personal mailbox.
Only the platform wallet (PLATFORM_WALLET env var) can send broadcasts.
Frontend Integration
The MailPage component:
- Fetches mail on mount via
getMail(authToken) - Displays inbox with unread indicators
- Supports compose modal for new messages
- Reply functionality via pre-filled compose
- Delete with confirmation
Unread Count Badge
AppContext tracks unread mail count and displays a badge on the Mail nav icon:
typescript
const refreshMailCount = useCallback(async () => {
if (!authToken) return;
const mail = await getMail(authToken);
const unread = mail.filter(m => !m.read && m.to !== '*').length;
_setUnreadMailCount(unread);
}, [authToken]);Security Considerations
- Authentication required — All mail endpoints require valid JWT
- Address verification — Users can only access their own mailbox
- At-rest encryption — Content encrypted before KV storage
- No plaintext logs — Encrypted data never logged