Email SDK
GuidesBuild

Publish a community adapter

Build, publish, and list a third-party provider adapter without making it official.

Community adapters are third-party npm packages. They let someone support a provider without adding that provider to @opencoredev/email-sdk, the official CLI, or the official adapter maintenance surface.

That split is intentional:

SurfaceWho owns itWhere it lives
Official adapterEmail SDK maintainers@opencoredev/email-sdk/{adapter}
Community adapterThe community package maintainerTheir npm package, such as email-sdk-acme-mail
Community docs listingEmail SDK docs, by pull requestapps/fumadocs/content/community/plugins.json
Verified listingEmail SDK docs, for one versionSame registry entry plus verification metadata

Most community adapters should start as status: "community". That means listed, discoverable, and clearly third-party. It does not mean endorsed, bundled, or maintained by OpenCore.

Adapter or plugin?

An adapter is the provider implementation. It maps Email SDK's normalized EmailMessage into one provider API request.

createEmailClient({
  adapters: [acmeMail({ apiKey: process.env.ACME_MAIL_API_KEY! })],
});

A plugin is the package-friendly wrapper. It registers the adapter through the plugins array, which is the cleanest setup for reusable community packages.

createEmailClient({
  plugins: [acmeMailPlugin({ apiKey: process.env.ACME_MAIL_API_KEY! })],
});

Ship both. The plain adapter keeps advanced users close to the provider. The plugin wrapper gives most users a stable one-line integration.

Publishing path

  1. Create a separate package owned by the community maintainer.
  2. Implement the adapter contract from Create an adapter.
  3. Export both the adapter factory and plugin factory.
  4. Publish the package to npm from the maintainer's own repo.
  5. Add a short README with install, setup, field support, and test instructions.
  6. Optionally open a pull request to list the package in the Email SDK community registry.

Do not add community adapters to packages/email-sdk/src, the official adapter catalog, the official CLI adapter list, or the official package exports unless the project has agreed to maintain that provider as official.

email-sdk-acme-mail/
  src/
    index.ts
    acme-mail.ts
    plugin.ts
    acme-mail.test.ts
  package.json
  tsconfig.json
  README.md

Export both forms

Export a plain adapter factory and an adapter plugin factory from the package root.

export { acmeMail } from "./acme-mail";
export { acmeMailPlugin } from "./plugin";
export type { AcmeMailOptions } from "./acme-mail";

The plain adapter keeps advanced users close to the provider. The plugin form gives most apps the cleanest setup:

createEmailClient({
  plugins: [acmeMailPlugin({ apiKey: process.env.ACME_MAIL_API_KEY! })],
});

Package exports

Community packages should use Email SDK as a peer dependency. That keeps apps from installing two copies of the core types.

{
  "name": "email-sdk-acme-mail",
  "type": "module",
  "sideEffects": false,
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "peerDependencies": {
    "@opencoredev/email-sdk": "^0.4.0"
  }
}

Avoid install scripts and binaries. Adapter packages should be boring library packages: importable code, types, tests, and provider docs.

Naming

ThingRecommendation
Packageemail-sdk-{provider}
AdapterProvider slug, such as acme-mail
Plugin IDSame slug unless you need variants
Env varProvider slug in uppercase, plus key
Export{provider} and {provider}Plugin

Example:

ThingExample
Packageemail-sdk-acme-mail
Adapteracme-mail
Plugin IDacme-mail
Env varACME_MAIL_API_KEY
ExportacmeMail, acmeMailPlugin

Document field support

Every adapter README should say whether these fields are supported:

  • html and text
  • cc, bcc, and replyTo
  • headers
  • attachments
  • tags
  • metadata
  • idempotencyKey

If a field is not supported, say whether the adapter throws. Community adapters should throw on unsupported non-empty fields instead of dropping them.

Use a small table in the package README:

FieldSupportNotes
html, textYesSent as provider body fields.
cc, bccYesMapped to provider recipient fields.
replyToNoThrows when provided.
headersNoProvider does not support custom headers.
attachmentsNoThrows when provided.
tagsYesMapped to provider tags.
metadataPartialIncluded when values are strings.
idempotencyKeyNoProvider does not expose this feature.

This is not paperwork. Field support determines whether the adapter is safe as a fallback route. A backup provider that silently drops attachments or reply-to addresses can make a send look successful while changing the email.

Include a smoke test

At minimum, test:

  1. Payload mapping.
  2. Response mapping.
  3. Unsupported field errors.
  4. AbortSignal forwarding.
  5. Plugin registration through createEmailClient.
import { describe, expect, test } from "bun:test";
import { createEmailClient } from "@opencoredev/email-sdk";
import { acmeMailPlugin } from "./index";

describe("acmeMailPlugin", () => {
  test("registers an adapter", () => {
    const email = createEmailClient({
      plugins: [acmeMailPlugin({ apiKey: "test" })],
    });

    expect(email.adapters.has("acme-mail")).toBe(true);
    expect(email.defaultAdapter).toBe("acme-mail");
  });
});

README checklist

  • Install command for the community package and @opencoredev/email-sdk.
  • Minimal createEmailClient example.
  • Required environment variables.
  • Field support table.
  • Error behavior for unsupported fields.
  • Test command.
  • Link to Email SDK adapter contract.
  • Link to the provider API docs used by the adapter.

Keep the README short enough that a user can decide if the adapter fits their fallback route.

List it in Email SDK docs

After publishing to npm, the maintainer can open a pull request that adds an entry to apps/fumadocs/content/community/plugins.json.

{
  "name": "Acme Mail",
  "package": "email-sdk-acme-mail",
  "kind": "adapter",
  "status": "community",
  "description": "Adds an Acme Mail provider adapter for Email SDK.",
  "href": "https://www.npmjs.com/package/email-sdk-acme-mail",
  "repo": "https://github.com/acme/email-sdk-acme-mail",
  "maintainer": "acme",
  "pluginId": "acme-mail",
  "adapter": "acme-mail",
  "importName": "acmeMailPlugin"
}

Then run:

bun run community:check

That check validates the registry shape. In CI, verified and official entries also get a published package metadata check. Community entries are listed as third-party packages without verification metadata.

Community, verified, or official?

StatusUse it when...
communityA third-party maintainer published the package and wants discoverability.
verifiedA specific published version passed the registry verification checklist.
officialThe adapter is maintained by OpenCore or ships in this repository.

Verification applies to one version. If the package releases again, the registry should keep the old verifiedVersion until the new version is reviewed.

What not to do

  • Do not publish under the @opencoredev scope.
  • Do not ask users to import from @opencoredev/email-sdk/{community-provider}.
  • Do not add provider secrets or live credentials to examples.
  • Do not hide provider limitations behind successful sends.
  • Do not mark a listing verified before a package version has actually passed verification.
  • Do not mark a third-party package official.

Next

On this page