npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chattee.ai/email

v1.1.1

Published

Transactional email client for applications built on Chattee.

Downloads

669

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.