Skip to content

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 line
  • body — Message content

Non-Encrypted Fields

  • id — Message identifier
  • from / to — Addresses (needed for routing)
  • type — Message type
  • sentAt / read — Metadata

Storage Structure

Each user has their own mailbox in KV:

mail:{address_lowercase} → [MailMessage, MailMessage, ...]
mail:broadcasts → [MailMessage, ...]  // Platform announcements

When a message is sent:

  1. Encrypted copy added to sender's mailbox
  2. 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/mail

Response:

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/mail

PATCH /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/mail

DELETE /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/mail

Message Types

TypeDescription
directUser-to-user message
questQuest-related notification
platform-updateSystem announcement (from platform wallet)
disputeDispute-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:

  1. Fetches mail on mount via getMail(authToken)
  2. Displays inbox with unread indicators
  3. Supports compose modal for new messages
  4. Reply functionality via pre-filled compose
  5. 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

  1. Authentication required — All mail endpoints require valid JWT
  2. Address verification — Users can only access their own mailbox
  3. At-rest encryption — Content encrypted before KV storage
  4. No plaintext logs — Encrypted data never logged

Fetch Quests — Decentralized Gig-Work Platform