React x Rust
A React frontend talking to a Rust/axum backend that records audit events via
sdk-rust and serves the React bundle on the same
port. Comes in single-tenant and multi-tenant variants.
Source: https://github.com/everscribe/examples/tree/main/react-fe-with-rust-be
Stack
| Layer | What it is |
|---|---|
| Frontend | React 18, Vite, @everscribe/components-react |
| Backend | Rust (axum + tokio), everscribe (path = "../../../sdk-rust", features = ["axum"]), tower-http ServeDir for static |
| Domain | In-memory secrets vault |
| Audit panel | <AuditTrail /> mounted under the vault UI |
Run it
git clone https://github.com/everscribe/examples
cd examples/react-fe-with-rust-be/single-tenant # or .../multi-tenant
cp .env.example .env # EVERSCRIBE_PROJECT_ID + EVERSCRIBE_API_KEY
(cd frontend && npm install && npm run build)
cargo run
Server on :9000 serves the React bundle and the API. The SDK is a path
dependency on the sibling repo (everscribe = { path = "../../../sdk-rust" }).
Backend integration
Construct the recorder and minter once at boot, then add the middleware layer:
let es = everscribe::new(project_id, api_key)?;
let rec = es.new_recorder(Default::default());
let minter = es.new_minter(Default::default());
// EverscribeLayer installs a per-request event (actor + origin) and
// auto-records it on response finish when the handler set an action.
let api = Router::new()
.route("/secrets/:id/reveal", post(reveal))
// ...
.layer(EverscribeLayer::new(rec, resolve_actor))
.with_state(state);
Recording from a handler — enrich CurrentEvent; the layer records on finish:
use everscribe::axum::CurrentEvent;
use everscribe::event::Target;
async fn reveal(State(st): State<AppState>, ev: CurrentEvent, Path(id): Path<String>) -> Response {
ev.with(|e| e.target = Target::new("secret", &id));
let Some(s) = st.vault.lock().unwrap().secrets.get(&id).cloned() else {
return (StatusCode::NOT_FOUND, "not found").into_response();
};
ev.with(|e| { e.action = "secret.reveal".into(); e.with_field("name", &s.name); });
Json(json!({ "value": s.value })).into_response()
}
record is buffered and non-blocking (a background tokio task does the HTTP
flush). The middleware fills result from the response status, so success and
error paths both record correctly.
Minting an embed token (multi-tenant customer view):
use everscribe::minter::TokenOptions;
use std::time::Duration;
async fn embed_token_customer(State(st): State<AppState>, ev: CurrentEvent) -> Response {
let me = st.actor_user(&ev).unwrap();
let opts = TokenOptions {
tenant_id: me.tenant_id.clone(),
expires_in: Duration::from_secs(60 * 60),
allowed_columns: Some(vec!["occurred_at".into(), "action".into(), "actor".into()]),
..Default::default()
};
match st.minter.mint_token(&opts).await {
Ok(token) => Json(json!({ "token": token })).into_response(),
Err(_) => (StatusCode::BAD_GATEWAY, "mint failed").into_response(),
}
}
The admin view drops tenant_id so the same component renders every event.
Frontend integration
Same one-liner as React + Go:
import { AuditTrail } from "@everscribe/components-react";
import "@everscribe/components-styles/default.css";
<AuditTrail tokenEndpoint="/api/embed-token" />
For the multi-tenant customer view, attach the demo auth header via
onTokenExpired:
<AuditTrail
onTokenExpired={async () => {
const res = await fetch("/api/embed-token/customer", {
method: "POST",
credentials: "include",
headers: { "X-Demo-Actor": currentUser.id },
});
return (await res.json()).token;
}}
/>
Single-Tenant vs Multi-Tenant
| Single-tenant | Multi-tenant | |
|---|---|---|
| Token endpoint | One. /api/embed-token |
Two. /api/embed-token/customer, /api/embed-token/admin |
| UI | One vault view | Tab strip: Customer view · Admin view |
| Seeded data | One implicit tenant | Acme, Initech |
| Audit-panel scope | Whole project | Customer: tenant-scoped · Admin: unscoped |
What to take away
record(...).awaitonly enqueues — a background tokio task does the network flush, so it never blocks the request path.- Construct one recorder at boot and share it;
close().awaiton shutdown to drain. - Frontend code is identical to the Go/Node/Python-backend variants.