@chattee.ai/email
v1.1.1
Published
Transactional email client for applications built on Chattee.
Downloads
669
Maintainers
Readme
@chattee.ai/email
Transactional email for applications built on Chattee.
import { email } from "@chattee.ai/email";
await email.send({
to: user.email,
subject: "Welcome aboard",
html: `<p>Hi ${user.name}, thanks for signing up.</p>`,
});That is the whole API. No SMTP settings, no provider SDK, no API key in your code.
Two things this deliberately does not let you do
You never handle the provider's API key. The project owner connects their own Brevo, Mailchimp
or SES account in the Chattee Data tab. That key stays on the platform — it is never placed in
your app's environment, so it cannot leak through a log line, an error page, or a dependency that
reads process.env. Your app asks Chattee to send; Chattee makes the provider call.
The useful consequence: switching providers needs no code change. Your send() calls are
identical whether the owner is on Brevo today or SES next month.
You never choose the sender. There is no from option. The sender address is configured once by
the owner alongside their credential, because a provider account with a verified domain can send as
any address at that domain — so an app able to pick its own from could send as the CEO. Your app
decides who receives a message and what it says; the owner decides who it comes from.
Server-side only
This module authenticates with a secret held in your backend's environment. Never import it into browser code — bundling it would ship that secret to every visitor, and anyone who found it could send mail from your domain.
There is no browser counterpart by design. Sending email is a decision, and decisions belong on your server, where you can check who is asking.
Setup
Nothing to configure. When the project has the email resource, the platform sets
CHATTEE_API_URL and CHATTEE_PROJECT_TOKEN in your backend container, and the shared email
instance reads them.
If the resource is missing, importing this module is still safe — an app that never sends will start
normally. The failure arrives at send(), with a message saying what to add.
API
email.send({ to, subject, html, text })
Sends one message. At least one of html or text is required. Resolves to { sent: true }, or
throws a ChatteeEmailError.
Obvious mistakes are caught locally before any request is made, so a missing subject fails immediately naming the field rather than after a round trip and a provider's paraphrase of it.
email.isConfigured()
true when the platform has supplied both values. Useful on a health or config endpoint, so your
landing page can say "email is not set up yet" rather than letting the first real signup be the
thing that discovers it.
It does not mean a provider is connected. There are three separate states and they are easy to confuse, so:
| Question | How you ask it |
| --- | --- |
| Does this app have the email resource? | email.isConfigured() — the bindings are present |
| Has the owner connected Brevo/SES/Mailchimp? | try to send; err.code === 'email_not_connected' |
| Should the browser offer "Forgot password"? | emailAvailable from @chattee.ai/auth's getConfig() |
Only the third answers for the browser, and only it is safe to render a login page from.
Email arrives with user accounts, whether or not you asked for it
A project with the auth resource always has this one too: Chattee's auth implies it, because confirming a new address, resetting a password and magic-link sign-in are all email, and an app with accounts and no mailer has no way to let a locked-out user back in.
So if you are building sign-up and sign-in, this client is available to you and there is nothing to declare. Note the platform sends the auth mail itself — confirmation, reset and magic links are none of your code's business, and you should not compose your own versions of them. Use this client for your app's mail: receipts, notifications, digests.
createEmailClient(options)
Builds a client explicitly. Most apps should use the shared email instance; this exists for tests
and for anything needing two clients at once.
| option | default |
| ----------- | ----------------------------------- |
| apiUrl | process.env.CHATTEE_API_URL |
| token | process.env.CHATTEE_PROJECT_TOKEN |
| fetchImpl | globalThis.fetch |
| timeoutMs | 20000 |
Errors
ChatteeEmailError carries a stable code worth branching on. The message is for humans and may
change.
| code | meaning | whose problem |
| ----------------------- | ------------------------------------------- | --------------------------------- |
| email_not_provisioned | this project has no email resource | ask the agent to add it |
| email_not_connected | no provider connected yet | the owner, in the Data tab |
| sender_not_configured | connected, but no sender address set | the owner, in the Data tab |
| provider_error | the provider refused the message | usually the owner's sender domain |
| recipient_required | to was missing or blank | yours |
| subject_required | subject was missing or blank | yours |
| body_required | neither html nor text given | yours |
| unauthorized | the project token is missing or wrong | redeploy the project |
| not_configured | no Chattee configuration in the environment | add the resource |
| unreachable | the platform could not be reached | transient; retry |
err.retryable is true only for unreachable and provider_error. The rest are configuration or
input problems — a retry loop around those is just a slower failure, and it would send the owner's
provider the same rejected request repeatedly.
Distinguishing "not set up" from "broken"
Worth handling separately, because they are not your bug and your users should not see a stack trace:
import { email, ChatteeEmailError } from "@chattee.ai/email";
try {
await email.send({ to, subject, html });
} catch (err) {
if (err instanceof ChatteeEmailError && err.code === "email_not_connected") {
return res
.status(503)
.json({ error: "Email is not set up for this app yet." });
}
if (err instanceof ChatteeEmailError && err.retryable) {
return res
.status(503)
.json({ error: "Email is temporarily unavailable. Please try again." });
}
throw err;
}A note on provider IP allowlists
Some providers — Brevo by default — refuse a request from an unrecognised source IP before they look at the key at all. When that happens the owner sees the provider's own message, including the addresses to allow, in the Data tab. If a key that is definitely correct is being rejected, that is almost always why.
Requirements
Node 18 or newer (uses the built-in fetch).
Licence
MIT
Release notes
1.1.1
Documentation only. No API change, and nothing you wrote needs to change.
Spells out the three states that isConfigured() does not distinguish. It reports whether the
platform supplied this client's bindings — not whether the owner has connected a provider, and not
whether the browser should offer a password reset. Those are a caught email_not_connected and
@chattee.ai/auth's getConfig().emailAvailable respectively, and conflating them is how an app
ends up rendering a "Forgot password" link that reports success and sends nothing.
Also records that the auth resource implies this one, so an app with user accounts always has a mailer available — and that the platform composes the auth mail itself, so confirmation, reset and magic-link messages are not yours to write.
1.1.0
Adds AGENT.md to the published package. It is the model-facing core of this README — the same
prose, without the install/requirements/licence/release-note sections — and it is what Chattee's
build agent is given when a project uses this resource. Publishing it means the copy in
node_modules always describes the version actually installed.
Nothing you wrote needs to change; there is no API change in this release.
1.0.1
Email failed with not_configured on a project without file storage, and 404'd on one with it.
Both halves are fixed; the binding half needs a Chattee backend from this release or later.
No API change, and the URL is normalised, so an app running against either form of the binding works.
1.0.0
First release: email.send({ to, subject, html, text }) against the project owner's own provider
account, connected in the Data tab. Provider-agnostic — swapping Brevo for SES needs no code change.
