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.