Skip to content

Install

bash
npm install @maillune/sdk

How authentication works

The api-key attribute must be a session token — a short-lived JWT issued by your own backend. Never use your secret API key in client-side code.

Browser → GET /api/editor-session → Your server → POST /v1/sessions → Maillune API

Browser ←──────────── sessionToken (2-hour JWT) ────────────────────

See Authentication for the full flow.

Server-side token endpoint

Add one route to your backend (runs server-side only):

ts
// app/api/editor-session/route.ts  (Next.js App Router)
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; // or your auth lib

export async function GET() {
  const session = await getServerSession();
  if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  const { sessionToken } = await fetch('https://api.maillune.com/v1/sessions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.MAILLUNE_SECRET_KEY}`, // server-only
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ userId: session.user.id }),
  }).then(r => r.json());

  return NextResponse.json({ sessionToken });
}

Never use NEXT_PUBLIC_ prefix for your Maillune key

NEXT_PUBLIC_* variables are embedded into the browser bundle. Always use a plain MAILLUNE_SECRET_KEY server-only variable and route through a backend endpoint.

EmailEditor component

tsx
// components/EmailEditor.tsx
'use client'; // Next.js App Router — required for browser APIs

import { useEffect, useRef, useState } from 'react';
import '@maillune/sdk';
import type { EmailBuilderElement, BlockTree } from '@maillune/sdk';

interface Props {
  userId: string;
  initialTemplate?: BlockTree;
  onSave: (json: BlockTree) => Promise<void>;
}

export function EmailEditor({ userId, initialTemplate, onSave }: Props) {
  const ref = useRef<EmailBuilderElement>(null);
  const [sessionToken, setSessionToken] = useState('');

  // Fetch session token from your backend on mount
  useEffect(() => {
    fetch('/api/editor-session')
      .then(r => r.json())
      .then(({ sessionToken }) => setSessionToken(sessionToken));
  }, []);

  // Load initial template once the element is ready
  useEffect(() => {
    if (initialTemplate && ref.current) {
      ref.current.load(initialTemplate);
    }
  }, [initialTemplate, sessionToken]);

  // Wire up save event
  useEffect(() => {
    const el = ref.current;
    if (!el || !sessionToken) return;

    const handler = (e: Event) => {
      const { json } = (e as CustomEvent<{ json: BlockTree }>).detail;
      onSave(json);
    };

    el.addEventListener('eb:save', handler);
    return () => el.removeEventListener('eb:save', handler);
  }, [onSave, sessionToken]);

  if (!sessionToken) return <div>Loading editor…</div>;

  return (
    <div style={{ height: '100vh' }}>
      <email-builder
        ref={ref}
        api-key={sessionToken}
        user-id={userId}
        theme="auto"
      />
    </div>
  );
}

JSX types

Add to globals.d.ts (or any .d.ts file in your project):

ts
import type { EmailBuilderElement } from '@maillune/sdk';

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'email-builder': {
        'api-key': string;
        'user-id': string;
        'template-id'?: string;
        theme?: 'light' | 'dark' | 'auto';
        ref?: React.Ref<EmailBuilderElement>;
        style?: React.CSSProperties;
        className?: string;
      };
    }
  }
}

Next.js (App Router) page

tsx
// app/editor/page.tsx
import { EmailEditor } from '@/components/EmailEditor';
import { getServerSession } from 'next-auth';

export default async function EditorPage() {
  const session = await getServerSession();
  return (
    <EmailEditor
      userId={session!.user.id}
      onSave={saveTemplate}
    />
  );
}

With template persistence

tsx
'use client';
import { useState, useEffect } from 'react';
import type { BlockTree } from '@maillune/sdk';
import { EmailEditor } from '@/components/EmailEditor';

export function EditorPage({ templateId, userId }: { templateId: string; userId: string }) {
  const [template, setTemplate] = useState<BlockTree | null>(null);

  useEffect(() => {
    fetch(`/api/templates/${templateId}`)
      .then(r => r.json())
      .then(data => setTemplate(data.blockTree));
  }, [templateId]);

  const handleSave = async (json: BlockTree) => {
    await fetch(`/api/templates/${templateId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ blockTree: json }),
    });
  };

  if (!template) return <div>Loading…</div>;

  return (
    <EmailEditor
      userId={userId}
      initialTemplate={template}
      onSave={handleSave}
    />
  );
}