React x Python
A React frontend talking to a Python/FastAPI backend that records audit events via sdk-python 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-python-be
Stack
| Layer | What it is |
|---|---|
| Frontend | React 18, Vite, @everscribe/components-react |
| Backend | Python 3.10+, FastAPI, uvicorn, everscribe (Recorder + Minter + ASGI middleware), serves frontend/dist |
| 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-python-be/single-tenant # or .../multi-tenant
cp .env.example .env # EVERSCRIBE_PROJECT_ID + EVERSCRIBE_API_KEY
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
(cd frontend && npm install && npm run build)
python backend/app.py
Server on :9000 serves the React bundle and the API. requirements.txt installs the SDK editable from the sibling repo (-e ../../../sdk-python).
Backend integration
Construct the recorder and minter once at boot, then mount the ASGI middleware:
import everscribe
from everscribe.asgi import EverscribeMiddleware
es = everscribe.new(project_id, api_key)
rec = es.new_recorder(flush_interval=2.0)
minter = es.new_minter()
# The middleware installs a per-request event (actor + origin) and
# auto-records it on response finish when the handler set an action.
app.add_middleware(EverscribeMiddleware, recorder=rec, resolve_actor=resolve_actor)
Recording from a route handler (the reveal verb) — enrich current_event(); the middleware records after the response is sent:
from everscribe.asgi import current_event
from everscribe.event import Target
@app.post("/api/secrets/{secret_id}/reveal")
async def reveal(secret_id: str, request: Request):
e = current_event()
e.target = Target(type="secret", id=secret_id)
s = vault.get(secret_id)
if s is None:
return PlainTextResponse("not found", status_code=404) # no action set -> no event
e.action = "secret.reveal"
e.with_field("name", s.name)
return JSONResponse({"value": s.value})
record() is buffered and non-blocking (a background thread does the HTTP flush), so it's safe to call from async def handlers. The middleware fills result from the response status, so success and error paths both record correctly.
Minting an embed token (multi-tenant customer view):
from everscribe.minter import TokenOptions
@app.post("/api/embed-token/customer")
async def embed_token_customer(request: Request):
me = actor(request)
token = minter.mint_token(TokenOptions(
tenant_id=me.tenant_id,
expires_in=60 * 60, # seconds
allowed_columns=["occurred_at", "action", "actor", "target"],
))
return JSONResponse({"token": token})
The admin view drops tenant_id so the same component renders every event in the project.
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()enqueues and returns immediately. A background thread does the network flush, so calling it from anasync defhandler never blocks the event loop.- The Recorder is one object per process. Construct once at boot, reuse across every request, and
close()it on shutdown (from a FastAPI lifespan hook). - Frontend code is identical to the Go- and Node-backend variants. Backend choice doesn't touch the UI.