WakaStart
Security

Best Practices — Performance & Observability

Cache, proactive refresh, pagination, TanStack Query, Prometheus metrics, and structured logging.

Version v1.03 min read

Best Practices — Performance & Observability

This chapter covers performance, caching, and observability patterns. Read Security best practices alongside it.


Performance

9. Cache /me — a single call at login

typescript
// GOOD: store in a React context, never re-fetch on navigation const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); useEffect(() => { // Called only once when the provider mounts (= at login) fetchMe().then(setUser); }, []); return <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>; }; // BAD: re-fetch /me on every page/component const UserPage = () => { const { data: user } = useQuery({ queryKey: ["me"], queryFn: fetchMe }); // refetch on every mount };

10. Proactive refresh before expiration

typescript
// GOOD: check exp - now < 60s and refresh before expiration const REFRESH_THRESHOLD = 60; // seconds function scheduleTokenRefresh(expiresIn: number) { const refreshIn = Math.max(0, expiresIn - REFRESH_THRESHOLD) * 1000; setTimeout(async () => { await refreshTokens(); }, refreshIn); } // BAD: refresh after a 401 (the call has already failed, degraded UX) catch (err) { if (err.status === 401) await refreshTokens(); // too late }

11. Pagination — never load everything

typescript
// GOOD: paginate, max limit = 100 const { data } = await apiGet("config/users", { page: "1", limit: "50" }); // BAD: try to load everything const { data } = await apiGet("config/users", { page: "1", limit: "10000" }); // timeout

12. TanStack Query — staleTime suited to criticality

typescript
// Data that rarely changes (branding, runtime-config): 5 minutes useQuery({ queryKey: ["runtime-config"], queryFn: fetchRuntimeConfig, staleTime: 5 * 60 * 1000, }); // Data that changes often (users, invitations): 30s useQuery({ queryKey: ["users", page], queryFn: () => fetchUsers(page), staleTime: 30_000, });

Architecture

13. Server-side proxy for all API calls

The browser must never call the Public API directly.

text
Browser → /api/proxy/[...path] (Next.js route) → Public API └── reads HttpOnly cookies └── adds Authorization + x-enriched-token └── forwards the response

14. Separate Discovery and API

typescript
// GOOD: Discovery used only once, before auth const { keycloakUrl } = await discoverStartLogin({ host }); window.location.assign(keycloakUrl); // BAD: calling Discovery after every login or on every page useEffect(() => { fetchDiscovery(email); // pointless after auth }, []);

15. Respect the hierarchy in your data model

typescript
// Your entities should reflect the Wakastart hierarchy interface AppContext { partnerId: string; // pid from the wakaToken networkId: string; // nid customerId: string; // cid appId: string; // your app (WID) }

16. Use WIDs in URLs

text
GOOD: /users/WKST05 (readable, stable) BAD : /users/550e8400-... (internal UUID)

Observability

17. Propagate x-trace-id

typescript
// Include the trace ID in all your API requests const traceId = crypto.randomUUID(); headers["x-trace-id"] = traceId; // Log on the frontend side with the same ID logger.info("API call", { traceId, url, method });

Expose Prometheus metrics from your Wakapp:

text
wakapp_api_calls_total{endpoint, status} wakapp_token_refresh_total{success} wakapp_invitations_created_total{customer} wakapp_invitations_accepted_total{customer} wakapp_auth_flow_duration_seconds (histogram)

19. Structured logging

typescript
// GOOD: structured JSON, parsable by Loki / CloudWatch logger.info({ event: "user_login_success", userId: user.wid, customerId: user.customer.wid, adminLevel: user.adminLevel, duration_ms: Date.now() - startTime, }); // BAD: free text that cannot be filtered console.log(`User ${email} logged in successfully in ${elapsed}ms`);

Pre-production performance checklist

  • Proactive refresh implemented (60s threshold)
  • Rate limit handled (exponential backoff)
  • /me loaded once, cached, invalidated on refresh
  • runtime-config loaded once at startup
  • TanStack Query configured with suitable staleTime values
  • Correct pagination (limit ≤ 100)
  • x-trace-id propagated across all API calls

Going further