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 handed to every handler that needs to record:

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()

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

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

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

The defer + buffered recorder pattern means 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", {
            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. defer rec.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