Sprinter Platform

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

RouteDescription
/Dashboard -- KPI cards, activity charts, entity distribution
/[typeSlug]Entity list -- paginated, sortable, searchable
/[typeSlug]/[id]Entity detail -- fields, relations, documents, audit trail
/chatAI chat -- conversation history, URL-addressable sessions
/agentsAgent management
/tasksTask list
/adminAdmin panel -- entity types, users, tenant settings

Dashboard

The dashboard is built from five focused components under apps/web/src/components/dashboard/:

ComponentWhat it renders
KpiCardsTotal Records, Activity (last 10), Open Tasks, Entity Types
ActivityTimelineChartRecharts area chart -- 30-day event count series with gradient fill
EntityDistributionChartRecharts horizontal bar chart -- record count per entity type
RecentActivityFeedLast 10 activity records with action color dots and relative timestamps
QuickActionsLink 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

ParamDefaultDescription
page1Current page number (1-indexed)
pageSize25Rows per page -- 10, 25, 50, or 100
sortcreated_atSort column
orderdescSort 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 active

Implementation details:

  • window.history.replaceState is used for URL updates rather than router.replace to avoid remounting the chat panel (which would interrupt a live stream)
  • A popstate event listener restores the active chat when the user navigates with browser history
  • The initial chat ID is read from useSearchParams on 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/.

ComponentFileUse case
MultiComboboxmulti-combobox.tsxMulti-select with badge pills and optional free-text entry
SingleComboboxmulti-combobox.tsxSingle-select with deselect on re-click
EntityTypePickerentity-type-picker.tsxFetches 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 / FormFieldPicker used
Agent dialog -- Configuration tabCustom toolsMultiCombobox (live from /api/tools)
Agent dialog -- Configuration tabInput guardrailsMultiCombobox (preset options + allowCustom)
Agent dialog -- Configuration tabOutput guardrailsMultiCombobox (preset options + allowCustom)
Agent dialog -- Configuration tabVoice IDSingleCombobox (provider-grouped OpenAI voices)
Agent dialog -- Scoring tabScorersMultiCombobox (11 built-in scorer options)
Source create dialogTarget entity typeEntityTypePicker
Navigation node settingsEntity type slugEntityTypePicker
Navigation node settingsSort fieldSelect dropdown
View create pageEntity typeEntityTypePicker (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.

FileResponsibility
prompt-input.tsxCoordinator + re-export barrel (524 lines)
prompt-input-context.tsxReact contexts for attachments, sources, and text input
prompt-input-helpers.tsPure utilities: convertBlobUrlToDataUrl
prompt-input-layout.tsxInputGroup wrappers and container components
prompt-input-textarea.tsxTextarea with auto-resize and keyboard handling
prompt-input-actions.tsxToolbar action buttons (attach, send, stop)
prompt-input-widgets.tsxFile attachment chips and referenced source chips

API Routes

The Hub exposes 93 API routes. Key routes relevant to the features above:

RouteMethodDescription
/api/entitiesGETList entities with sort, order, search, limit, offset
/api/entitiesPOSTCreate entity (resolves entity_type_id from slug)
/api/entities/[id]GET, PATCH, DELETEEntity CRUD
/api/chatsGET, POSTList or create conversations
/api/chats/[id]GETFetch a single conversation (used by chat URL sync)
/api/chatPOSTAI chat stream route (AI SDK streamText)
/api/toolsGETList tools (used by agent dialog custom tools picker)
/api/entity-typesGETList 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 in apps/web/src/components/
  • Reusable logic goes in packages/ -- never in apps/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.