Node SDK · Minter

Use the minter when your application's frontend wants to display events directly. See Embed Tokens for the full model. Construct once at boot, mint per request:

import * as minter from "@everscribe/sdk-node/minter";

const m = new minter.Client(projectId, apiKey);

The token returned from mintToken is a JWT. Hand it to your frontend via your normal page-render path. Never expose your project API key to the browser.

Set Cache-Control: no-store on every token response. This is defense in depth. The embed component POSTs this endpoint, and a POST response is not cached without an explicit opt-in, so forgetting the header is no longer a leak by default. Set it anyway: a handler that also answers GET, or a CDN or proxy configured to cache POSTs, puts a per-user tenant-scoped token back in a shared cache, which is a cross-tenant leak. The handlers below set it. Keep it if you rewrite them.

Single-tenant

Use when your project has one customer per project, or when you're embedding the UI in your own internal tooling. The token has no tenant_id claim, so the embed shows every event in the project.

app.post("/everscribe/embed-token", async (req, res) => {
    const user = await authenticate(req);
    if (!user) {
        res.status(401).send("unauthorized");
        return;
    }

    try {
        const token = await m.mintToken({
            // No tenantId. The project has one customer (or is
            // internal), so the token doesn't need to filter by tenant.
            expiresIn: 60 * 60 * 1000, // 1 hour, in ms
            allowedColumns: ["occurred_at", "action", "actor", "target"],
        });
        // Never let a proxy or CDN cache a per-user token.
        res.set("Cache-Control", "no-store");
        res.json({ token });
    } catch {
        res.status(500).send("mint failed");
    }
});

Multi-tenant

Use when your project partitions events by tenant_id and each customer should only see their own. Mint a token whose tenantId matches the signed-in user's tenant. The server enforces the scope on every read.

app.post("/everscribe/embed-token", async (req, res) => {
    const user = await authenticate(req);
    if (!user) {
        res.status(401).send("unauthorized");
        return;
    }

    try {
        const token = await m.mintToken({
            // tenantId set to the signed-in user's tenant, so reads are
            // filtered server-side. For an admin view (your internal
            // support tool that should see every tenant), use the
            // same minter but omit tenantId.
            tenantId: user.tenantId,
            expiresIn: 60 * 60 * 1000,
            allowedColumns: ["occurred_at", "action", "actor", "target"],
            allowedActions: ["user.*", "billing.invoice.created"],
        });
        // Never let a proxy or CDN cache a per-user token.
        res.set("Cache-Control", "no-store");
        res.json({ token });
    } catch {
        res.status(500).send("mint failed");
    }
});

Multi-tenant products often expose two endpoints:

Endpoint Token Used for
/everscribe/embed-token/customer tenantId = signed-in user's tenant the end-customer's view of their own audit trail
/everscribe/embed-token/admin tenantId omitted internal admin / support tool, shows every tenant

Same minter, different scoping rules.

Mounting the route

app.post(...) registers the route as a side effect of the module being evaluated, which means it registers only if something evaluates the module. Put the handler in a new file that nothing imports and the route never exists: the embed component's fetch gets a 404 and the whole embed is dead. That failure is silent at build time, which is what makes it worth stating.

If the handler lives in the same file that creates app, the examples above are complete and there is nothing more to do. Otherwise export a Router and mount it:

// routes/everscribe.ts
import { Router } from "express";
import * as minter from "@everscribe/sdk-node/minter";

const m = new minter.Client(
    process.env.EVERSCRIBE_PROJECT_ID!,
    process.env.EVERSCRIBE_API_KEY!,
);

export const embedTokenRouter = Router();

embedTokenRouter.post("/everscribe/embed-token", async (req, res) => {
    // exactly the handler body from above
});
// app.ts
import { embedTokenRouter } from "./routes/everscribe.js";

app.use(embedTokenRouter); // without this line the route does not exist

Fastify and Hono are the same two steps under different names:

// Fastify: export a plugin, register it
export async function embedTokenRoutes(app: FastifyInstance) {
    app.post("/everscribe/embed-token", async (req, reply) => { /* ... */ });
}
app.register(embedTokenRoutes);

// Hono: export a sub-app, route it
export const embedTokenRoutes = new Hono();
embedTokenRoutes.post("/everscribe/embed-token", async (c) => { /* ... */ });
app.route("/", embedTokenRoutes);

Next.js App Router is the one exception. File placement is the registration, so there is no mount step: a POST export from a route.ts is reachable as soon as the file exists. The directory path under app/ is the URL, so put it where you want it served:

// app/everscribe/embed-token/route.ts  ->  POST /everscribe/embed-token
export async function POST() {
    const token = await m.mintToken({ expiresIn: 60 * 60 * 1000 });
    return Response.json({ token });
}

app/api/everscribe/embed-token/route.ts would serve /api/everscribe/embed-token instead. Either is fine as long as the tokenEndpoint you pass the component matches.

Two things about the route itself:

  • The path is yours to pick. It only has to match the tokenEndpoint you hand the embed component. /everscribe/embed-token is what Everscribe's own tooling assumes, so prefer it unless you have a reason not to.
  • The verb is POST. The component POSTs tokenEndpoint with no body, so a GET-only route answers 405 and the embed never gets a token. Minting issues a bearer credential and consumes quota, so it is not a safe idempotent read.

Mount it behind the same authentication as the rest of your app. The handler is what decides the caller's scope, so an unauthenticated mint route hands your whole audit trail to anyone who asks for it.

TokenOptions

Field Type Notes
tenantId string Optional. Scopes reads to events with matching tenant_id. Trimmed; ≤ 256 chars. Omit for unscoped (single-tenant or admin view).
expiresIn number Token lifetime in milliseconds. Rejected client-side outside [MIN_EXPIRES_IN_MS, MAX_EXPIRES_IN_MS], before any HTTP call. Omit or pass 0 for the server default (1h).
allowedColumns string[] Optional whitelist of Event JSON field names (snake_case). Omit for no restriction; empty array rejected.
allowedActions string[] Exact match or suffix wildcard (user.*). Omit for no restriction; empty array rejected.
allowedFields string[] Restricts which catalog fields the token's DSL and NLP queries may reference. Omit for no restriction; empty array rejected. Entries are validated server-side.
allowDSLInput boolean Unlocks the Query (advanced DSL) tab in the embed and ?q= on the read API. Default false.
allowNLP boolean Unlocks the AI ("Ask in plain English") tab and the NLP endpoint. Default false.

The SDK exports MIN_EXPIRES_IN_MS, MAX_EXPIRES_IN_MS, and ALLOWED_COLUMNS constants if you want to validate before calling, plus MinterError for inspecting a non-2xx mint response.