Hub Application
The reference Next.js application that ships the Sprinter Platform as a production multi-tenant workspace.
Overview
The Hub (apps/web/) is the reference implementation of a Sprinter-powered application. It wires the platform packages (@sprinterai/core, @sprinterai/runtime, @sprinterai/supabase) to registry UI blocks and exposes the result as a multi-tenant workspace with:
- A data dashboard with KPI cards and interactive charts
- Entity type browsing with server-side pagination, sort, and search
- AI chat with conversation history and URL-addressable sessions
- Entity detail views with related entities, documents, and audit history
- Agent management, task tracking, and extraction review
- 93 API routes covering the full Amble feature surface
The Hub is pure wiring -- no reusable logic lives here. Every algorithm belongs in a package and every reusable UI block belongs in the registry.
Key Pages
| Route | Description |
|---|---|
/ | Dashboard -- KPI cards, activity charts, entity distribution |
/[typeSlug] | Entity list -- paginated, sortable, searchable |
/[typeSlug]/[id] | Entity detail -- fields, relations, documents, audit trail |
/chat | AI chat -- conversation history, URL-addressable sessions |
/agents | Agent management |
/tasks | Task list |
/admin | Admin panel -- entity types, users, tenant settings |
Dashboard
The dashboard is built from five focused components under apps/web/src/components/dashboard/:
| Component | What it renders |
|---|---|
KpiCards | Total Records, Activity (last 10), Open Tasks, Entity Types |
ActivityTimelineChart | Recharts area chart -- 30-day event count series with gradient fill |
EntityDistributionChart | Recharts horizontal bar chart -- record count per entity type |
RecentActivityFeed | Last 10 activity records with action color dots and relative timestamps |
QuickActions | Link cards for common actions (New Entity, New Chat, etc.) |
The dashboard page (app/(app)/page.tsx) is a Next.js Server Component that fetches data in parallel with Promise.all and passes it to these components as props. The page itself is under 100 lines.
Entity List
The entity list page (/[typeSlug]) supports server-side pagination, sort, and search. All state is encoded in the URL so filtered views are shareable and bookmarkable.
URL Parameters
| Param | Default | Description |
|---|---|---|
page | 1 | Current page number (1-indexed) |
pageSize | 25 | Rows per page -- 10, 25, 50, or 100 |
sort | created_at | Sort column |
order | desc | Sort direction -- asc or desc |
search | (empty) | Case-insensitive filter on title and slug |
Sort Allowlist
The /api/entities route validates the sort parameter against a fixed allowlist to prevent column injection:
const ALLOWED_SORT_COLUMNS = ['created_at', 'updated_at', 'title', 'slug'] as const;Any unrecognized value falls back to created_at.
Client Component
EntityListClient uses useSearchParams + useTransition to update URL params and trigger server-side refetches. An opacity transition (0.6 during pending) gives immediate visual feedback without a skeleton loader.
Chat URL State
The chat page (/chat) syncs the active conversation ID to a ?id= URL query parameter. This makes individual conversations bookmarkable and restores correctly on browser back/forward.
/chat -- no active conversation
/chat?id=abc123 -- conversation abc123 is activeImplementation details:
window.history.replaceStateis used for URL updates rather thanrouter.replaceto avoid remounting the chat panel (which would interrupt a live stream)- A
popstateevent listener restores the active chat when the user navigates with browser history - The initial chat ID is read from
useSearchParamson mount so a bookmarked URL opens the correct conversation
Entity Detail Sidebar
The entity detail view (EntityDetailEnhanced) includes two contextual sidebar sections that render only when data is present:
Documents Section
Shows up to five documents linked to the entity, with title, file type badge, and creation date. A "View more" link navigates to the entity's documents sub-route when more than five exist.
Recent Changes Section
Shows the last five entries from audit_logs for the entity, with the operation name and a relative timestamp (e.g., "3 hours ago"). Uses formatRelativeTime from @sprinterai/core.
Admin Combobox Pickers
Admin dialog forms use searchable combobox pickers instead of raw text inputs for fields that reference slugs or IDs. This eliminates the need for users to look up valid values elsewhere.
Reusable picker components
All three components live in apps/web/src/components/admin/.
| Component | File | Use case |
|---|---|---|
MultiCombobox | multi-combobox.tsx | Multi-select with badge pills and optional free-text entry |
SingleCombobox | multi-combobox.tsx | Single-select with deselect on re-click |
EntityTypePicker | entity-type-picker.tsx | Fetches entity types from /api/entity-types |
MultiCombobox / SingleCombobox props
export interface ComboboxOption {
value: string;
label: string;
description?: string;
}
export interface ComboboxGroup {
heading: string;
options: ComboboxOption[];
}
// MultiCombobox
interface MultiComboboxProps {
options?: ComboboxOption[];
groups?: ComboboxGroup[]; // use instead of options for grouped display
selected: string[];
onSelect: (values: string[]) => void;
placeholder: string;
allowCustom?: boolean; // enables free-text "Add ..." entry
}
// SingleCombobox
interface SingleComboboxProps {
options?: ComboboxOption[];
groups?: ComboboxGroup[];
value: string;
onChange: (value: string) => void;
placeholder: string;
allowCustom?: boolean;
disabled?: boolean;
}EntityTypePicker props
interface EntityTypePickerProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
optional?: boolean; // shows a "None" option that clears the selection
}Fields replaced in each dialog
| Dialog / Form | Field | Picker used |
|---|---|---|
| Agent dialog -- Configuration tab | Custom tools | MultiCombobox (live from /api/tools) |
| Agent dialog -- Configuration tab | Input guardrails | MultiCombobox (preset options + allowCustom) |
| Agent dialog -- Configuration tab | Output guardrails | MultiCombobox (preset options + allowCustom) |
| Agent dialog -- Configuration tab | Voice ID | SingleCombobox (provider-grouped OpenAI voices) |
| Agent dialog -- Scoring tab | Scorers | MultiCombobox (11 built-in scorer options) |
| Source create dialog | Target entity type | EntityTypePicker |
| Navigation node settings | Entity type slug | EntityTypePicker |
| Navigation node settings | Sort field | Select dropdown |
| View create page | Entity type | EntityTypePicker (optional) |
Middleware and API Route Auth
API routes bypass the middleware redirect
All /api/** routes are in the middleware public-route allowlist. Unauthenticated requests to API routes are not redirected to /login -- they reach the route handler directly, which returns { error: "Unauthorized" } with status 401.
const PUBLIC_ROUTE_PREFIXES = [
'/login',
'/signup',
'/forgot-password',
'/reset-password',
'/auth/callback',
'/share',
'/embed',
'/api', // API routes handle auth themselves, returning 401 JSON
] as const;This prevents fetch clients and the AI SDK from receiving an unexpected HTML redirect (302 to /login) when a session expires. Every route handler calls requireAuthGuard as its first action to enforce the 401 contract.
Non-null assertions replaced
Environment variable reads in apps/web/src/middleware.ts and apps/web/src/lib/supabase/server.ts use ?? '' fallbacks instead of ! non-null assertions. This keeps TypeScript satisfied and produces clearer error messages when env vars are missing.
PromptInput Component Architecture
PromptInput is split across seven files under apps/web/src/components/ai-elements/. All types and sub-components are re-exported from prompt-input.tsx so import paths for consumers do not change.
| File | Responsibility |
|---|---|
prompt-input.tsx | Coordinator + re-export barrel (524 lines) |
prompt-input-context.tsx | React contexts for attachments, sources, and text input |
prompt-input-helpers.ts | Pure utilities: convertBlobUrlToDataUrl |
prompt-input-layout.tsx | InputGroup wrappers and container components |
prompt-input-textarea.tsx | Textarea with auto-resize and keyboard handling |
prompt-input-actions.tsx | Toolbar action buttons (attach, send, stop) |
prompt-input-widgets.tsx | File attachment chips and referenced source chips |
API Routes
The Hub exposes 93 API routes. Key routes relevant to the features above:
| Route | Method | Description |
|---|---|---|
/api/entities | GET | List entities with sort, order, search, limit, offset |
/api/entities | POST | Create entity (resolves entity_type_id from slug) |
/api/entities/[id] | GET, PATCH, DELETE | Entity CRUD |
/api/chats | GET, POST | List or create conversations |
/api/chats/[id] | GET | Fetch a single conversation (used by chat URL sync) |
/api/chat | POST | AI chat stream route (AI SDK streamText) |
/api/tools | GET | List tools (used by agent dialog custom tools picker) |
/api/entity-types | GET | List entity types (used by EntityTypePicker) |
All routes are tenant-scoped via requireAuthGuard from @sprinterai/supabase and use handleApiError for consistent error responses.
Component Placement Rules
The Hub follows strict placement rules inherited from the platform:
- Reusable UI goes in
registry/new-york/blocks/-- never inapps/web/src/components/ - Reusable logic goes in
packages/-- never inapps/web/src/lib/ - Hub-specific wiring is the only thing allowed in
apps/web/src/
Dashboard components (kpi-cards.tsx, etc.) live in apps/web/src/components/dashboard/ because they depend on Hub-specific data shapes and are not general-purpose registry blocks.
Related
- Architecture -- package boundaries and dependency graph
- @sprinterai/core -- entity types, store interfaces, builders
- @sprinterai/runtime -- agent execution, chat handler, workflows
- @sprinterai/supabase -- Supabase stores, auth adapter
- Entity System -- entity types, fields, schema definition
- Agent System -- agent definitions and execution model