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

email-craft-sdk

v1.3.0

Published

Backend SDK for the Email Craft editor, render designs to email-safe HTML and MJML on the server, and resolve entitlements server-to-server with a secret sk_live_ key. The Admin SDK paired with the react-email-craft frontend. Like firebase-admin, for an e

Readme

email-craft-sdk

npm version downloads license

The backend half of Mailkaro, the server SDK that holds your secret key and tells the editor what your users are allowed to do. It's the same split as Firebase:

| | Package | Runs on | Key it uses | | --- | --- | --- | --- | | Frontend | react-email-craft | your web app (browser) | public pk_live_... (or nothing) | | Backend | email-craft-sdk (this one) | your server (Node) | secret sk_live_... |

The idea in one line: your browser never holds a secret, it asks your server "what can this user do?", and your server uses this SDK (with the secret key) to answer.

It also renders designs to email-safe HTML and MJML on the server, renderDesign(design, { fullDocument: true }) and renderMjml(design) are pure functions (no headless browser, no key required).

Try it live

Open the Next.js starter (editor + this SDK) in your browser:

Open in StackBlitz Edit in CodeSandbox

Guide: Render MJML and email-safe HTML on the server in Node.js


Requirements

  • Node.js 18 or newer (it uses the built-in fetch).
  • A secret server key that looks like sk_live_..., from your Mailkaro dashboard. Keep it in your backend environment only, never send it to the browser.

Install

npm install email-craft-sdk
# or: pnpm add email-craft-sdk   /   yarn add email-craft-sdk

Quick start

Three lines on your backend, create the client, resolve the runtime, send it to your editor:

import { Mailkaro } from "email-craft-sdk";

const mailkaro = Mailkaro.fromEnv();          // reads MAILKARO_SERVER_KEY (sk_live_…) from your env

// inside an authenticated backend route:
const runtime = await mailkaro.resolveRuntime();   // → { plan, entitlements, capabilities, merge_tags, … }
res.json(runtime);                                  // hand it to your frontend <EmailEditor runtimeUrl=… />

That's the whole backend. Everything below is optional detail. Full runnable versions are in A complete Express route and the combined example.


The flow (how the two packages work together)

1. Browser (react-email-craft)  ──▶  2. YOUR backend route (email-craft-sdk)  ──▶  3. Mailkaro API
   "what can I do?"                    checks the user is logged in, validates the
                                       then resolveRuntime()                       secret sk_live_ key
   ◀──────────────────────────────────  returns the runtime  ◀────────────────────
   editor unlocks the allowed features

Your frontend points the editor at your endpoint (no secret in the browser):

import EmailEditor from "react-email-craft";

<EmailEditor runtimeUrl="/mailkaro/embed-runtime" />;

A complete Express route

import express from "express";
import { Mailkaro } from "email-craft-sdk";

const app = express();

// Create it once and reuse it. Reads MAILKARO_SERVER_KEY / MAILKARO_API_URL / MAILKARO_ORG_ID
// from your environment, and auto-detects features from your env (S3 bucket, AI key, …).
const mailkaro = Mailkaro.fromEnv();

// ...or configure it by hand:
// const mailkaro = new Mailkaro({
//   serverKey: process.env.MAILKARO_SERVER_KEY!, // sk_live_… (backend only)
//   apiUrl: process.env.MAILKARO_API_URL, // optional, defaults to https://api.mailkaro.com
//   orgId: process.env.MAILKARO_ORG_ID, // optional, for multi-tenant accounts
// });

// The endpoint your editor calls. `requireAuth` is YOUR existing login check.
app.get("/mailkaro/embed-runtime", requireAuth, async (req, res) => {
  const runtime = await mailkaro.resolveRuntime();
  res.json(runtime); // { plan, entitlements, merge_tags, capabilities, expired, free }
});

app.listen(3000);

That's the whole backend. Your frontend uses runtimeUrl="/mailkaro/embed-runtime" and the editor unlocks whatever the runtime says.


Every key, one by one (what it is, and what it turns on)

Each key is independent. Only the first one is required, add the others only if you want that feature. For every key below you'll see the env line, what it means, and a tiny standalone example.

1. MAILKARO_SERVER_KEY, your secret key (required)

MAILKARO_SERVER_KEY=sk_live_xxxxxxxxxxxxx

Meaning: this proves the request is coming from you. The SDK uses it to ask Mailkaro what plan and features your account has. Never send it to the browser.

// This one key alone is enough to know your plan:
const mailkaro = new Mailkaro({ serverKey: process.env.MAILKARO_SERVER_KEY! });
const runtime = await mailkaro.resolveRuntime(); // → { plan: "pro", entitlements, … }

2. MAILKARO_API_URL, which Mailkaro server to talk to (optional)

MAILKARO_API_URL=https://api.mailkaro.com   # this is the default, only change it for self-hosting

Meaning: the address of the Mailkaro API. Leave it out and it uses https://api.mailkaro.com.

3. MAILKARO_ORG_ID, which workspace (optional, for multi-tenant)

MAILKARO_ORG_ID=org_xxxxxxxx

Meaning: if your account has several organizations/workspaces, this picks which one to resolve. Most single-company setups don't need it.

4. MAILKARO_S3_BUCKET (+ AWS creds), turn on S3 uploads (optional, free on any plan)

MAILKARO_S3_BUCKET=my-bucket
AWS_ACCESS_KEY_ID=xxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxx
AWS_REGION=us-east-1

Meaning: shows an S3 upload/browse tab in the editor, and files go to your bucket. Because you supply the storage, it works on any plan, free included.

const mailkaro = Mailkaro.fromEnv();
const runtime = await mailkaro.resolveRuntime();
runtime.capabilities.s3; // → true (the editor now shows the S3 tab)

5. An AI key, turn on AI writing (optional, free on any plan)

OPEN_AI_KEY=sk-xxxxxxxx        # or:  CLAUDE_AI_KEY=xxxxxxxx   or:  GEMINI_AI_KEY=xxxxxxxx

Meaning: enables the editor's AI features (e.g. "write subject lines"). The key stays on your server, the browser calls your backend, your backend calls the AI provider.

runtime.capabilities.ai; // → true
// and you can run AI on the server directly:
await mailkaro.ai("generateSubjectLines", { prompt: "Summer sale email", count: 5 });

6. MAILKARO_GOOGLE_CLIENT_ID, turn on Google Drive (optional, free on any plan)

MAILKARO_GOOGLE_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com

Meaning: shows the "Connect Google Drive" picker so users can pull images from Drive. Only the public client id reaches the browser, the client secret stays on your backend.


Full example, all the keys, working together

Here's a complete setup with every key turned on. Three files: your .env, your Node backend, and your React page. Every line is explained.

Step 1, .env (backend only; never shipped to the browser):

# ── Required ──────────────────────────────────────────────
MAILKARO_SERVER_KEY=sk_live_xxxxxxxx          # your secret key, proves the server is you

# ── Optional ──────────────────────────────────────────────
MAILKARO_ORG_ID=org_xxxxxxxx                  # your workspace (only for multi-tenant accounts)

# Turn on S3 uploads (free), files go to YOUR bucket
MAILKARO_S3_BUCKET=my-bucket
AWS_ACCESS_KEY_ID=xxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxx
AWS_REGION=us-east-1

# Turn on AI writing (free), pick ONE provider
OPEN_AI_KEY=sk-xxxxxxxx

# Turn on Google Drive picker (free)
MAILKARO_GOOGLE_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com

Step 2, your Node backend (server.js):

import express from "express";
import { Mailkaro } from "email-craft-sdk";

const app = express();

// Build the SDK from ALL the env keys above at once.
// It reads the server key, and auto-detects S3 + AI + Google from the env, nothing else to wire up.
const mailkaro = Mailkaro.fromEnv();

// This is the single endpoint your editor will call.
// `requireAuth` is YOUR normal login check, only logged-in users get past it.
app.get("/mailkaro/embed-runtime", requireAuth, async (req, res) => {
  // Ask Mailkaro: what plan is this account on, and which features are on?
  const runtime = await mailkaro.resolveRuntime();
  // Send the answer to the browser. It's just on/off flags + public config, no secrets inside.
  res.json(runtime);
});

app.listen(3000, () => console.log("backend ready on http://localhost:3000"));

Step 3, your React page (Editor.jsx):

import EmailEditor from "react-email-craft";
import { useRef } from "react";

export function Editor() {
  const ref = useRef(null);

  // When the user clicks Save, ask the editor for the finished email.
  const save = () =>
    ref.current.editor.exportHtml(({ design, html }) => {
      saveToMyDatabase(design); // store the design so the user can edit it again later
      sendWithMyEmailProvider(html); // or send the HTML right away
    });

  return (
    <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
      <button onClick={save}>Save</button>
      <EmailEditor
        ref={ref}
        // Point at YOUR backend route from Step 2, no secret in the browser.
        // The editor calls it, gets the runtime, and turns on S3 + AI + Google automatically.
        runtimeUrl="/mailkaro/embed-runtime"
        style={{ flex: 1 }}
      />
    </div>
  );
}

What happens when it runs: the editor loads → it calls your /mailkaro/embed-runtime route → your backend replies with the plan + features → the editor opens with S3, AI, and Google Drive all enabled, powered entirely by your own keys, and the browser never saw a single secret.

Prefer the 2-minute version? If you don't need your own S3/AI and just want the editor, skip the backend entirely: put a publishable token straight on the component, <EmailEditor token="pk_live_..." />, and you're done. Add the backend later when you want BYO infrastructure.


Bring your own S3 / AI / Google, free on any plan

Anything you provide runs on any plan, free included, Mailkaro only gates the features Mailkaro itself hosts. Because the credentials live in your backend, the browser never sees them.

| You set in your backend | The editor shows | Costs a paid plan? | | --- | --- | --- | | MAILKARO_S3_BUCKET (+ AWS creds) | S3 asset browser/upload | ❌ No, free | | an AI key (OPEN_AI_KEY, …) | AI writing features | ❌ No, free | | MAILKARO_GOOGLE_CLIENT_ID | Google Drive picker | ❌ No, free |

resolveRuntime() returns a capabilities object with on/off flags and non-secret config (like font names), never a secret. The editor renders those, and for the privileged work (signing an upload, generating AI text) it calls back to your backend, so no credential ever reaches the browser.

Per-tenant S3 folders

Keep each customer's uploads in their own folder:

const prefix = mailkaro.s3AssetPrefix(user.organization_id); // e.g. "mailkaro/<org-id>/"

AI on your server

const result = await mailkaro.ai("generateSubjectLines", { prompt: "Summer sale email", count: 5 });

Supported methods: rewriteText, generateSubjectLines, generateAltText, generateDesignFromPrompt and editDesign (the editor's chat assistant). Method names are camelCase. For editDesign pass { message, history?, design_summary }; it returns { operations, summary } and the editor applies the operations itself. Any other method throws.


Render an email on the server (no browser needed)

email-craft-sdk bundles the renderer, so you can turn a saved design into HTML on the backend, handy for sending the actual email:

import { renderDesign } from "email-craft-sdk";

const html = renderDesign(design, { mergeTags, fullDocument: true });
// hand `html` to your email provider (SES, SendGrid, Postmark, …)

API reference

Mailkaro.fromEnv()

Builds the client from your environment variables (see the table above). The easiest way to start.

new Mailkaro({ serverKey, apiUrl?, orgId?, capabilities?, ai?, fetch? })

Build it by hand. serverKey is required. fetch is optional (defaults to the global fetch). createMailkaro(options) does the same thing if you prefer a factory function.

resolveRuntime({ project? }): Promise<MailkaroRuntime>

The main method. Returns { plan, entitlements, merge_tags, capabilities, expired, free } for your key. Never throws, if the key is invalid, expired, or Mailkaro is unreachable, it safely returns the free runtime, so a paid tier can never leak by accident.

isPaid(project?): Promise<boolean>

Shortcut: true when the plan is pro, ai_pro, or enterprise.

ai(method, input): Promise<unknown>

Run an AI action on your server using your configured AI key.

s3AssetPrefix(scopeId): string

The isolated S3 key prefix for one tenant (e.g. "mailkaro/<org-id>/").

renderDesign(design, options?)

Turn a design JSON into HTML on the server.


Why this is safe

  • The secret never reaches the browser, sk_live_... lives only in your backend env.
  • Your backend authorizes the request using your own login/session before resolving anything.
  • Enforcement is server-side, resolveRuntime() never self-grants; an invalid/expired/unreachable key falls back to free. Paid features cannot leak.

Frontend companion

Pair this with react-email-craft in your web app. Point the editor at your backend with runtimeUrl="/mailkaro/embed-runtime" and you're done.


License

MIT