Rust 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:
let m = es.new_minter(Default::default());
// or, single-surface: everscribe::minter::Client::new(project_id, api_key, Default::default())
mint_token returns a JWT string. Hand it to your frontend via your normal
page-render path. Never expose your project API key to the browser.
Set
Cache-Control: no-storeon 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 answersGET, 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 the project has one customer (or is internal). The token has no
tenant_id, so the embed shows every event in the project.
use everscribe::minter::TokenOptions;
use std::time::Duration;
let token = m.mint_token(&TokenOptions {
expires_in: Duration::from_secs(60 * 60),
allowed_columns: Some(vec!["occurred_at".into(), "action".into(), "actor".into()]),
..Default::default()
}).await?;
Multi-tenant
Use when the project partitions events by tenant_id. Mint a token whose
tenant_id matches the signed-in user's tenant; the server enforces the scope
on every read.
let token = m.mint_token(&TokenOptions {
tenant_id: user.tenant_id.clone(),
expires_in: Duration::from_secs(60 * 60),
allowed_actions: Some(vec!["user.*".into(), "billing.invoice.created".into()]),
..Default::default()
}).await?;
Multi-tenant products typically expose two endpoints:
/everscribe/embed-token/customer (with tenant_id) and
/everscribe/embed-token/admin (without). Same minter, different scoping.
Mounting the route
mint_token is a method call, so nothing above is reachable over HTTP yet.
Defining a handler registers nothing in Rust: the router has to be told the
path, or the embed component's fetch gets a 404 and the whole embed is dead.
minter::Client derives Clone, so it goes straight into axum's State
with no Arc wrapper. Cloning it clones a reqwest::Client, which is itself
a cheap handle onto a shared connection pool. The minter is part of the crate
core, so this needs no Cargo feature; the axum feature gates only the audit
middleware.
use axum::{
extract::State,
http::{header, StatusCode},
response::IntoResponse,
routing::post,
Json, Router,
};
use everscribe::minter::{self, TokenOptions};
use serde_json::json;
use std::time::Duration;
// AuthenticatedUser is your own FromRequestParts extractor, the same one
// the rest of your routes use. It is what makes this endpoint safe to
// expose: the handler decides the caller's scope.
async fn embed_token(
State(m): State<minter::Client>,
user: AuthenticatedUser,
) -> impl IntoResponse {
let opts = TokenOptions {
tenant_id: user.tenant_id.clone(),
expires_in: Duration::from_secs(60 * 60),
allowed_columns: Some(vec!["occurred_at".into(), "action".into(), "actor".into()]),
..Default::default()
};
match m.mint_token(&opts).await {
// Never let a proxy or CDN cache a per-user token.
Ok(token) => (
[(header::CACHE_CONTROL, "no-store")],
Json(json!({ "token": token })),
)
.into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "mint failed").into_response(),
}
}
let app = Router::new()
.route("/everscribe/embed-token", post(embed_token))
.with_state(m);
Drop the tenant_id line for a single-tenant or admin token. Everything else
is identical.
If your app already has its own state type, keep the client inside it and
implement FromRef, and the extractor above compiles unchanged:
use axum::extract::FromRef;
#[derive(Clone)]
struct AppState {
minter: minter::Client,
// ... the rest of your state
}
impl FromRef<AppState> for minter::Client {
fn from_ref(state: &AppState) -> Self {
state.minter.clone()
}
}
For actix-web, put the client in web::Data and register the handler with
.service(...). The mint call itself is unchanged.
Two things about the route itself:
- The path is yours to pick. It only has to match the
tokenEndpointyou hand the embed component./everscribe/embed-tokenis what Everscribe's own tooling assumes, so prefer it unless you have a reason not to. - The verb is
POST. The component POSTstokenEndpointwith no body, so aget(...)-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.
TokenOptions
| Field | Type | Notes |
|---|---|---|
tenant_id |
String |
Scopes reads to a tenant. Trimmed; ≤ 256 chars. Empty = unscoped. |
expires_in |
Duration |
Rejected client-side outside [MIN_EXPIRES_IN, MAX_EXPIRES_IN] (60s to 24h), before any HTTP call. Duration::ZERO = server default (1h). |
allowed_columns |
Option<Vec<String>> |
Whitelist of Event field names. None = no restriction; empty rejected. |
allowed_actions |
Option<Vec<String>> |
Exact or suffix wildcard (user.*). None = no restriction; empty rejected. |
allowed_fields |
Option<Vec<String>> |
Restricts catalog fields the token's DSL/NLP queries may reference. None = no restriction; empty rejected. Validated server-side. |
allow_dsl_input / allow_nlp |
bool |
Unlock the Query / AI tabs and their endpoints. Default false. |
The crate exports MIN_EXPIRES_IN, MAX_EXPIRES_IN, and ALLOWED_COLUMNS,
plus MinterError for inspecting a validation failure or a non-2xx mint
response.