Skip to content

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.

  1. In-App Persistence First: Every notification method in email.service.js attempts to persist an in-app Notification record 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.
  2. 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.
  3. 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.
  4. Missing Key Handling: If BREVO_API_KEY is omitted in development or test environments, the Brevo client gracefully initializes as null. 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_KEY is 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 via src/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 submitReportSchema in src/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:
    1. The controller resolves the reporting user's identity (req.user.name, req.user.email).
    2. Formats a structured diagnostic email with the user's name, email address, problem page, URL, blocking severity, timestamp, and sanitized description.
    3. Dispatches the email directly to toodle.issues@gmail.com.
    4. The issue is triaged by the team on the bug tracker and linked to resolving commits and tests.

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 PENDING status.
  • 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.com with 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.