React x Go

A React frontend talking to a Go backend that records audit events via sdk-go and serves the React bundle from a single self-contained binary. Comes in single-tenant and multi-tenant variants.

Source: https://github.com/everscribe/examples/tree/main/react-fe-with-go-be

Stack

Layer What it is
Frontend React 18, Vite, @everscribe/components-react
Backend Go 1.25+, net/http, sdk-go (Recorder + Minter), //go:embed all:web/dist
Domain In-memory secrets vault. Create, reveal, rotate, share, expire, revoke
Audit panel <AuditTrail /> mounted under the vault UI, live-polling on a JWT

Run it

git clone https://github.com/everscribe/examples
cd examples/react-fe-with-go-be/single-tenant   # or .../multi-tenant

cp .env.example .env   # set EVERSCRIBE_PROJECT_ID + EVERSCRIBE_API_KEY
cd web && npm install && npm run build && cd ..

make build && make run

The binary serves the frontend and the API on :8080. Open http://localhost:8080 and start clicking around. Every action records an event you'll see appear in the audit panel within seconds.

Backend integration

The Recorder is constructed once at boot from EVERSCRIBE_PROJECT_ID and EVERSCRIBE_API_KEY, then wired into the audit adapter that wraps every route:

import (
    everscribe "github.com/everscribe/sdk-go"
    "github.com/everscribe/sdk-go/pkg/event"
    "github.com/everscribe/sdk-go/pkg/minter"
    "github.com/everscribe/sdk-go/pkg/recorder"
)

es, err := everscribe.New(cfg.ProjectID, cfg.APIKey)
if err != nil {
    log.Fatalf("everscribe.New: %v", err)
}

rec := es.NewRecorder(
    recorder.WithFlushInterval(2*time.Second),
    recorder.WithSlogLogger(slog.Default()),
)
defer rec.Close()

m := es.NewMinter()

The audit adapter wraps the routes, so handlers never call Record themselves:

auditMW := event.Middleware(event.Options{
    ActorResolver: actorResolver(users),
    Recorder:      rec,
})
handler := withSession(auditMW(apiMux))

Recording an event from a handler (the reveal verb):

func revealSecret(v *vault, users map[string]User) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        actor := actorFor(users, r)
        e := event.Current(r.Context())
        e.Action = "secret.reveal"
        e.Actor = actor
        e.Target = event.Target{Type: "secret", ID: r.PathValue("id")}

        // ... business logic, set e.Result, etc.
    }
}

The audit adapter records the event once the handler returns, backed by the buffered recorder, so recording adds nothing to request latency.

Minting an embed token (multi-tenant customer view. Scoped to one tenant):

func embedTokenCustomer(m *minter.Client, users map[string]User) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        user := userFor(users, r)
        token, err := m.MintToken(r.Context(), minter.TokenOptions{
            TenantID:       user.TenantID,
            ExpiresIn:      time.Hour,
            AllowedColumns: []string{"occurred_at", "action", "actor", "target"},
        })
        if err != nil {
            http.Error(w, "mint failed", http.StatusInternalServerError)
            return
        }
        json.NewEncoder(w).Encode(map[string]string{"token": token})
    }
}

The admin view drops TenantID so the same component renders all events in the project.

Frontend integration

The audit panel is one line:

import { AuditTrail } from "@everscribe/components-react";
import "@everscribe/components-styles/default.css";

<AuditTrail tokenEndpoint="/api/embed-token" />

For the multi-tenant customer view, the demo backend needs the X-Demo-Actor header to identify which tenant to scope to. The component's built-in fetcher only sends credentials: 'include', so the example uses the onTokenExpired escape hatch:

<AuditTrail
    onTokenExpired={async () => {
        const res = await fetch("/api/embed-token/customer", {
            method: "POST",
            credentials: "include",
            headers: { "X-Demo-Actor": currentUser.id },
        });
        const { token } = await res.json();
        return token;
    }}
/>

In a real app you'd use your own auth headers / session cookie instead of X-Demo-Actor.

Single-Tenant vs Multi-Tenant

Single-tenant Multi-tenant
Token endpoint One. /api/embed-token Two. /api/embed-token/customer (tenant-scoped) and /api/embed-token/admin (unscoped)
UI One vault view Tab strip: Customer view · Admin view
Seeded data One implicit tenant Acme (2 users), Initech (2 users)
What changes when you switch users Nothing Customer-view audit panel re-scopes

What to take away

  • The Recorder is a fire-and-forget client. The adapter's automatic record adds zero latency to request handlers.
  • The Minter is small. A handful of token-options fields gives you tenant scoping, column whitelisting, and action filtering.
  • The component is one tag. Everything else (pagination, polling, filters, claim-driven UI) is handled inside.

What next