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:
| Surface | Who owns it | Where it lives |
|---|---|---|
| Official adapter | Email SDK maintainers | @opencoredev/email-sdk/{adapter} |
| Community adapter | The community package maintainer | Their npm package, such as email-sdk-acme-mail |
| Community docs listing | Email SDK docs, by pull request | apps/fumadocs/content/community/plugins.json |
| Verified listing | Email SDK docs, for one version | Same 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
- Create a separate package owned by the community maintainer.
- Implement the adapter contract from Create an adapter.
- Export both the adapter factory and plugin factory.
- Publish the package to npm from the maintainer's own repo.
- Add a short README with install, setup, field support, and test instructions.
- 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.
Recommended package shape
email-sdk-acme-mail/
src/
index.ts
acme-mail.ts
plugin.ts
acme-mail.test.ts
package.json
tsconfig.json
README.mdExport 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
| Thing | Recommendation |
|---|---|
| Package | email-sdk-{provider} |
| Adapter | Provider slug, such as acme-mail |
| Plugin ID | Same slug unless you need variants |
| Env var | Provider slug in uppercase, plus key |
| Export | {provider} and {provider}Plugin |
Example:
| Thing | Example |
|---|---|
| Package | email-sdk-acme-mail |
| Adapter | acme-mail |
| Plugin ID | acme-mail |
| Env var | ACME_MAIL_API_KEY |
| Export | acmeMail, acmeMailPlugin |
Document field support
Every adapter README should say whether these fields are supported:
htmlandtextcc,bcc, andreplyToheadersattachmentstagsmetadataidempotencyKey
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:
| Field | Support | Notes |
|---|---|---|
html, text | Yes | Sent as provider body fields. |
cc, bcc | Yes | Mapped to provider recipient fields. |
replyTo | No | Throws when provided. |
headers | No | Provider does not support custom headers. |
attachments | No | Throws when provided. |
tags | Yes | Mapped to provider tags. |
metadata | Partial | Included when values are strings. |
idempotencyKey | No | Provider 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:
- Payload mapping.
- Response mapping.
- Unsupported field errors.
AbortSignalforwarding.- 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
createEmailClientexample. - 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:checkThat 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?
| Status | Use it when... |
|---|---|
community | A third-party maintainer published the package and wants discoverability. |
verified | A specific published version passed the registry verification checklist. |
official | The 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
@opencoredevscope. - 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
verifiedbefore a package version has actually passed verification. - Do not mark a third-party package
official.
Next
- Create an adapter for implementation details.
- Adapter contract for the exact TypeScript types.
- Community registry for listing and verification fields.
