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

better-auth-mail

v0.1.3

Published

Better Auth plugin for Gmail + Outlook (Microsoft Graph) integration.

Readme

better-auth-mail

Better Auth plugin that adds Gmail + Outlook (Microsoft Graph) integrations.

Status

Gmail watch + Pub/Sub webhook processing is implemented.

Outlook (Microsoft Graph) is not implemented yet.

This package stores no email bodies or attachments—only minimal state (watch/subscription cursors + expirations).

Quickstart (Gmail)

  1. bun add better-auth-mail
  2. In your Better Auth config:
    • enable account.encryptOAuthTokens + account.updateAccountOnSignIn
    • configure socialProviders.google with accessType: "offline" and prompt: "consent"
    • add the plugin with:
      • providerIds.gmail = "google"
      • gmail.pubsubTopicName = "projects/<gcp-project-id>/topics/<topic>"
      • maintenanceKey = <random secret>
  3. In Google Cloud:
    • create the Pub/Sub topic
    • grant publisher to [email protected]
    • create a push subscription to:
      • https://<your-domain>/api/auth/better-auth-mail/webhooks/gmail
    • include header:
      • x-better-auth-mail-maintenance-key: <maintenanceKey>
  4. In your app UI: call POST /better-auth-mail/enable with { provider: "gmail" }
  5. Verify:
    • GET /better-auth-mail/status shows Gmail valid: true
    • send yourself an email and confirm your onEvents handler fires (or webhook returns processed > 0)
  6. Run maintenance periodically (scheduler/worker of your choice):
    • POST /better-auth-mail/maintenance/renew with the maintenance header

Quickstart (Outlook / Microsoft Graph)

  1. Configure Better Auth socialProviders.microsoft and request:
    • Mail.Read
    • offline_access
  2. Add plugin config:
    • providerIds.outlook = "microsoft"
    • ensure Better Auth baseURL is set (or pass outlook.notificationUrl)
  3. Microsoft Graph will validate your webhook URL via a GET request with validationToken:
    • GET /better-auth-mail/webhooks/graph?validationToken=... (the plugin supports this)
  4. In your app UI: call POST /better-auth-mail/enable with { provider: "outlook" }
  5. Verify:
    • GET /better-auth-mail/status shows Outlook valid: true

Notes:

  • Graph will call your webhook with a GET validationToken during subscription creation.
  • Subscriptions expire and must be renewed via POST /better-auth-mail/maintenance/renew.

Install

bun add better-auth-mail

Server usage (Better Auth plugin)

import { betterAuth } from "better-auth";
import {
  betterAuthMail,
  DEFAULT_REQUIRED_SCOPES,
  GMAIL_SCOPE_READONLY,
} from "better-auth-mail";

export const auth = betterAuth({
  account: {
    // recommended when storing refresh tokens in the DB
    encryptOAuthTokens: true,
    updateAccountOnSignIn: true,
  },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      // Needed to reliably get a refresh token from Google for server-side sync/webhooks.
      accessType: "offline",
      // Often needed to get a refresh token for existing users.
      prompt: "consent",
      // You can also request these incrementally via linkSocial({ scopes }) later.
      scope: [GMAIL_SCOPE_READONLY],
    },
  },
  plugins: [
    betterAuthMail({
      providerIds: {
        gmail: "google",
        outlook: "microsoft",
      },
      // optional override
      requiredScopes: DEFAULT_REQUIRED_SCOPES,
      maintenanceKey: process.env.BETTER_AUTH_MAIL_MAINTENANCE_KEY!,
      gmail: {
        pubsubTopicName: process.env.GMAIL_PUBSUB_TOPIC_NAME!, // projects/<gcp-project-id>/topics/<topic>
      },
    }),
  ],
});

Client usage

import { createAuthClient } from "better-auth/client";
import { betterAuthMailClient } from "better-auth-mail";

export const authClient = createAuthClient({
  plugins: [betterAuthMailClient()],
});

Search/list messages from the client

// Gmail search (Gmail UI search syntax)
const gmail = await authClient.betterAuthMail.messages({
  provider: "gmail",
  q: "from:stripe has:attachment newer_than:30d",
  include: "metadata",
  maxResults: 25,
});

// Outlook search (Graph $search)
const outlook = await authClient.betterAuthMail.messages({
  provider: "outlook",
  q: "invoice",
});

Gmail setup (Pub/Sub + watch)

1) OAuth scopes (Google)

This plugin’s default Gmail scope requirement is:

  • https://www.googleapis.com/auth/gmail.readonly

If a user signed up without Gmail scopes, you must re-run OAuth for that provider (incremental consent). In Better Auth that’s typically done via:

  • authClient.linkSocial({ provider: "google", scopes: [GMAIL_SCOPE_READONLY] })

2) Pub/Sub topic + push subscription (Google Cloud)

Per app:

  • Create a Pub/Sub topic named by GMAIL_PUBSUB_TOPIC_NAME:
    • Format: projects/<gcp-project-id>/topics/<topic>
  • Grant Gmail permission to publish to your topic:
  • Create a push subscription pointing to your webhook endpoint.

3) Webhook endpoint

The plugin exposes:

  • POST /better-auth-mail/webhooks/gmail

Because Better Auth is usually mounted under /api/auth, your full URL will typically look like:

  • https://<your-domain>/api/auth/better-auth-mail/webhooks/gmail

4) Webhook authentication

The plugin currently protects webhooks with a shared secret header:

  • x-better-auth-mail-maintenance-key: <BETTER_AUTH_MAIL_MAINTENANCE_KEY>

Configure your Pub/Sub push subscription to include this header.

Using it

Enable Gmail for the signed-in user

Call the plugin endpoint (either via the inferred client API or the helper actions from betterAuthMailClient()):

  • POST /better-auth-mail/enable with { provider: "gmail" }

On success, the plugin:

  • Calls Gmail users.watch
  • Stores historyId and watchExpiration
  • Stores the mailbox emailAddress for webhook routing

List/search messages

  • GET /better-auth-mail/messages?provider=gmail&q=<query>&pageToken=<token>&maxResults=<n>&include=ids|metadata

Notes:

  • q uses Gmail’s search syntax (same as the Gmail UI search box).
  • include=ids returns message IDs + thread IDs (fast, no per-message fetch).
  • include=metadata returns basic metadata (subject/from/to/snippet/date/unread). This reduces client-side N+1 calls, but the server will make one Gmail API call per message ID.

Get a message (metadata + attachment list)

  • GET /better-auth-mail/messages/<messageId>?provider=gmail

Download an attachment

  • GET /better-auth-mail/messages/<messageId>/attachments/<attachmentId>?provider=gmail&filename=<optional>

Check status

  • GET /better-auth-mail/status

Returns per provider:

  • enabled (user opted in)
  • valid (has account + refresh token + required scopes + active watch/subscription)
  • reasons (actionable reasons like MISSING_REFRESH_TOKEN, MISSING_SCOPES, SUBSCRIPTION_EXPIRED)

Watch expiration / renewals

Gmail watches expire. This package exposes a secured maintenance endpoint that your scheduler/worker can call (TBD):

  • POST /better-auth-mail/maintenance/renew

Send:

  • x-better-auth-mail-maintenance-key: <BETTER_AUTH_MAIL_MAINTENANCE_KEY>

Body (optional):

  • { "provider": "gmail", "renewBeforeMinutes": 60, "limit": 200, "repairMissingState": true }

Repairing missing state

If mailIntegration is enabled but mailGmailState is missing/corrupt (no historyId or watchExpiration), status will show SUBSCRIPTION_MISSING.

To repair, call:

  • POST /better-auth-mail/maintenance/renew with repairMissingState: true (default)

Production note: history cursor expiry

Gmail history cursors (historyId) can become invalid (for example, if you don’t process notifications for too long). When this happens, webhook processing will re-establish a watch and reset the stored cursor, so you may miss historical events during the gap.