External API Integration — Brevo Transactional Email¶
Verified against the backend implementation on 15 September 2026. Code implemented in toodle-api/src/services/email.service.js and toodle-api/src/config/brevo.js; unit test suite verified with 15 tests passing.
Overview & Selection Motivation¶
Toodle integrates Brevo (formerly Sendinblue) as its primary external API provider. In an academic institution, tutors, course organisers, and student applicants do not remain actively signed into the web application at all times. Transactional email provides the critical asynchronous notification channel required for real-time academic workflows:
- alerting tutors when they are officially allocated to a course;
- notifying organisers when weekly timesheets or tutor applications are submitted;
- informing tutors immediately when timesheets are approved or disputed;
- alerting tutors of excusal request outcomes;
- broadcasting emergency overflow teaching opportunities to qualified student volunteers; and
- delivering user-submitted in-app problem reports directly to the development team's issue inbox.
Why Brevo?¶
The team evaluated several external email and communication APIs against project requirements:
| Provider | Evaluation | Decision |
|---|---|---|
| Brevo | Modern REST API, official @getbrevo/brevo Node.js SDK, generous free tier (300 emails/day), immediate API key provisioning without requiring domain ownership or DNS TXT/SPF records for initial development. |
Selected — ideal for rapid integration, reliable delivery, and academic prototype evaluation. |
| SendGrid | Complex account verification process and strict domain identity requirements that frequently flag university student project accounts. | Rejected due to high account suspension risk during grading. |
| AWS SES | Requires sandbox exit approval and AWS IAM setup, introducing unnecessary cloud infrastructure complexity. | Rejected in favor of an independent, dedicated API provider. |
| Resend | Evaluated during early Sprint 2 planning; initial domain verification restrictions led the team to standardize on Brevo for reliable multi-recipient testing. | Superseded by Brevo. |
Architectural Blueprint & Sequence Flow¶
Toodle uses a dual-channel notification architecture with fire-and-forget asynchronous execution:
sequenceDiagram
autonumber
actor User as Client / Tutor / Organiser
participant API as Express API Service
participant DB as Supabase PostgreSQL
participant EmailService as email.service.js
participant Brevo as Brevo External API
User->>API: Submit business action (e.g. approveExcusal)
activate API
API->>DB: Execute business transaction (prisma.$transaction)
DB-->>API: Transaction committed successfully
API->>EmailService: Trigger notification (e.g. sendExcusalReviewedNotification)
activate EmailService
EmailService->>DB: Create persistent in-app Notification record
DB-->>EmailService: Notification stored
EmailService-)Brevo: Async HTTPS POST /v3/smtp/email (fire-and-forget)
deactivate EmailService
API-->>User: 200 OK (Immediate response, zero network latency penalty)
deactivate API
Note over EmailService,Brevo: Asynchronous delivery in background
Brevo-->>User: Delivers transactional email to recipient's inbox
Non-Blocking Resilience & Fault Isolation¶
A core architectural principle of Toodle is graceful degradation: failures in external third-party services must never crash internal operations or roll back database state.
- In-App Persistence First: Every notification method in
email.service.jsattempts to persist an in-appNotificationrecord in PostgreSQL before contacting the external email provider. Even if external email delivery fails completely, users will still see their unread notifications when logging into the web interface. - Fire-and-Forget Async Execution: Service functions dispatch emails without awaiting network responses (or using
.catch(() => {})wrappers). The HTTP request to the Express API completes in milliseconds without waiting for Brevo's round-trip network latency. - Silent Error Logging: If the Brevo API is unreachable, down, or encounters rate limits, the error is caught, formatted, and logged using
logger.error(). It is never allowed to bubble up to the Express global error handler or abort user transactions. - Missing Key Handling: If
BREVO_API_KEYis omitted in development or test environments, the Brevo client gracefully initializes asnull. Outgoing email operations log a warning and return cleanly without throwing exceptions.
// Fire-and-forget error resilience implementation in src/services/email.service.js
const sendEmail = async ({ to, subject, html }) => {
if (!brevoClient) {
logger.warn("Brevo not configured — skipping email send");
return;
}
try {
const result = await brevoClient.transactionalEmails.sendTransacEmail({
sender: { name: fromName, email: fromEmail },
to: [{ email: to }],
subject,
htmlContent: html,
});
logger.info(`Email sent to ${to}: ${result?.messageId || "OK"}`);
} catch (err) {
// Log but don't throw — business operations must still succeed
logger.error(`Failed to send email to ${to}:`, err.message);
}
};
Security, Secrets & Sanitization¶
- Server-Side Key Isolation: The
BREVO_API_KEYis strictly confined to the backend environment (.env). It is never exposed to the React frontend client or bundled in public assets. - Environment Configuration: Key names are documented in
.env.example(BREVO_API_KEY,BREVO_FROM_EMAIL,ISSUES_EMAIL) and loaded viasrc/config/index.js. - HTML Injection Defense: User-supplied input (such as course application motivation letters, timesheet dispute reasons, or problem report descriptions) could be exploited for HTML/CSS injection in email clients. Toodle routes all dynamic strings through an
escapeHtml()sanitizer that replaces&,<,>,", and'with their respective character entity equivalents before embedding into email bodies.
System Notification & Trigger Matrix¶
The backend wires transactional emails into 9 core system events across 5 domain services:
| Event | Triggering Service / Function | Recipient(s) | Email Subject | In-App Notification Type |
|---|---|---|---|---|
| Allocation Assigned | allocation.service.js (createAllocation) |
Allocated Tutor | You have been allocated to <CourseCode> |
ALLOCATION_ASSIGNED |
| Timesheet Submitted | timesheet.service.js (submitTimesheet) |
All Organisers | Timesheet submitted by <TutorName> |
TIMESHEET_SUBMITTED |
| Timesheet Approved | timesheet.service.js (approveTimesheet) |
Tutor | Your timesheet for <Date> was approved |
TIMESHEET_APPROVED |
| Timesheet Disputed | timesheet.service.js (disputeTimesheet) |
Tutor | Your timesheet for <Date> was disputed |
TIMESHEET_DISPUTED |
| Excusal Resolved | excusal.service.js (approveExcusal, declineExcusal) |
Tutor | Your excusal for <CourseCode> on <Date> was approved/declined |
EXCUSAL_RESOLVED |
| Overflow Posted | overflow.service.js (createOverflowPost) |
All active Students | New volunteer opportunity: <CourseCode> |
OVERFLOW_POSTED |
| Session Swap Approved | swap.service.js (approveSwap) |
Both Tutors | Your session swap for <CourseCode> has been approved |
SWAP_RESOLVED |
| Course Application Submitted | course-application.service.js (apply) |
All Organisers | New tutor application for <CourseCode> |
SYSTEM (Course) |
| Course Application Reviewed | course-application.service.js (review) |
Applicant | Your application for <CourseCode> was approved/rejected |
ALLOCATION_ASSIGNED / SYSTEM |
| Problem Report Submitted | report.service.js (submitReport) |
Issues Inbox (toodle.issues@gmail.com) |
[Toodle] Problem report: <Page> (blocking) |
N/A (Direct email to developers) |
In-App Problem Reporting Integration¶
To support formal user testing, continuous feedback collection, and rubric-grade bug tracking, Toodle integrates an in-app problem reporting pipeline directly through the external email service.
Endpoint: POST /api/v1/problem-reports¶
- Access: Any authenticated user (Student, Tutor, Organiser).
- Validation: Enforced via
submitReportSchemainsrc/validators/report.validator.js. - Payload:
{ "page": "Allocation Board", "description": "Dragging a tutor card on mobile Safari does not trigger drop target highlighting.", "blocking": true, "url": "/allocations" } - Execution Flow:
- The controller resolves the reporting user's identity (
req.user.name,req.user.email). - Formats a structured diagnostic email with the user's name, email address, problem page, URL, blocking severity, timestamp, and sanitized description.
- Dispatches the email directly to
toodle.issues@gmail.com. - The issue is triaged by the team on the bug tracker and linked to resolving commits and tests.
- The controller resolves the reporting user's identity (
Automated Testing & Mocking Strategy¶
The external API integration is covered by automated unit tests in toodle-api/tests/unit/services/email.service.test.js.
To prevent test runs from exhausting the daily Brevo email quota or failing when offline, tests mock the @getbrevo/brevo module using Vitest:
vi.mock("../src/config/brevo.js", () => ({
default: {
transactionalEmails: {
sendTransacEmail: vi
.fn()
.mockResolvedValue({ messageId: "mock-id" }),
},
},
}));
Verified Test Cases (15/15 Passed):¶
- Verifies correct recipient email, subject line, and HTML template generation for active allocations.
- Verifies special advisory notes rendered when allocations are created in
PENDINGstatus. - Verifies broadcast loops send individual personalized emails to all course organisers.
- Verifies timesheet dispute reasons are cleanly formatted in email bodies.
- Verifies excusal resolution emails correctly include course code, date, and status.
- Verifies overflow opportunity broadcasts to all students.
- Verifies session swap approval notifications are sent to both participating tutors.
- Verifies course application submission and approval/rejection notifications.
- Verifies problem report emails are delivered to
toodle.issues@gmail.comwith blocking indicators. - Verifies HTML entities (
<script>,"quotes",&) are sanitized to prevent injection attacks. - Verifies error resilience: service does not throw when database connection fails or when Brevo returns a
503 Service Unavailable.