Standalone email application service. Validates, composes, sends, and tracks job application emails via a pluggable registry adapter.
Mail-Bridge handles the full lifecycle of sending a job application email in a single API call:
- Validates the request — checks the resume variant is approved, the job is open and has a contact email, the user hasn't already applied, and rate limits are respected
- Composes a professional HTML email from a Handlebars template using job and user data
- Attaches the resume PDF via a signed URL fetched from the registry
- Sends the email through SendGrid
- Records the application back to the registry for tracking
All data access goes through a RegistryAdapter interface, so Mail-Bridge has no direct database dependency and can be wired to any backend.
- Node.js 18+
- A SendGrid API key
- A registry service (or use
REGISTRY_TYPE=memoryfor local testing without one)
git clone <repo-url> && cd mail-bridge
npm install
cp .env.example .envOpen .env and fill in at minimum:
SENDGRID_API_KEY=SG.your-key-here
REGISTRY_URL=http://your-registry-service/api
REGISTRY_API_KEY=your-registry-key
To run locally without a registry service, set REGISTRY_TYPE=memory instead — data lives in memory and resets on restart.
npm run dev # development (ts-node)
npm run build # compile TypeScript
npm start # run compiled outputcurl http://localhost:3000/health
# → { "status": "ok", "version": "0.1.0", ... }
curl http://localhost:3000/health/ready
# → { "status": "ready", "registry": "ok" } (200 if registry reachable, 503 if not)Mail-Bridge provides two endpoints for sending applications:
POST /api/mail/send — Accepts all data in the request payload. No registry lookups for variant/job data.
curl -X POST http://localhost:3000/api/mail/send \
-H "Content-Type: application/json" \
-d '{
"to_email": "hr@company.com",
"template": "standard",
"context": {
"user": {
"name": "John Doe",
"email": "john@example.com",
"phone": "9876543210",
"summary": "3 years experience in Python and FastAPI"
},
"job": {
"title": "Python Developer",
"company_name": "Acme Corp",
"location": "Bangalore"
}
},
"attachments": [
{
"filename": "resume.pdf",
"signed_url": "https://s3.example.com/resumes/user-123/resume.pdf?expires=900"
}
]
}'Success response:
{ "success": true, "message_id": "sendgrid-msg-id", "sent_at": "2026-05-07T10:00:00Z" }Error codes: INVALID_PAYLOAD · INVALID_EMAIL · TEMPLATE_NOT_FOUND · NO_ATTACHMENTS · ATTACHMENT_DOWNLOAD_FAILED · SEND_FAILED
POST /api/applications/send — Requires variant/job IDs. Fetches data from registry and enforces 5 validation gates.
curl -X POST http://localhost:3000/api/applications/send \
-H "Content-Type: application/json" \
-d '{"variant_id":"var-123","job_id":"job-456","user_id":"user-789"}'On success you get back the message ID and sent timestamp:
{ "success": true, "message_id": "...", "sent_at": "..." }If a validation check fails, no email is sent and you get a specific error code:
{ "success": false, "error": { "code": "VARIANT_NOT_APPROVED", "message": "..." } }Error Codes: VARIANT_NOT_FOUND · VARIANT_NOT_APPROVED · JOB_NOT_FOUND · JOB_CLOSED · JOB_NO_EMAIL · DUPLICATE_APPLICATION · DAILY_LIMIT_EXCEEDED · RATE_LIMIT_TOO_FAST · INVALID_REQUEST · INTERNAL_ERROR
Rate limit errors return HTTP 429; all others return 400.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Server port |
NODE_ENV |
development |
development / production |
REGISTRY_TYPE |
rest |
rest (HTTP) or memory (in-process, no persistence) |
REGISTRY_URL |
— | Registry service base URL |
REGISTRY_API_KEY |
— | Registry Bearer token |
SENDGRID_API_KEY |
— | SendGrid API key |
SENDER_EMAIL |
noreply@example.com |
From address on all emails |
SENDER_NAME |
Mail-Bridge |
From display name |
MAX_APPLICATIONS_PER_DAY |
10 |
Max emails a user can send per day |
MIN_SECONDS_BETWEEN_SENDS |
30 |
Minimum seconds between consecutive sends |
LOG_LEVEL |
info |
debug / info / warn / error |
Build and run the image:
docker build -t mail-bridge:latest .
docker run -p 3000:3000 \
-e SENDGRID_API_KEY=SG.your-key \
-e REGISTRY_TYPE=memory \
mail-bridge:latestOr use Docker Compose for a one-command start:
docker-compose up -d
docker-compose logs -f mail-bridgeMail-Bridge is registry-agnostic. To connect it to a different data source, implement the RegistryAdapter interface in src/adapters/ and register it in src/server.ts. The interface requires methods to fetch variants, jobs, and signed URLs, check for duplicates, enforce rate limits, and save application records.