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 an async def handler 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.

What next