Go 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:

m := es.NewMinter()

The returned token from MintToken is a JWT. Hand it to your frontend over 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.

func tokenHandler(m *minter.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if _, err := authenticate(r); err != nil {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }

        token, err := m.MintToken(r.Context(), minter.TokenOptions{
            // No TenantID. The project has one customer (or is
            // internal), so the token doesn't need to filter by tenant.
            ExpiresIn:      time.Hour,
            AllowedColumns: []string{"occurred_at", "action", "actor", "target"},
        })
        if err != nil {
            http.Error(w, "mint failed", http.StatusInternalServerError)
            return
        }
        // Never let a proxy or CDN cache a per-user token.
        w.Header().Set("Cache-Control", "no-store")
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"token": token})
    }
}

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.

func tokenHandler(m *minter.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        user, err := authenticate(r)
        if err != nil {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }

        token, err := m.MintToken(r.Context(), minter.TokenOptions{
            // 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:      time.Hour,
            AllowedColumns: []string{"occurred_at", "action", "actor", "target"},
            AllowedActions: []string{"user.*", "billing.invoice.created"},
        })
        if err != nil {
            http.Error(w, "mint failed", http.StatusInternalServerError)
            return
        }
        // Never let a proxy or CDN cache a per-user token.
        w.Header().Set("Cache-Control", "no-store")
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"token": token})
    }
}

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

tokenHandler returns an http.HandlerFunc, and defining one registers nothing. Until a router knows the path, the embed component's fetch gets a 404 and the whole embed is dead. Register it at boot, alongside the rest of your routes:

m := es.NewMinter()

mux := http.NewServeMux()
mux.HandleFunc("POST /everscribe/embed-token", tokenHandler(m))

Same handler, other routers:

// chi
r.Post("/everscribe/embed-token", tokenHandler(m))

// gorilla/mux
r.HandleFunc("/everscribe/embed-token", tokenHandler(m)).Methods("POST")

// gin
r.POST("/everscribe/embed-token", gin.WrapF(tokenHandler(m)))

// echo v4
e.POST("/everscribe/embed-token", echo.WrapHandler(tokenHandler(m)))

// fiber v3
app.Post("/everscribe/embed-token", adaptor.HTTPHandlerFunc(tokenHandler(m)))

chi and gorilla/mux need no bridge. Both take an http.HandlerFunc directly, the same way net/http does. gin, echo, and fiber each have their own handler type, so they need their stdlib bridge: gin.WrapF, echo.WrapHandler (an http.HandlerFunc is an http.Handler), and adaptor.HTTPHandlerFunc from github.com/gofiber/fiber/v3/middleware/adaptor.

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. tokenHandler 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 time.Duration Rejected client-side outside [MinExpiresIn, MaxExpiresIn], before any HTTP call. Zero uses the server default (1h).
AllowedColumns []string Optional whitelist of Event JSON field names (snake_case). Nil for no restriction; empty slice rejected.
AllowedActions []string Exact match or suffix wildcard (user.*). Nil for no restriction; empty slice rejected.
AllowedFields []string Restricts which catalog fields the token's DSL and NLP queries may reference. Nil for no restriction; empty slice rejected. Entries are validated server-side.
AllowDSLInput bool Unlocks the Query (advanced DSL) tab in the embed and ?q= on the read API. Default false.
AllowNLP bool Unlocks the AI ("Ask in plain English") tab and the NLP endpoint. Default false.

The package exports minter.MinExpiresIn (60s) and minter.MaxExpiresIn (24h) if you want to validate a lifetime before calling, plus minter.Error for inspecting a non-2xx mint response with errors.As.