[Guide] Build an Admin Dashboard - implementation notes
The Admin Dashboard guide leans into a "business renderer" posture: instead of hiding policy behind external layers, it merges UI and policy in render so the route tree stays readable and snapshot-friendly. I also liked that it stores derived surfaces (visible sections, table columns, inspector summaries) so the dashboard feels stable even while policy is changing.
If you implemented a business renderer, where did you keep the config table (data, code, JSON), and how did you prevent it from becoming untyped chaos? Did you store derived columns/visibleRowIds, or did you compute them in render and accept the churn? How do you keep policy decisions legible for debugging (especially readonly/hidden behavior)? What did you use as "snapshot-worthy" evidence for the inspector panel?
Comments (18)
Back to latestPolicy-in-render is controversial, but it made audits easier for us.
When policy lives in code you can snapshot, people stop arguing about what the rules are.
The best thing about storing columns is stability.
If your columns are derived per render from policy, the table can "shift" under users when policy updates.
We kept the business renderer config as a typed table with explicit policy hooks:
ts
type SectionId = 'users' | 'billing' | 'flags' | 'audit';
type Mode = 'safe' | 'full';
type SectionDef = {
id: SectionId;
label: string;
columns: string[];
visibleWhen: (ctx: { mode: Mode }) => boolean;
};
export const sections: SectionDef[] = [
{ id: 'users', label: 'Users', columns: ['email', 'role'], visibleWhen: () => true },
{ id: 'billing', label: 'Billing', columns: ['plan', 'status'], visibleWhen: (c) => c.mode === 'full' },
{ id: 'flags', label: 'Flags', columns: ['key', 'enabled'], visibleWhen: () => true },
{ id: 'audit', label: 'Audit', columns: ['action', 'actor'], visibleWhen: () => true },
];
Then a derive step writes visibleSections and columns into the section document.
Counterpoint: merging policy and UI can make refactors painful because policy gets coupled to component structure.
We had to be disciplined about keeping policy functions small and "data-ish".
Agree. We treated policy like view-model logic: it can reference route keys, but it shouldn't reach into component internals.
If policy needs DOM knowledge, it's probably the wrong layer.
Legibility trick: render readonly/hidden decisions as evidence, even if it's just a tooltip or a debug string.
Otherwise it looks like a bug when a field is disabled.
We made the inspector summary a derived key and stored it per row selection.
If you compute it live, you can get mismatches between list selection and inspector detail when selection changes quickly.
A misunderstanding I had: I assumed the business renderer meant dynamic imports and magic.
It's really just: generate route components from a table so the tree stays consistent.
We stored derived visibleRowIds because it stabilized pagination and selection.
If the visible list changes due to policy, you want that change to be explicit and traceable.
We also stored an audit trail of policy decisions as a string array (just for debug builds).
It helped answer "why is this hidden" without reading code in prod.
Docs tie-in: this guide aligns with "make behavior visible" more than anything.
Related: Testing and Debugging and Performance and Rendering (Best Practices).
We used mode=safe|full in global route state like the guide suggests and it made demos easier.
You could share a URL and get the same policy posture instantly.
Counterpoint: if policy is in render, you need to keep it fast or the dashboard gets sluggish.
We memoized policy outputs and stored them in the doc so the render tree isn't doing heavy checks per cell.
Same. We treated "policy outputs" like derived state: compute once, store, render from stored keys.
It made the table feel stable and it kept policy from being a perf wildcard.
If you're worried about coupling, I found that naming helps.
If the policy function is named like a domain rule, you can move UI around without losing the meaning.
The guide made me re-think dashboards as "policy viewers" as much as "data viewers".
Once you accept that, policy-in-render feels like the natural place to put it.
We had issues with generated routes and analytics (hard to name screens).
We fixed it by storing a derived screenName key and rendering it as evidence for events.
Business renderer is a weird phrase, but the outcome is good: route trees you can actually reason about.
The biggest payoff was that policy stopped being "some backend thing" and became a UI contract.
Once it's a contract, you can test it.
Late note: storing derived columns also helps with accessibility because the table headers remain stable.
If columns change unexpectedly, screen reader users get a really confusing experience.