Send email from Firebase without exposing a mail API key by using a Firestore-triggered Cloud Function as the server-side bridge to Volanea. This guide shows the event model, message document, REST request, secret handling, testing approach, and failure modes that matter in production.
What Firebase integration means in this setup
Firebase does not provide a native Volanea marketplace app or a no-code Volanea connector. The direct integration is a server-side Firebase Cloud Function: a document is created in Cloud Firestore, Firebase invokes the function, and the function makes an authenticated HTTPS request to Volanea’s email API.
That distinction is important. A browser app, iOS app, or Android app must never call an email provider with a long-lived API key. If it did, anyone who could inspect the application bundle, browser network traffic, or client-side configuration could reuse the key to send mail from your domain.
The practical flow is:
- Your trusted backend creates a document in an
outboundMailFirestore collection. - A Cloud Firestore document-created event invokes a second-generation Firebase Cloud Function.
- The function reads the new document, validates the fields, and loads the Volanea API key from Firebase’s secret configuration.
- The function sends the mapped message to Volanea over HTTPS.
- The function records delivery-attempt metadata in Firestore for support and debugging.
This is an event-driven outbound email pattern rather than a request-response email endpoint. It is especially useful for receipts, passwordless sign-in links generated by your own system, account alerts, team invitations, onboarding messages, and workflow notifications.
Firebase also has an official Trigger Email extension for creating email from Firestore documents through SMTP. That is a different implementation path, and it is not a native Volanea integration. Because Volanea supports both SMTP and REST sending, the REST route below is preferable when you want application-controlled request handling, structured errors, and a clear boundary around API credentials.
The architecture before you write code
A small amount of design work avoids common mistakes, particularly accidental duplicate sends and a Firestore collection that untrusted users can abuse.
Recommended collections
Use a write-only queue collection for the event that starts the email:
outboundMail/{mailId}
A new document in that collection is the concrete Firebase trigger. A typical document contains the recipient address and the rendered or renderable message content:
{
"to": "maya@example.com",
"toName": "Maya Chen",
"subject": "Your order is confirmed",
"html": "<h1>Thanks for your order</h1><p>Your order number is A-1042.</p>",
"text": "Thanks for your order. Your order number is A-1042.",
"orderId": "A-1042",
"createdAt": "server timestamp"
}
Keep a separate collection for attempt records if you need auditability:
emailAttempts/{firebaseEventId}
This separation matters because updates to an outboundMail document should not themselves be treated as a fresh request to send an email. A function using onDocumentCreated only responds to creation, not later updates, which makes it a good fit for an immutable queue item.
Decide who may create queue documents
Do not let every signed-in client directly create arbitrary documents under outboundMail. That turns your email infrastructure into a potential spam relay: a malicious user could pick any recipient, subject, and HTML.
For customer-facing actions, have the client call a callable function or your own authenticated backend route. That trusted code should check authorization, rate limits, product state, and recipient eligibility before it creates the queue document. A Firestore security rule can deny direct client writes to outboundMail, while Admin SDK code in Cloud Functions can still write there.
For internal systems, a trusted server can create the queue document through the Firebase Admin SDK. The central principle is the same: the event is safe only when the writer is trusted.
Verify the sending identity first
Before deploying the function, add and authenticate the sending domain in Volanea. Your from address should use that authenticated domain, such as notifications@updates.example.com, rather than a personal mailbox or a recipient-supplied address.
Set a stable sender name and address in server-side configuration. Do not allow callers to freely supply from; otherwise one product feature can accidentally impersonate another department, or an attacker can try to use your account for deceptive mail. Review the email API reference and setup guides for the sending request, domain setup, and current account requirements before moving the example into production.
The Firebase event payload you receive
Cloud Firestore triggers in Firebase Functions are CloudEvents. For a document-created trigger, the handler receives an event with CloudEvent metadata such as an event ID, source, type, and timestamp. In the Firebase Functions Node.js SDK, event.data is exposed as a Firestore QueryDocumentSnapshot, not as a raw HTTP webhook body.
Conceptually, the underlying document-created event has CloudEvent metadata in this shape:
{
"specversion": "1.0",
"id": "firebase-event-id",
"source": "//firestore.googleapis.com/projects/PROJECT_ID/databases/(default)",
"type": "google.cloud.firestore.document.v1.created",
"time": "2026-10-04T12:34:56.000Z",
"data": {
"value": {
"name": "projects/PROJECT_ID/databases/(default)/documents/outboundMail/mail_123",
"fields": {
"to": { "stringValue": "maya@example.com" },
"subject": { "stringValue": "Your order is confirmed" }
}
}
}
}
Inside a Firebase Functions handler, you normally do not parse those Firestore wire-format values. Instead, you use the snapshot API:
const snapshot = event.data;
const message = snapshot.data();
const mailId = snapshot.id;
const firebaseEventId = event.id;
For the example queue document, message is a normal JavaScript object with message.to, message.subject, message.html, and message.text. That SDK conversion is why the function code below is simpler than the raw event representation.
The event ID is valuable operationally. Store it with the attempt record, include it in logs, and use it when investigating whether a Firebase retry corresponds to an earlier attempt. It is not the same thing as a provider message ID.
Create the Firestore trigger and map it to Volanea
Install the Firebase Functions and Admin SDK packages in the directory containing your Cloud Functions source. The code below uses the second-generation Firestore trigger API and Node’s built-in fetch capability.
It expects a secret named VOLANEA_API_KEY, discussed in the next section. It also assumes your Volanea account uses the POST https://api.volanea.com/v1/email REST endpoint with a Bearer API token and a JSON email payload.
import { initializeApp } from "firebase-admin/app";
import { getFirestore, FieldValue } from "firebase-admin/firestore";
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { defineSecret } from "firebase-functions/params";
import { logger } from "firebase-functions";
initializeApp();
const db = getFirestore();
const volaneaApiKey = defineSecret("VOLANEA_API_KEY");
export const sendQueuedEmail = onDocumentCreated(
{
document: "outboundMail/{mailId}",
region: "us-central1",
secrets: [volaneaApiKey]
},
async (event) => {
const snapshot = event.data;
if (!snapshot) {
logger.warn("No Firestore document was included in the event", {
eventId: event.id
});
return;
}
const message = snapshot.data();
const mailId = snapshot.id;
const attemptRef = db.collection("emailAttempts").doc(event.id);
// Required application fields. Validate before contacting the provider.
if (
typeof message.to !== "string" ||
typeof message.subject !== "string" ||
typeof message.html !== "string" ||
typeof message.text !== "string"
) {
await attemptRef.set({
mailId,
status: "rejected",
reason: "Required fields are missing or have the wrong type",
createdAt: FieldValue.serverTimestamp()
});
logger.error("Rejected malformed outboundMail document", {
eventId: event.id,
mailId
});
return;
}
// A transaction prevents two concurrent deliveries of the same Firebase
// event from both proceeding during ordinary retry/concurrency scenarios.
const claimed = await db.runTransaction(async (transaction) => {
const existing = await transaction.get(attemptRef);
if (existing.exists) return false;
transaction.set(attemptRef, {
mailId,
status: "sending",
to: message.to,
subject: message.subject,
createdAt: FieldValue.serverTimestamp()
});
return true;
});
if (!claimed) {
logger.info("Firebase event was already claimed", {
eventId: event.id,
mailId
});
return;
}
// Firebase document fields -> Volanea REST email fields.
const volaneaPayload = {
from: {
email: "notifications@updates.example.com",
name: "Example App"
},
to: [
{
email: message.to,
...(typeof message.toName === "string" ? { name: message.toName } : {})
}
],
subject: message.subject,
html: message.html,
text: message.text
};
try {
const response = await fetch("https://api.volanea.com/v1/email", {
method: "POST",
headers: {
"Authorization": `Bearer ${volaneaApiKey.value()}`,
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(volaneaPayload)
});
const responseBody = await response.text();
if (!response.ok) {
throw new Error(
`Volanea returned HTTP ${response.status}: ${responseBody}`
);
}
await attemptRef.update({
status: "accepted",
acceptedAt: FieldValue.serverTimestamp()
});
logger.info("Volanea accepted email request", {
eventId: event.id,
mailId,
status: response.status
});
} catch (error) {
await attemptRef.update({
status: "failed",
error: error instanceof Error ? error.message : String(error),
failedAt: FieldValue.serverTimestamp()
});
// Throw so Firebase marks the invocation as failed. Whether it retries
// depends on the retry configuration you choose for the function/event.
throw error;
}
}
);
Field mapping in plain language
The important mapping is explicit rather than inferred:
| Firestore document field | Volanea request field | Purpose |
|---|---|---|
| fixed server-side value | from.email and from.name | The authenticated sender identity |
to | to[0].email | Recipient address |
toName | to[0].name | Optional recipient display name |
subject | subject | Email subject line |
html | html | HTML body |
text | text | Plain-text alternative |
Keep the sender value out of the queue document unless you have a tightly controlled, server-side allowlist. Likewise, treat HTML as trusted only if it is generated by your code or sanitized before being stored. A customer support note or profile field is not automatically safe to interpolate into HTML.
Store the Volanea key as a Firebase secret
The Volanea API key belongs in Firebase’s server-side secret configuration, not in a web app environment variable, a mobile app configuration file, Firestore, Remote Config, or source control.
For Firebase Functions, set the secret through the Firebase CLI:
firebase functions:secrets:set VOLANEA_API_KEY
When prompted, paste the API key created in Volanea. The defineSecret("VOLANEA_API_KEY") declaration and the secrets: [volaneaApiKey] function option grant that deployed function access to the secret. At runtime, volaneaApiKey.value() reads it only inside the server-side function.
Deploy the function after setting the secret:
firebase deploy --only functions:sendQueuedEmail
Secret access and Cloud Functions deployment have billing and project-configuration implications. Confirm that the Firebase project is on a plan and configuration that supports the Cloud Functions and secret features you are using. Do not work around those requirements by copying the key into public configuration.
Why client-visible configuration is unsafe
Values prefixed for exposure in front-end build tooling, Firebase web configuration, Android resources, iOS bundles, and browser JavaScript should be assumed public. Firebase API keys used to identify a Firebase project are not equivalent to a private Volanea sending key; the latter authorizes mail activity and must be protected accordingly.
If a sending key is exposed, revoke it in Volanea, create a replacement, update the Firebase secret, and redeploy or otherwise ensure the active function revision receives the new secret. Then review Volanea activity and your function logs for unexpected sending.
Create queue documents safely
A trusted backend can enqueue a message with the Admin SDK. This is deliberately separate from the sending function, so your product logic decides when an email is allowed.
import { getFirestore, FieldValue } from "firebase-admin/firestore";
const db = getFirestore();
await db.collection("outboundMail").add({
to: "maya@example.com",
toName: "Maya Chen",
subject: "Your order is confirmed",
html: "<h1>Thank you</h1><p>Your order number is A-1042.</p>",
text: "Thank you. Your order number is A-1042.",
orderId: "A-1042",
createdAt: FieldValue.serverTimestamp()
});
In a real application, render templates on the server from a known template and validated data. For example, use an order ID from a completed payment record rather than accepting a subject line and HTML body directly from a browser request.
For bulk sends, do not create an unbounded number of queue documents at once without considering provider limits, function concurrency, recipient consent, and your own product’s rate limits. Transactional messages should remain tied to an event a recipient expects. Campaign mail typically needs additional audience, unsubscribe, consent, and scheduling controls.
Test the full delivery path
A successful deployment is not yet a successful email integration. Test each boundary in the chain: the queue writer, Firestore trigger, function secret access, Volanea API request, authenticated sender domain, and final inbox placement.
Use a dedicated test recipient you control and create one queue document with a visible unique marker, such as Firebase test 2026-10-04 001, in the subject. Then inspect the function logs and the emailAttempts document created for that event.
A practical test checklist is:
- Confirm the Firestore document is created under the exact
outboundMail/{mailId}path. - Confirm the function logs include the same Firebase event ID and mail ID.
- Confirm an
emailAttempts/{event.id}record changes fromsendingtoaccepted. - Confirm Volanea accepted the request and that the message appears in its sending activity.
- Confirm the message arrives and has the expected sender, subject, HTML rendering, and plain-text alternative.
- Inspect spam placement and authentication headers before treating the setup as production-ready.
Do not log the API key, full HTML body, password-reset URL, or personally sensitive content. Logs are useful for correlation, but they should not become an uncontrolled copy of private email content.
When this breaks: Firebase-to-email failure modes
The hop between Firestore and Volanea has specific failure modes. Designing for them is more useful than assuming an HTTP 200 means the complete workflow is perfect.
Firebase retries can cause duplicate sends
Firestore-triggered functions are delivered at least once. A transient failure, timeout, infrastructure interruption, or failure after the provider receives the request can result in another invocation for the same logical event.
The emailAttempts/{event.id} transaction in the example prevents ordinary concurrent invocations of the identical Firebase event from both initiating a send. It is a helpful guard, but it does not magically create end-to-end exactly-once delivery. The hardest case is an ambiguous outcome: Volanea receives the email request, but the function crashes or loses its network response before it records accepted.
Use business-level idempotency as well. For an order receipt, make the queue document ID deterministic, such as receipt_A-1042, and only create it once when the order enters a paid state. For a passwordless sign-in email, use an explicit request ID and a short-lived token policy. If your Volanea API configuration supports an idempotency mechanism, use a stable application request identifier according to its documented syntax; do not assume that a custom header is honored unless the API documentation says it is.
A webhook-style timeout is not proof that mail failed
In this architecture, Firebase Functions is the HTTPS client and Volanea is the API server; there is no Firebase dashboard webhook configuration acting as the sender. Still, an outbound HTTPS call can time out after the remote service has accepted the request.
Treat timeouts as uncertain, not automatically as a signal to send another independent message. Capture the Firebase event ID, mail ID, request timestamp, and any provider response identifier available in the documented response. Those records give support staff something concrete to compare with Volanea activity before manually replaying a message.
Set a reasonable function timeout for your workload, but keep the email send fast. Avoid doing slow unrelated work—large database scans, image processing, or third-party enrichment—in the same invocation that sends transactional mail.
Fields may be missing, malformed, or unavailable to the trigger
Firestore documents are schemaless. A field can be absent because an older app version did not write it, a backend branch created a partial record, or a form submission path used a different schema. The example explicitly rejects documents without string values for to, subject, html, and text rather than sending an incomplete payload.
Some Firebase capabilities, deployment options, or Google Cloud event configurations may also depend on the Firebase project plan and enabled services. Check the current Firebase documentation for requirements before basing a critical mail path on a feature that is unavailable in your project. If the Cloud Function cannot deploy or cannot access its configured secret, no Volanea request will be made.
Provider acceptance is not inbox delivery
An accepted API request means Volanea accepted the message for processing; it does not guarantee a recipient mailbox will place it in the inbox. Bounces, suppression rules, recipient mailbox filtering, DMARC alignment, message content, and sender reputation all affect the eventual outcome.
Use a properly authenticated domain, send a useful plain-text alternative, avoid misleading sender identities, and build bounce or suppression handling into your customer data process. Never keep sending transactional-looking messages to addresses that have permanently bounced or unsubscribed from the relevant category.
Reliability and deliverability practices
The integration code is only one layer of email reliability. The next layer is making the message expected, identifiable, and easy for recipient systems to authenticate.
Use separate sender addresses or subdomains when the product has materially different streams, such as receipts, account security, and marketing. This does not eliminate the need for good sending practices, but it makes operational ownership clearer and can limit the blast radius of a poorly performing stream.
Render both HTML and text. HTML can carry branding and layout; text remains useful for accessibility, clients that block HTML, and readers who prefer simple mail. Keep the two versions semantically aligned so a recipient sees the same essential order number, action, deadline, or security information in either format.
Add observability around the email event, not just the function exception. At minimum, retain:
- the Firestore queue document ID;
- the Firebase CloudEvent ID;
- the internal business event ID, such as an order or invitation ID;
- the attempt state and timestamps;
- a non-sensitive provider request or message identifier when available;
- a categorized failure reason without secret or message-body leakage.
This data makes it possible to answer the support question that otherwise takes hours: “Was this email requested, did our function attempt it, did the provider accept it, and did we accidentally create another request?”
Alternatives and when to use them
A Firestore trigger is a strong choice when a document creation is already the authoritative business event. It provides an audit trail in the database and decouples the product action from the API call.
For an action that must immediately report an outcome to a user—such as an admin clicking “send test email”—an HTTPS callable function or an authenticated HTTP endpoint may be more appropriate. That endpoint can validate the caller and send through Volanea in the same server-side request. The API key still remains in a Firebase secret, never in the client.
For a non-developer workflow, an automation product can sit between a source system and an email provider, but it adds another credential store, another retry policy, and another payload transformation. Use it only when the source system cannot run trusted server-side code or when business users genuinely need to manage the workflow. Firebase Cloud Functions is usually the cleaner direct path for a Firebase application.
For a high-volume mail pipeline, consider a queue and worker design with explicit throughput controls rather than allowing every Firestore event to immediately produce a network call. That gives you clearer backpressure behavior during an outage and makes batch operations easier to manage.
Conclusion
To send email from Firebase with Volanea, use a Firestore document-created event to invoke a server-side Cloud Function, map the trusted queue document to the Volanea REST email payload, and load the API key through Firebase secret configuration. The key architectural decisions are to protect who can create queue records, validate every field, and plan for at-least-once event delivery.
Start with one transactional use case, one authenticated sender domain, and a test recipient. Once the logs, attempt records, and inbox results agree, expand the pattern with deterministic business IDs, template rendering, rate limits, and the operational controls appropriate to your message stream.
FAQ
Can a Firebase web app call Volanea directly?
No. A browser call would expose the Volanea API key or require an unsafe proxy. Call Volanea from a Cloud Function or another trusted backend, and keep the key in Firebase secret configuration.
What Firebase event starts the email send?
In this guide, a Cloud Firestore document-created event starts the send. Creating outboundMail/{mailId} invokes an onDocumentCreated Cloud Function.
Does creating a Firestore document guarantee exactly one email?
No. Event-driven functions are at-least-once, so retries are possible. Use deterministic business records, event-attempt tracking, and any documented provider idempotency facility to reduce duplicate-send risk.
Can I use Firebase’s Trigger Email extension instead?
The Trigger Email extension is an SMTP-based Firestore email option, not a native Volanea connector. A Cloud Function calling Volanea’s REST API provides a direct, code-controlled integration.
Where should the Volanea API key be stored?
Store it as a Firebase Functions secret, such as VOLANEA_API_KEY, and attach that secret only to functions that need it. Never place it in client-visible Firebase configuration, Firestore documents, or source code.