Chapter 62: Application 4: Admin Table
The fourth application exercises boundary inheritance at list scale.
An admin table — a paginated, sortable, filterable list of users (or any other entity) with per-row actions — is one of the most common application shapes. Every row is its own entity context. Every action — view, edit, delete, suspend, impersonate — needs to know which entity it’s acting on. The boundary system from Chapter 41 is what makes the architecture handle this cleanly.
The admin table also exercises permissions (some users can suspend; only super-admins can impersonate), audit logging (every privileged action is recorded), and URL state sync (the current sort, filter, and page should be reflected in the URL for sharing and reload-restoration).
The Product
Section titled “The Product”An Admin: Users table showing:
- A paginated list of users (50 per page).
- Columns: email, display name, role, last seen, status.
- Sortable columns (clicking a column header sorts; clicking again reverses).
- A filter bar (filter by role, status, search by email/name).
- Per-row actions (view, edit, suspend, impersonate).
- URL state synchronization for sort, filter, and page.
- Audit logging for privileged actions.
The Boundary Structure
Section titled “The Boundary Structure”Each row is its own boundary. Each action button inside the row inherits the row’s entity context:
<kit-shell name="admin-app"> <kit-boundary surface="admin" feature="users" mode="admin"> <kit-boundary surface="users-list"> <header> <h1>Users</h1> <form data-meta-event="users.filter_changed"> <kit-text-field name="search" placeholder="Search by email or name"></kit-text-field> <kit-select name="role"> <option value="">All roles</option> <option value="user">User</option> <option value="admin">Admin</option> </kit-select> <kit-select name="status"> <option value="">All statuses</option> <option value="active">Active</option> <option value="suspended">Suspended</option> </kit-select> </form> </header>
<table> <thead> <tr> <th><button data-meta-event="users.sort_requested" data-meta-prop-field="email">Email</button></th> <th><button data-meta-event="users.sort_requested" data-meta-prop-field="displayName">Name</button></th> <th><button data-meta-event="users.sort_requested" data-meta-prop-field="role">Role</button></th> <th><button data-meta-event="users.sort_requested" data-meta-prop-field="lastSeen">Last seen</button></th> <th>Actions</th> </tr> </thead> <tbody id="rows"></tbody> </table>
<nav> <kit-button meta-command="users.page_previous">Previous</kit-button> <span id="page-indicator">Page 1 of 12</span> <kit-button meta-command="users.page_next">Next</kit-button> </nav> </kit-boundary> </kit-boundary></kit-shell>The rows are rendered into the #rows container. Each row is a <kit-boundary> with the user’s entity context:
<tr> <kit-boundary surface="user-row" entity-type="user" entity-id="user_123" > <td>jane@example.com</td> <td>Jane Doe</td> <td>admin</td> <td>2 hours ago</td> <td> <kit-button meta-event="user.view_requested">View</kit-button> <kit-button meta-event="user.edit_requested">Edit</kit-button> <kit-button meta-event="user.suspend_requested" meta-intent="destructive">Suspend</kit-button> <kit-button meta-event="user.impersonate_requested" meta-intent="destructive">Impersonate</kit-button> </td> </kit-boundary></tr>When the user clicks Suspend on Jane’s row, the event fires with the full context:
{ type: 'user.suspend_requested', context: { surfaces: ['admin', 'users-list', 'user-row'], surface: 'user-row', feature: 'users', mode: 'admin', entity: { type: 'user', id: 'user_123' } }}The button didn’t know about Jane. The row boundary supplied her ID. Every action on every row produces an event with the correct user attribution.
The Modules
Section titled “The Modules”Users repository — provides the UserRepository interface with list(query), findById, save, suspend, unsuspend. The implementation handles the server’s pagination, sorting, and filtering parameters.
Users module — owns the table’s state (current sort, filter, page, loaded users). Observes filter and sort events, dispatches the appropriate fetch command, updates the table when results arrive.
Permissions module — checks whether the current user can perform the requested action on the target entity. Observes user.suspend_requested, user.impersonate_requested, etc. If the user lacks permission, emits permission.denied and prevents the action.
Audit module — records audit entries for every privileged action. The audit module is unsampled (every action recorded) and writes to a separate backend than analytics.
URL state module — observes users.sort_requested and users.filter_changed, updates the URL via history.replaceState. On page load, reads the URL parameters and applies them.
The module set is bigger than the previous applications because the table has more concerns. The architecture handles the composition through the same event/command pattern.
The Sort Flow
Section titled “The Sort Flow”A user clicks the Email column header. The flow:
- The column header button fires
users.sort_requestedwithmeta-prop-field="email". - The runtime distributes the event.
- The URL state module observes; updates the URL’s
sort=emailparameter. - The users module observes; updates its sort state; dispatches
users.fetchcommand. - The fetch command’s handler calls the users repository.
- On result, the module emits
users.fetchedwith the new data. - The table-renderer module observes
users.fetched; renders the rows; attaches boundaries.
Several modules respond to the same event. The composition is what produces the user-visible behavior. Each module is small.
The Suspend Flow
Section titled “The Suspend Flow”A user clicks Suspend on Jane’s row. The flow:
- The button fires
user.suspend_requestedwith Jane’s entity context. - The runtime distributes the event.
- The permissions module observes. Checks whether the current user (the actor) can suspend a user. If not, emits
permission.deniedand the flow stops. - If allowed: the suspend orchestrator observes. Shows a confirmation dialog. The user confirms. The orchestrator dispatches the
user.suspendcommand. - The handler calls
usersRepository.suspend(user_123). - On success, the orchestrator emits
user.suspended. - The audit module observes
user.suspended. Records the action with full context (admin, target user, timestamp). - The users module observes; refreshes the row’s display.
- The notifications module shows a success toast.
The flow exercises the architecture’s permission check (a sentinel module that can block events), the orchestrator pattern (multi-step flows with user interaction), the audit module (durable record of privileged actions), and the standard analytics/notifications response.
URL State Synchronization
Section titled “URL State Synchronization”The URL state module keeps the URL in sync with the table’s current state:
const urlStateModule = defineKitModule({ name: 'url-state', onStart: ({ runtime }) => { function updateURL(updates: Record<string, string | null>) { const url = new URL(location.href) for (const [key, value] of Object.entries(updates)) { if (value === null || value === '') url.searchParams.delete(key) else url.searchParams.set(key, value) } history.replaceState(null, '', url.toString()) }
runtime.on('users.sort_requested', (event) => { updateURL({ sort: event.payload.field }) })
runtime.on('users.filter_changed', (event) => { updateURL({ search: event.payload.data.search, role: event.payload.data.role, status: event.payload.data.status }) })
runtime.on('users.page_changed', (event) => { updateURL({ page: String(event.payload.page) }) })
// On startup, apply URL state to initial fetch runtime.emit({ type: 'users.url_state_loaded', payload: Object.fromEntries(new URL(location.href).searchParams) }) }})The URL state is the application’s sharable view of the table. A user copying the URL and sending it to a colleague includes the current sort, filter, and page; the colleague opening the URL sees the same view.
The pattern uses history.replaceState (not pushState) — the URL changes don’t create new history entries. The back button takes the user to the previous navigation, not back through each filter change.
Patterns the Application Exercises
Section titled “Patterns the Application Exercises”Per-row boundaries. Each row’s <kit-boundary> supplies the entity context for actions inside. The action buttons are generic; the entity attribution is structural.
Permission gating. The permissions module observes events and can block them. The pattern is the sentinel pattern from Chapter 54, applied to authorization.
URL state synchronization. The application’s table state (sort, filter, page) is reflected in the URL through URLSearchParams and history.replaceState. No client-side router; the URL is just a serialization surface.
Audit-grade logging. The audit module is dedicated infrastructure. It’s separate from analytics. It records every privileged action with full context.
Server-driven pagination. The users repository handles the server’s pagination, sorting, and filtering. The application doesn’t load all users into memory; the server returns the current page.
Bridge to the Prompt Workbench
Section titled “Bridge to the Prompt Workbench”Chapter 63 takes the final application — a mini prompt workbench that exercises AI-generated, streaming, stochastic UI. The workbench sets up Part VIII’s AI-future arguments concretely.
Exercise: Build the Admin Table
Section titled “Exercise: Build the Admin Table”Implement the admin table from this chapter.
The complete application:
- The users repository.
- The users, permissions, audit, URL state, and orchestrator modules.
- The markup with the table and the per-row boundaries.
- The render logic that produces rows with boundaries on each.
Then verify:
- Sort by clicking a column. Watch the URL update.
- Filter by search. Watch the rows update and the URL update.
- Click Suspend on a row. Verify the permission check runs. Verify the audit log records the action.
- Copy the URL while filtered/sorted, paste into a new tab. Verify the same view loads.
- Use the debug overlay. Trace a suspend action end to end.
The admin table is one of the most common application shapes. The architecture handles it cleanly because the per-row boundary pattern makes the entity attribution structural, the permission and audit modules are separately maintainable, and the URL state sync works without a client-side router.