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.