Cloudflare Pages Functions · Firebase Auth + Firestore

SMTP API
Reference

Functional route reference for the bundled API backend: payloads, response examples, role access, whitelist rules, API-key counters, admin operations, and setup configuration.

One send endpoint. Server-side authorization.

All login, permissions, SMTP secrets and Firestore operations are handled in Functions. Clients call the HTTP API; they do not use a Firebase client SDK.

3roles
5Paid SMTP limit
43documented routes

Roles and authorization rules

PermissionFreePaidAdmin
Sign up / sign inYesYesYes
Verified domainsYesYesYes
Personal SMTP accountsNoneUp to 5Unlimited
Sending key per verified domainOne per domain; default branded SMTP——
Sending key per SMTP account—One per SMTPOne per SMTP
Admin operationsNoNoYes

Public signup always assigns role free. Roles accepted by admin role management are free, paid, and admin. The server checks the current role in Firestore for protected operations.

One key = one verified domain. No wildcard or automatic subdomain access. Paid/Admin SMTP accounts must bind to one verified domain and their saved fromEmail must match it exactly.

Developer Mode: when enabled, sending requests that include Origin or Referer are rejected and headerless Bruno/cURL tests are allowed. When disabled, a browser request requires the exact https://your-verified-domain Origin. Headers are not strong authentication for non-browser callers; keep API keys private.

API keys in URL: the required endpoint is POST /api/send/:apikey. Configure logs and analytics to redact that path segment.

Common response format

Success response

{
  "success": true,
  "message": "OK",
  "data": {}
}

Error response

{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message."
  }
}

Session-protected routes require Authorization: Bearer <sessionToken>. JSON POST/PATCH requests require Content-Type: application/json. Examples below are illustrative; they contain no working credentials.

Authentication & account

5 route(s)
GET/api/healthPublic

Health check

Returns basic liveness status. It does not reveal whether secrets are configured.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "status": "ok",
    "service": "SMTP API",
    "timestamp": "2026-10-26T12:00:00.000Z"
  }
}
POST/api/authPublic

Sign up or sign in

One route for both auth actions. Signup requires name, email and password and always creates a Free user. Signin does not require name. Firebase Authentication validates the password; the Function issues its own opaque session token.

Request payload
// Signup
{
  "action": "signup",
  "name": "Vuyufambom Boris",
  "email": "user@example.com",
  "password": "replace-with-a-strong-password"
}

// Signin
{
  "action": "signin",
  "email": "user@example.com",
  "password": "your-password"
}
Example response
{
  "success": true,
  "message": "Signed in.",
  "data": {
    "user": {
      "uid": "FIREBASE_UID",
      "name": "Vuyufambom Boris",
      "email": "user@example.com",
      "role": "free",
      "status": "active",
      "settings": {
        "developerMode": false
      }
    },
    "token": "sess.FIREBASE_UID.RANDOM_SECRET",
    "tokenType": "Bearer",
    "expiresIn": 604800
  }
}
NOTES

Signup responds 201; signin responds 200. Never provide role in the signup payload. Store the returned token securely and send it as Authorization: Bearer <token>.

POST/api/logoutSession

Logout

Revokes only the current Function session. The user must sign in again to obtain another session.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "Logged out.",
  "data": {
    "loggedOut": true
  }
}
NOTES

Header: Authorization: Bearer <sessionToken>.

GET/api/meSession

Current user

Returns current profile, role, account status and Developer Mode without credential secrets.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "user": {
      "uid": "FIREBASE_UID",
      "name": "Boris Example",
      "email": "user@example.com",
      "role": "free",
      "status": "active",
      "settings": {
        "developerMode": false
      }
    }
  }
}
DELETE/api/accountSession

Delete my account

Deletes the signed-in user’s Firebase Authentication account and supported Firestore user subcollections. The final active admin cannot delete itself.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "Account deleted.",
  "data": {
    "uid": "FIREBASE_UID",
    "deleted": true,
    "cleanup": {
      "domainsDeleted": 1,
      "smtpDeleted": 0,
      "keysDeleted": 1,
      "sessionsDeleted": 1,
      "settingsDeleted": 1
    }
  }
}
NOTES

Destructive; no JSON body required.

Settings & developer mode

2 route(s)
GET/api/settingsSession

Read settings

Returns the user’s settings/general document.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "settings": {
      "developerMode": false,
      "updatedAt": "2026-10-26T12:00:00.000Z"
    }
  }
}
PATCH/api/settingsSession

Update Developer Mode

Developer Mode must be a boolean. When true, sends without Origin or Referer are allowed for Bruno/CLI testing and requests with either header are rejected. When false, send requests require the exact whitelisted HTTPS Origin.

Request payload
{
  "developerMode": true
}
Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "settings": {
      "developerMode": true,
      "updatedAt": "2026-10-26T12:00:00.000Z"
    }
  }
}
NOTES

Do not include this switch in an email send request. Origin is not proof against a non-browser caller that can forge headers.

Domain verification & keys

10 route(s)
GET/api/domainsSession

List domains

Lists the current user’s registered domains without verification hashes or the original verification token.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "domains": [
      {
        "id": "DOMAIN_ID",
        "domain": "example.com",
        "status": "active",
        "verified": true,
        "apiKeyId": "KEY_ID",
        "verifiedAt": "2026-10-26T12:00:00.000Z"
      }
    ]
  }
}
POST/api/domainsSession

Register a domain

Creates a globally unique pending domain claim and returns a DNS TXT challenge. Add the exact TXT name and value at your DNS host.

Request payload
{
  "domain": "example.com"
}
Example response
{
  "success": true,
  "message": "Domain added. Publish its TXT verification record.",
  "data": {
    "domain": {
      "id": "DOMAIN_ID",
      "domain": "example.com",
      "status": "pending",
      "verified": false,
      "verification": {
        "type": "TXT",
        "name": "_smtp-api-verification.example.com",
        "value": "smtp-api-verification=RANDOM_TOKEN"
      }
    }
  }
}
NOTES

Domain must be a hostname only: no scheme, path, port, wildcard, localhost or IP.

POST/api/verify/:domainIdSession

Verify a domain

Checks the TXT record through Cloudflare DNS-over-HTTPS. Successful verification activates the domain.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "Domain verified.",
  "data": {
    "domain": {
      "id": "DOMAIN_ID",
      "domain": "example.com",
      "verified": true,
      "status": "active",
      "verifiedAt": "2026-10-26T12:00:00.000Z"
    }
  }
}
NOTES

Returns 409 DNS_VERIFICATION_PENDING if the exact TXT record is not visible yet.

DELETE/api/domains/:domainIdSession

Delete a domain

Deletes the domain, removes its global claim and revokes its Free-domain key. Deletion is rejected while an active SMTP account uses the domain.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "Domain deleted and its free-domain key revoked.",
  "data": {
    "id": "DOMAIN_ID",
    "deleted": true,
    "apiKeyRevoked": true
  }
}
NOTES

Destructive. Delete SMTP accounts first if the domain is in use.

GET/api/keysSession

List my API keys

Lists API-key metadata, statuses, domain/SMTP binding, counts and last-used time. It never returns the full secret or hash.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "keys": [
      {
        "id": "KEY_ID",
        "keyPrefix": "sk_live.UID.KEY_ID",
        "type": "free_domain",
        "domainId": "DOMAIN_ID",
        "smtpId": null,
        "status": "active",
        "sendCount": 12,
        "successfulSends": 11,
        "failedSends": 1,
        "lastUsedAt": "2026-10-26T12:00:00.000Z"
      }
    ]
  }
}
GET/api/key/:keyIdSession

Get key metadata

Returns one owned key’s metadata and counters, without the secret hash.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "key": {
      "id": "KEY_ID",
      "type": "free_domain",
      "domainId": "DOMAIN_ID",
      "status": "active",
      "sendCount": 12,
      "successfulSends": 11,
      "failedSends": 1
    }
  }
}
POST/api/key/:domainIdSession · Free

Issue/rotate Free domain key

Creates or rotates the one sending key for a verified domain. Existing send counters remain when the same key record is rotated. Not available to Paid/Admin roles.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "Domain API key generated.",
  "data": {
    "apiKey": "sk_live.UID.KEY_ID.RANDOM_SECRET",
    "key": {
      "id": "KEY_ID",
      "type": "free_domain",
      "domainId": "DOMAIN_ID",
      "status": "active",
      "sendCount": 12,
      "successfulSends": 11,
      "failedSends": 1
    },
    "domain": "example.com"
  }
}
NOTES

Save apiKey immediately; only this creation/rotation response contains the full secret.

POST/api/key/:keyId/regenerateSession

Regenerate my key

Replaces a user-owned API secret while keeping the same key record, linked domain/SMTP account, send counts and last-used history. Admin-blocked keys must be unblocked first.

Request payload
{}
Example response
{
  "success": true,
  "message": "API key regenerated.",
  "data": {
    "apiKey": "sk_live.UID.KEY_ID.NEW_RANDOM_SECRET",
    "key": {
      "id": "KEY_ID",
      "status": "active",
      "sendCount": 125,
      "successfulSends": 120,
      "failedSends": 5
    },
    "warning": "Save this API key now. It will not be shown again."
  }
}
NOTES

Use only for the currently permitted key type/role. Old secret becomes invalid.

POST/api/key/smtp/:smtpIdSession · Paid/Admin

Regenerate SMTP key

Regenerates the API key attached to an owned SMTP account. SMTP account must be active.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "SMTP API key regenerated.",
  "data": {
    "apiKey": "sk_live.UID.SMTP_ID.NEW_SECRET",
    "key": {
      "id": "SMTP_ID",
      "type": "smtp",
      "smtpId": "SMTP_ID",
      "domainId": "DOMAIN_ID",
      "status": "active",
      "sendCount": 20
    },
    "warning": "Save this API key now. It will not be shown again."
  }
}
NOTES

The URL parameter is the SMTP ID (the implementation uses the SMTP ID as its API-key document ID).

DELETE/api/key/:keyIdSession

Revoke my API key

Revokes an owned key. Counters and historical record remain. A new secret can be issued later if the linked domain/account remains eligible.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "API key revoked.",
  "data": {
    "key": {
      "id": "KEY_ID",
      "status": "revoked",
      "sendCount": 125
    },
    "revoked": true
  }
}
NOTES

Revocation is permanent for the current secret.

SMTP account management

6 route(s)
GET/api/smtpSession

List SMTP accounts

Lists the authenticated user’s saved SMTP accounts without passwords. Supports pagination using ?limit=50&pageToken=TOKEN.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "smtp": [],
    "nextPageToken": null,
    "limit": 5,
    "role": "paid"
  }
}
NOTES

Free users receive no personal SMTP accounts and limit 0. Paid max is 5; Admin limit is null (unlimited).

POST/api/smtpSession · Paid/Admin

Create SMTP account

Saves one SMTP account with AES-256-GCM encrypted password and creates one API key bound to exactly one verified domain. Free role is rejected.

Request payload
{
  "name": "Business SMTP",
  "host": "mail.example.com",
  "port": 587,
  "secure": false,
  "username": "sales@example.com",
  "password": "SMTP_APP_PASSWORD",
  "fromName": "Example Business",
  "fromEmail": "sales@example.com",
  "domainId": "DOMAIN_ID"
}
Example response
{
  "success": true,
  "message": "SMTP account created and its API key generated.",
  "data": {
    "smtp": {
      "id": "SMTP_ID",
      "name": "Business SMTP",
      "host": "mail.example.com",
      "port": 587,
      "secure": false,
      "username": "sales@example.com",
      "fromName": "Example Business",
      "fromEmail": "sales@example.com",
      "domainId": "DOMAIN_ID",
      "domain": "example.com",
      "apiKeyId": "SMTP_ID",
      "status": "active",
      "passwordConfigured": true
    },
    "apiKey": "sk_live.UID.SMTP_ID.RANDOM_SECRET",
    "key": {
      "id": "SMTP_ID",
      "type": "smtp",
      "domainId": "DOMAIN_ID",
      "smtpId": "SMTP_ID",
      "status": "active",
      "sendCount": 0,
      "successfulSends": 0,
      "failedSends": 0
    }
  }
}
NOTES

Password is encrypted before Firestore write. Never paste real credentials into public examples. Ports: 465 with secure=true; 587/2525 with secure=false and mandatory STARTTLS. Port 25 is blocked.

GET/api/smtp/:smtpIdSession · Owner

Get SMTP account

Returns non-secret SMTP metadata and passwordConfigured flag.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "smtp": {
      "id": "SMTP_ID",
      "name": "Business SMTP",
      "host": "mail.example.com",
      "port": 587,
      "secure": false,
      "username": "sales@example.com",
      "fromName": "Example Business",
      "fromEmail": "sales@example.com",
      "domainId": "DOMAIN_ID",
      "status": "active",
      "passwordConfigured": true
    }
  }
}
PATCH/api/smtp/:smtpIdSession · Owner

Update SMTP account

Partial update of SMTP settings. Password remains unchanged unless password is supplied. The domain binding cannot change; delete and recreate the account to move it to another verified domain.

Request payload
{
  "fromName": "New Business Name",
  "password": "NEW_SMTP_APP_PASSWORD"
}
Example response
{
  "success": true,
  "message": "SMTP account updated.",
  "data": {
    "smtp": {
      "id": "SMTP_ID",
      "fromName": "New Business Name",
      "passwordConfigured": true,
      "domainId": "DOMAIN_ID",
      "status": "active"
    }
  }
}
NOTES

Allowed fields include name, host, port, secure, username, password, fromName and fromEmail. fromEmail must match the account’s verified domain.

DELETE/api/smtp/:smtpIdSession · Owner

Delete SMTP account

Deletes the stored SMTP config and revokes its associated key. API key usage history remains.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "SMTP account deleted and its API key revoked.",
  "data": {
    "id": "SMTP_ID",
    "deleted": true,
    "apiKeyRevoked": true
  }
}
NOTES

Destructive.

POST/api/smtp/:smtpId/testSession · Owner

Test SMTP connection

Opens a TLS-protected connection and tests SMTP authentication without sending an email.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "SMTP connection and authentication succeeded.",
  "data": {
    "connected": true,
    "authenticated": true,
    "host": "mail.example.com",
    "port": 587,
    "tls": true
  }
}
NOTES

Requires outbound TCP access supported by Cloudflare and SMTP AUTH LOGIN support.

Email delivery

1 route(s)
POST/api/send/:apikeyAPI key in path

Send an email

The sole sending endpoint. Key selects the account: Free domain key uses Cloudflare’s default branded SMTP; Paid/Admin SMTP key uses that saved SMTP. Sender identity cannot be overridden. Recipient sender-domain and Origin policies are validated.

Request payload
{
  "to": "customer@example.net",
  "cc": [
    "copy@example.net"
  ],
  "bcc": [
    "audit@example.net"
  ],
  "subject": "Welcome",
  "message": "Welcome to our platform.",
  "html": "<p>Welcome to our platform.</p>",
  "replyTo": "support@example.com"
}
Example response
{
  "success": true,
  "message": "Email accepted by SMTP.",
  "data": {
    "sendId": "SEND_ID",
    "status": "accepted_by_smtp",
    "acceptedRecipients": 3,
    "messageId": "<message@example.com>",
    "apiKeyId": "KEY_ID",
    "transport": "user_smtp",
    "usage": {
      "sendCount": 13,
      "successfulSends": 12,
      "failedSends": 1
    }
  }
}
NOTES

Required: to, subject, and at least one of message/html. Up to 20 unique to/cc/bcc recipients. from/fromEmail/fromName are rejected. Developer Mode ON requires no Origin or Referer; OFF requires exact HTTPS Origin.

Admin controls

19 route(s)
GET/api/adminAdmin session

Admin overview

Simple authenticated admin status.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "status": "ok",
    "role": "admin",
    "timestamp": "2026-10-26T12:00:00.000Z"
  }
}
GET/api/admin/healthAdmin session

Admin health

Checks authenticated admin access.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "status": "ok",
    "role": "admin",
    "timestamp": "2026-10-26T12:00:00.000Z"
  }
}
GET/api/admin/checkAdmin session

Configuration diagnostics

Checks required environment variables and verifies that the service account can obtain an OAuth token. No secret values are returned.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "checks": {
      "FIREBASE_PROJECT_ID": true,
      "FIREBASE_WEB_API_KEY": true,
      "FIREBASE_SERVICE_ACCOUNT_JSON": true,
      "SMTP_ENCRYPTION_KEY": true,
      "DEFAULT_SMTP_HOST": true
    },
    "firebaseServiceAccount": "ok",
    "complete": true
  }
}
NOTES

Aliases: GET /api/check and GET /api/admin/diagnostics.

GET/api/admin/usersAdmin session

List/search users

Paginates users; query params: limit (1-100), q (search current page by name/email/role), pageToken.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "users": [
      {
        "uid": "USER_UID",
        "name": "Boris Example",
        "email": "user@example.com",
        "role": "free",
        "status": "active",
        "settings": {
          "developerMode": false
        }
      }
    ],
    "nextPageToken": null
  }
}
NOTES

Example: /api/admin/users?limit=50&q=boris

GET/api/admin/user/:uidAdmin session

Get user details

Returns profile, domain metadata, SMTP metadata and key counts/statuses. It excludes SMTP ciphertext and API-key hashes.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "user": {
      "uid": "USER_UID",
      "name": "Boris Example",
      "email": "user@example.com",
      "role": "paid",
      "status": "active",
      "settings": {
        "developerMode": false
      }
    },
    "domains": [],
    "smtp": [],
    "keys": []
  }
}
PATCH/api/admin/user/:uidAdmin session

Update user account

Updates name and/or email in Firebase Authentication and Firestore. Email is marked unverified after change.

Request payload
{
  "name": "Updated Name",
  "email": "new-email@example.com"
}
Example response
{
  "success": true,
  "message": "User account updated.",
  "data": {
    "user": {
      "uid": "USER_UID",
      "name": "Updated Name",
      "email": "new-email@example.com",
      "role": "free",
      "status": "active",
      "settings": {
        "developerMode": false
      }
    }
  }
}
NOTES

Only name/email are accepted here. Use the role route for role changes.

PATCH/api/admin/user/:uid/roleAdmin session

Change user role

Sets the role to free, paid or admin. Prevents self-role changes and protects the last active admin. A role transition away from Free revokes Free-domain keys; Paid users must have no more than five active SMTP accounts.

Request payload
{
  "role": "paid"
}
Example response
{
  "success": true,
  "message": "User role updated.",
  "data": {
    "user": {
      "uid": "USER_UID",
      "role": "paid",
      "status": "active",
      "revokedFreeKeys": 1
    }
  }
}
NOTES

Allowed roles: free, paid, admin.

POST/api/admin/user/:uid/banAdmin session

Ban user

Marks the user banned, revokes their active sessions and tries to disable the Firebase Auth account. API sends are denied based on Firestore status immediately.

Request payload
{
  "reason": "Suspicious sending activity"
}
Example response
{
  "success": true,
  "message": "User banned.",
  "data": {
    "uid": "USER_UID",
    "status": "banned",
    "firebaseDisabled": true
  }
}
NOTES

Does not allow self-ban or banning the last active admin.

POST/api/admin/user/:uid/unbanAdmin session

Unban user

Re-enables the Firebase Auth account then marks the Firestore user active. Old sessions are not restored.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "User unbanned.",
  "data": {
    "uid": "USER_UID",
    "status": "active"
  }
}
DELETE/api/admin/user/:uidAdmin session

Delete user

Deletes Firebase Auth user and supported Firestore subcollections and domain claims. Protects the last active admin and writes an admin audit event.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "User and associated account data deleted.",
  "data": {
    "uid": "USER_UID",
    "deleted": true,
    "cleanup": {
      "domainsDeleted": 1,
      "smtpDeleted": 0,
      "keysDeleted": 1,
      "sessionsDeleted": 0,
      "settingsDeleted": 1
    }
  }
}
NOTES

Destructive. Large cleanup jobs that exceed the pagination safety limit are rejected rather than partially deleting records.

GET/api/admin/user/:uid/keysAdmin session

List user keys

Lists API key records and usage without hashes or full secrets.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "keys": [
      {
        "id": "KEY_ID",
        "type": "smtp",
        "status": "active",
        "sendCount": 125,
        "successfulSends": 120,
        "failedSends": 5
      }
    ]
  }
}
GET/api/admin/user/:uid/key/:keyIdAdmin session

Get user key

Returns one key metadata record without key hash/secret.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "key": {
      "id": "KEY_ID",
      "status": "active",
      "sendCount": 125,
      "successfulSends": 120,
      "failedSends": 5
    }
  }
}
POST/api/admin/user/:uid/key/:keyId/blockAdmin session

Block API key

Blocks a key immediately. Existing usage counters remain.

Request payload
{
  "reason": "Suspicious sending activity"
}
Example response
{
  "success": true,
  "message": "API key blocked.",
  "data": {
    "id": "KEY_ID",
    "status": "blocked",
    "sendCount": 125
  }
}
POST/api/admin/user/:uid/key/:keyId/unblockAdmin session

Unblock API key

Restores a blocked key only if the key owner has an active account. Revoked keys cannot be unblocked.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "API key unblocked.",
  "data": {
    "id": "KEY_ID",
    "status": "active",
    "sendCount": 125
  }
}
POST/api/admin/user/:uid/key/:keyId/regenerateAdmin session

Regenerate user key

Admin replacement key secret, same key ID, preserved counters. The full secret is returned only in this response.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "API key regenerated.",
  "data": {
    "apiKey": "sk_live.UID.KEY_ID.NEW_SECRET",
    "key": {
      "id": "KEY_ID",
      "status": "active",
      "sendCount": 125
    },
    "warning": "The full API key is shown only in this response. Save it securely now."
  }
}
GET/api/admin/smtpAdmin session

List SMTP metadata

Returns SMTP metadata across users without encrypted passwords. Current endpoint scans a bounded set of users for dashboard overview use.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "smtp": [
      {
        "uid": "USER_UID",
        "smtpId": "SMTP_ID",
        "host": "mail.example.com",
        "username": "sales@example.com",
        "fromEmail": "sales@example.com",
        "passwordConfigured": true
      }
    ]
  }
}
NOTES

Never logs/decrypts credentials as part of inventory.

GET/api/admin/usageAdmin session

Aggregate usage

Summarizes users, keys, SMTP accounts, verified domains and send counters. Response includes `truncated` when the bounded scan does not cover all users.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "userCount": 12,
    "keyCount": 20,
    "smtpCount": 8,
    "verifiedDomainCount": 15,
    "sendCount": 450,
    "successfulSends": 420,
    "failedSends": 30,
    "truncated": false
  }
}
GET/api/admin/logsAdmin session

Admin audit logs

Returns recent administrative audit entries. Do not place credentials, full API keys or SMTP passwords into reason fields.

Request payload

No JSON body required.

Example response
{
  "success": true,
  "message": "OK",
  "data": {
    "logs": [
      {
        "actorUid": "ADMIN_UID",
        "action": "user.role_change",
        "targetUid": "USER_UID",
        "newRole": "paid",
        "createdAt": "2026-10-26T12:00:00.000Z"
      }
    ]
  }
}
POST/api/admin/testAdmin session

Test/send admin email

With `testConnection:true`, test the default SMTP or a selected user SMTP without sending. Otherwise send a real test message through the default SMTP or the requested user SMTP.

Request payload
// Test default SMTP without sending
{ "testConnection": true }

// Test a user's saved SMTP without sending
{ "testConnection": true, "userId": "USER_UID", "smtpId": "SMTP_ID" }

// Send a real test message through default SMTP
{ "to": "admin@example.com", "subject": "SMTP test", "message": "Test message." }
Example response
{
  "success": true,
  "message": "Test email accepted by SMTP.",
  "data": {
    "status": "accepted_by_smtp",
    "acceptedRecipients": 1,
    "messageId": "<message@example.com>"
  }
}
NOTES

Admin-only. Sending a real test message may incur provider charges or recipient impact; prefer testConnection:true first.

Firestore data model

users/{uid}
  name, email, role, status, createdAt, updatedAt
  settings/general
    developerMode, updatedAt, updatedBy
  domains/{domainId}
    domain, verified, status, verificationTxtName, verificationTokenHash, apiKeyId
  smtp/{smtpId}
    host, port, secure, username, encryptedPassword, fromName, fromEmail, domainId, apiKeyId, status
  smtpSlots/{smtpId}
    status: reserved | active, smtpId, createdAt, updatedAt
  apiKeys/{keyId}
    keyHash, keyPrefix, type, domainId, smtpId, status, sendCount, successfulSends, failedSends, lastUsedAt
  sessions/{sessionHash}
    status, createdAt, expiresAt
domainClaims/{sha256-of-domain}
  domain, uid, domainId, status
adminLogs/{logId}
  actorUid, action, targetUid, limited action metadata, createdAt

Only the API stores/manages these documents with the service account. API keys are stored as SHA-256 hashes; the full key appears only in the create/regenerate response. SMTP passwords are encrypted using AES-256-GCM before being stored.

Cloudflare variables and secrets

In Cloudflare Dashboard open Workers & Pages → your Pages project → Settings → Variables and Secrets. Set Production and Preview separately; use a separate Firebase project and SMTP credentials for Preview.

NameTypeExample / details
FIREBASE_PROJECT_IDVariableFirebase project ID; must match service account JSON.
FIREBASE_WEB_API_KEYVariableUsed by Firebase Auth REST signup/signin only.
FIREBASE_SERVICE_ACCOUNT_JSONSecretEntire service-account JSON, including private key.
SMTP_ENCRYPTION_KEYSecretThe single data-encryption key for stored SMTP passwords. Must be base64 for exactly 32 random bytes.
DEFAULT_SMTP_HOSTVariableSMTP server host.
DEFAULT_SMTP_PORTVariable465, 587 or 2525. Cloudflare blocks port 25.
DEFAULT_SMTP_SECUREVariabletrue for 465; false for 587/2525, where STARTTLS is required.
DEFAULT_SMTP_USERNAMEVariablePlatform default SMTP login.
DEFAULT_SMTP_PASSWORDSecretPlatform default SMTP password/app password.
DEFAULT_FROM_EMAILVariableFixed branded From address for Free-domain keys.
DEFAULT_FROM_NAMEVariableFixed branded From display name.
One encryption key: generate the value with openssl rand -base64 32 and save it as the Cloudflare Secret SMTP_ENCRYPTION_KEY. Firebase service-account JSON and the default SMTP password are separate credentials, not additional data-encryption keys. Changing the encryption key without re-encrypting stored passwords makes them unreadable.

Setup and deployment checklist

  1. Create a Firebase project and enable Email/Password sign-in under Firebase Authentication.
  2. Create a Firestore database. Enable the Identity Toolkit API and Cloud Firestore API for the project.
  3. Create a service account. Grant roles/datastore.user and the required Firebase Authentication Admin permissions. Store its full JSON as FIREBASE_SERVICE_ACCOUNT_JSON in Cloudflare Secrets.
  4. Deploy firestore.rules. They deny all direct client reads/writes. The backend accesses Firestore as a privileged service account through Google IAM; clients should not use the Firebase client SDK against these records.
  5. Configure all Cloudflare variables and secrets above. Do not put secret values in wrangler.toml, the HTML, or GitHub.
  6. Connect the Git repository to Cloudflare Pages. Build command: npm install; output directory: public. Keep functions/ at the repository root.
  7. Deploy, then call GET /api/health. Sign up, locate the created users/{uid} document in Firestore, and manually set the trusted first account’s role to admin.
  8. Sign in again and call GET /api/admin/check. Then add a domain, publish the TXT record, verify it, create a Free key or Paid/Admin SMTP, test SMTP, and finally send a test email.

Local development

npm install
cp .dev.vars.example .dev.vars
# edit .dev.vars with real test credentials
npm run check
npm test
npm run dev

Never commit .dev.vars, service-account JSON, session tokens or API keys.

Bruno quick start

Environment variables example:

{
  "baseUrl": "http://127.0.0.1:8788",
  "sessionToken": "",
  "apiKey": ""
}
  1. POST {{baseUrl}}/api/auth with signup or signin JSON and copy data.token into sessionToken.
  2. Add Authorization: Bearer {{sessionToken}} and Content-Type: application/json to session/admin requests.
  3. POST /api/domains, add the returned TXT record to DNS, and POST /api/verify/{domainId}.
  4. Free: POST /api/key/{domainId}. Paid/Admin: POST /api/smtp and save data.apiKey.
  5. PATCH /api/settings with {"developerMode":true}. Send to POST /api/send/{{apiKey}} without Origin or Referer headers. Disable Developer Mode before browser use.
  6. Test SMTP with POST /api/smtp/{smtpId}/test or Admin POST /api/admin/test and payload {"testConnection":true}.

Common errors and operational limitations

CodeMeaning / next step
AUTH_REQUIRED / SESSION_INVALID / SESSION_EXPIREDMissing or invalid Function session; sign in again and use the returned opaque token.
ADMIN_REQUIREDRoute requires a currently active user with Firestore role admin.
DOMAIN_ALREADY_CLAIMEDThat hostname was already registered; a domain claim is globally unique.
DNS_VERIFICATION_PENDINGTXT record absent or not yet visible; confirm name/value and retry.
SMTP_NOT_AVAILABLE_ON_FREE / SMTP_LIMIT_REACHEDFree cannot add SMTP; Paid has at most five SMTP accounts; conditional server-side reservations prevent concurrent creates from bypassing the limit.
DOMAIN_NOT_VERIFIED / SMTP_OR_DOMAIN_UNAVAILABLEVerify domain or reactivate the linked configuration.
DEVELOPER_MODE_TEST_ONLY / ORIGIN_REQUIRED / ORIGIN_NOT_ALLOWEDApply the mode/origin rules in the policy section.
SMTP_CONNECTION_FAILEDCheck host, port, TLS, credentials and provider permissions. Runtime supports AUTH LOGIN.
SMTP_SEND_FAILEDThe SMTP service did not accept the message; inspect provider-side rejection logs.
CRYPTO_CONFIGURATION_ERROR / ENCRYPTED_CREDENTIAL_INVALIDCheck the single encryption key. Do not rotate it without re-encrypting saved SMTP credentials.

Delivery note: SMTP acceptance means the SMTP server accepted the message for submission; it does not guarantee inbox delivery. Cloudflare Workers blocks outbound TCP port 25; this project supports port 465 implicit TLS and 587/2525 with mandatory STARTTLS. The transport uses SMTP AUTH LOGIN; providers requiring other mechanisms need an adapter change and test. Configure Cloudflare rate-limit/WAF protections before production use.

No endpoint matches that search. Clear the filter or try a shorter term.