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.
textBrowser → /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
textGOOD: /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 });
18. Recommended metrics
Expose Prometheus metrics from your Wakapp:
textwakapp_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
staleTimevalues - Correct pagination (limit ≤ 100)
-
x-trace-idpropagated across all API calls
Going further
- Headers & Rate limits: exponential backoff in detail
- Common pitfalls: diagnosing performance issues