In this guide, we will show you how to easily create a working visual editor with Mirin Embeds.

Embeds work with any React framework and any Javascript backend. For simplicity, we’ll be using Next.js 15 with React Server Components in this guide.

Create a new Embed

Login to your account on Mirin and create a new Embed. You will then need to generate a new Private Key that you will use to setup the SDKs later in this guide.

Project setup

  1. Install Next.js
npx create-next-app@latest
  1. Install SDK

After you have setup a fresh Next.js app, cd into the project directory and install the following packages:

npm install --save @mirinhq/embed @mirinhq/admin
  1. Add your private key

In your Next.js project directory, create a new .env.local file and add your Embed’s private key.

MIRIN_EMBED_PRIVATE_KEY=...
  1. Setup the Admin client

Then, we will need to setup a new admin Client which we’ll use to perform priviledged operations such as authenticating our users into our new embedded Mirin editor.

Typically, this will live in your backend and you would create API endpoints to interface with the Client. In Next.js 15, we can use server actions without having to create a separate backend:

lib/server.tsx
"use server";

import { Client } from "@mirinhq/admin";

export const client = new Client({
  apiKey: process.env["MIRIN_EMBED_PRIVATE_KEY"],
});

Creating Documents

Before we can actually launch the editor, we need to first have some Documents that we can edit.

Let’s now create a simple Dashboard page where we can:-

  1. List all Documents associated with our Embed
  2. Let our user to create a new Document

To do that, we’ll create 2 server actions respectively:

lib/server.tsx
'use server';

import { Client } from '@mirinhq/admin';
import { revalidatePath } from 'next/cache';

const client = new Client({ ... });

export const getDocuments = async () => {
  return await client.getDocuments();
};

export const createDocument = async (
  ...args: Parameters<typeof client.createDocument>
) => {
  await client.createDocument(...args);
  revalidatePath('/');
};

For our frontend, we’ll just create a really simple UI to list our user documents and provide a button to create a new document:

import { getDocuments } from '@/lib/server';
import Link from 'next/link';

import { CreateDocumentButton } from './CreateDocumentButton';

export default async function Dashboard() {
  const { documents } = await getDocuments();

  return (
    <div className="flex flex-col gap-5 p-10">
      <div className="flex items-center gap-5">
        <h2 className="text-xl">Documents</h2>
        <CreateDocumentButton />
      </div>
      {documents.length === 0 ? (
        <p>No documents</p>
      ) : (
        <ul className="flex flex-col">
          {documents.map((document) => {
            return (
              <li className="w-full" key={document.id}>
                <Link href={`/editor/${document.id}`}>{document.id}</Link>
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
}

In a real app, you would ideally only want to list documents that is associated with a specific user. Check out the Documents page for more information.

Setup the editor

Onto the most exciting part - actually integrating the editor.

The editor requires a valid session token in order to authenticate the user and to fetch the correct user document to display in the editor.

We can generate this token with our admin Client - so let’s create a new server action to do just that:

lib/server.tsx
'use server';

import { Client } from '@mirinhq/admin';
import { revalidatePath } from 'next/cache';

const client = new Client({ ... });

export const authenticate = async (documentId: string) => {
  const res = await client.authorize({
    documentId,
  });

  return res.token;
};

Here, we’re simply taking a documentId argument which our page on the frontend will pass, and we’re generating a valid token that effectively grants the user access to the edit the document on our Editor.

In a real app, you probably want to add additional logic here - e.g. to check if your user has the correct permissions to edit a given document.

The client.authorize() method accepts a context object, by default - this object is used to specify the documentId to grant the user access to edit.

Finally, let’s create a new page in our project where we’ll render our editor.

app/editor/[documentId]/page.tsx

'use client';

import * as React from 'react';
import { useParams } from 'next/navigation';

import { Editor } from '@mirinhq/embed';
import '@mirinhq/embed/dist/index.css';

import { authenticate } from '@/lib/server';

export default function EditorPage() {
  const { documentId } = useParams<{ documentId: string }>();

  const  handleOnAuthenticate = async function () {
    // 1. Get the token from our server action via client.authorize()
    const token = await authenticate(documentId);

    // 2: Return the token and documentId to the editor
    return {
      token,
      documentId,
    };
  }

  return (
    <Editor authenticate={handleOnAuthenticate} />
  );
}

That’s it, really 🎉

Just like that - you now have a fully functioning visual editor.