Browse the docs
Build
Send transactional email from handlers with ctx.email.send. Where it works, delivery, transactions, limits, and local development.
Apps can send email: an approval request, a "your document was signed" note, a daily digest. fuast handles delivery. You do not need an email provider or API key.
await ctx.email.send({
to: "ana@acme.com",
subject: "Approval needed: New monitor",
text: "Bo asked for a new monitor ($349).\n\nOpen the app to decide: https://approvals.fuast.com/"
});ctx.email.send(message):
| Field | Type | Notes |
|---|---|---|
to |
string |
One address. Loop for several recipients. |
subject |
string |
Required. |
text |
string |
Plain-text body. |
html |
string |
HTML body. Send text as well for clients that prefer it. |
At least one of text and html is required. The promise resolves to { id } once fuast has accepted the message for delivery, and rejects if the message is invalid or the app is over its daily email limit.
Mail comes from fuast's sending address with the app's name as the sender name. Replies go to the app owner.
Where it works
ctx.email.send works in mutations, actions, endpoints, and schedules. It does not work in queries, which must not have side effects, or in onGuestUpgrade.
It does not work in an unclaimed preview either. An app deployed without an account cannot send mail until a person claims it: the call fails with Claim this app to use email: ctx.email.send is off in an unclaimed preview. (code unclaimed). After the claim the app is restricted and email stays off (code restricted) until its owner trusts it with the workspace.
Email and transactions
An email is sent when you call send. A database rollback does not unsend it. If a mutation sends an email and then throws, the email still goes out and the rows do not change. Send email as the last step, after every check that can fail:
decide: mutation(async (ctx, id: string, approve: boolean) => {
const me = ctx.auth.requireSignedIn();
const request = await ctx.db.requests.get(id);
if (!request || request.status !== "pending") throw new Error("This request was already decided.");
await ctx.db.requests.update(id, { status: approve ? "approved" : "rejected", deciderId: me.userId });
if (request.submitterEmail) {
await ctx.email.send({
to: request.submitterEmail,
subject: `${request.title} was ${approve ? "approved" : "rejected"}`,
text: `${me.displayName} decided your request.`
});
}
})Who to email
Use addresses you got from a trusted place:
ctx.auth.emailfor the caller, whenemailVerifiedis true.ctx.workspace.members()for coworkers.- A stored email from a row that a signed-in user wrote about themselves.
Do not email arbitrary addresses typed into a form by a guest on a public app. That turns your app into a spam relay, and it uses your daily limit.
Limits
Each app can send 100 emails per UTC day by default. Over the limit, send throws an error with code fuast_quota_exceeded and bucket: "email". Batch notifications into a digest (see schedules) instead of one email per event.
Local development
fuast dev does not deliver email. Each message is written to the app log with its recipient, subject, and body, so you can check it:
fuast logs --port 3000