Status: implemented application behavior
Scope: current codebase, routes, access rights, persistence model, and public/private boundaries
Purpose: this document is the refactor baseline. It describes what the app does now, not the broader target-state architecture described elsewhere.
SDMX Surfer is a Next.js App Router application for building SDMX dashboards through chat.
At a high level, the app combines:
- a protected builder UI
- a server-side chat route that talks to an MCP gateway
- a dashboard rendering library (
sdmx-dashboard-components) - database-backed session persistence
- invite-only authentication
- optional publication of dashboards to a public gallery
This repository contains the production app, not only a prototype shell. Some older architecture documents still describe planned features or earlier states. This document is the source of truth for the currently implemented behavior.
/— authenticated home page showing recent sessions and entry points/builder— authenticated chat + live preview editor/dashboard/[id]— authenticated presentation view for a private session/p/[id]— public presentation view for a published dashboard/gallery— public listing of published dashboards/exploreand/explore/[id]— authenticated dataset/dataflow exploration pages/settings— authenticated user settings and BYOK key management/admin— authenticated admin page
/api/chat— authenticated agent loop/api/sessionsand/api/sessions/[id]— authenticated session CRUD, owner-scoped/api/sessions/[id]/publish— authenticated owner publish/unpublish endpoint/api/public/dashboardsand/api/public/dashboards/[id]— public read-only published dashboard endpoints/api/admin/*— authenticated admin-only operational endpoints/api/auth/*— NextAuth endpoints
- MCP gateway for SDMX discovery/query tooling
sdmx-dashboard-componentsfor dashboard rendering- NextAuth with email magic links for authentication
- Postgres via Drizzle for persistence
The route model is simple but important. Some pages look very similar in the UI while having different security semantics.
These routes are intentionally reachable without authentication:
/login/gallery/p/[id]/api/public/dashboards/api/public/dashboards/[id]/api/auth/*
This is enforced in proxy.ts, which excludes gallery, p(...), and api/public from auth middleware.
These require a signed-in user:
//builder/dashboard/[id]/explore/explore/[id]/settings/api/chat/api/sessions/api/sessions/[id]/api/sessions/[id]/publish
These require both authentication and session.user.role === "admin":
/adminin practice/api/admin/invites/api/admin/users/api/admin/published-dashboards
The admin restriction is enforced in the route handlers, not by middleware matcher rules alone.
/builder is the main product surface.
The builder combines:
- a chat panel
- a dashboard preview / JSON editing panel
- autosaved session state
- model selection and BYOK support
- publish/unpublish controls
The builder persists:
- chat messages
- dashboard config history
- current config pointer
- session title
- publish metadata
The builder is the only place where dashboards are authored or edited.
/dashboard/[id] is a private presentation view backed by the owner’s session.
It is not public sharing.
It loads session data through authenticated, owner-scoped session APIs. A user can open it for their own session and export or continue editing from there.
Publishing is an explicit state on a session.
When a user publishes from the builder:
- the session keeps its private editor/chat state internally
published_atis set- public metadata is stored on the session row
- the session becomes visible through
/p/[id] - the session can appear in
/gallery
The public view is read-only and deliberately narrower than the private session view.
A public dashboard can be used as a starting point for a new private builder session.
Flow:
- user opens
/p/[id] - user clicks "Explore this data"
- if unauthenticated, they go through login
- they land on
/builder?fork=[id]&new=1 - the builder creates a new private session seeded from the public dashboard config
This is a copy/fork workflow, not collaborative editing on the published dashboard itself.
The main durable application object is dashboard_sessions.
Each row stores:
iduser_idtitlemessagesconfig_historyconfig_pointercreated_atupdated_atdeleted_atpublic_titlepublic_descriptionauthor_display_namepublished_at
messagescontains the conversational history used by the builderconfig_historystores dashboard revisionsconfig_pointerselects the currently active configdeleted_atimplements soft-deletepublished_atindicates whether the session is currently public
Public dashboards are not stored in a separate table. A published dashboard is still the same underlying session; publication is a state transition on that session.
That keeps authoring and publication linked, but it also means refactors must preserve the difference between:
- private session data
- public dashboard projection of that session
This is the most important architecture boundary in the app.
Private session APIs and pages can access:
- chat history
- full config history
- current editing state
- owner-specific controls
Examples:
/builder/dashboard/[id]/api/sessions/[id]
Public published-dashboard APIs intentionally return a reduced projection:
id- public-facing title
- public description
- compiled/current dashboard config
- public author display name
- publication timestamp
They do not expose:
- chat messages
- full edit history
- owner email
- BYOK data
- session internals beyond the active config
The current public detail API is GET /api/public/dashboards/[id]. It only serves dashboards where:
published_at IS NOT NULLdeleted_at IS NULL
This projection boundary must be preserved in any cleanup or component extraction work.
Authentication is invite-only and email-link based.
- NextAuth v4
- EmailProvider / magic-link flow
- allowlist check against
allowed_emails - session enriched with:
userIdrole
- ordinary authenticated routes require a session
- session CRUD is owner-scoped by
dashboard_sessions.user_id - admin routes check
role === "admin" - public routes do not require auth, but only expose published data
Authentication answers "who is this user?"
Authorization is handled per route and is where the important ownership/admin/public rules live. Any refactor that centralizes page shells or shared loaders must preserve those checks.
The app generates dashboards through a server-side chat loop.
- builder sends chat messages to
/api/chat - server authenticates the user
- server resolves the model to use
- server creates a per-request MCP client
- LLM can call MCP tools plus the synthetic
update_dashboardtool - the app compiles authoring-schema output into native dashboard config
- the builder extracts the latest dashboard config from tool output and renders it
The app does not require the model to emit raw sdmx-dashboard-components config for every case.
Instead, it prefers a simpler authoring schema with intent visuals such as:
kpichartmapnote
The server compiles this into the native rendering-library config. Native passthrough still exists for advanced cases.
This matters for maintainability because there are effectively two config layers:
- LLM-facing authoring contract
- runtime-facing dashboard-library contract
Three surfaces render dashboards from config:
- builder preview
- private
/dashboard/[id]page - public
/p/[id]page
They are visually similar, but they are not equivalent.
- interactive editing context
- tabbed preview/JSON editor
- error capture and repair loop
- autosave-oriented
- authenticated
- session-backed
- includes edit/export path
- public
- published dashboards only
- no chat or edit controls
- includes export and fork/explore entry point
Any shared component extraction should treat these as related renderers with different access and control semantics, not as identical pages.
Publication is session-scoped.
Publication currently stores:
published_atpublic_titlepublic_descriptionauthor_display_name
This metadata lives on the session row and is used by the public API and gallery.
/gallery is a public listing of published dashboards.
Its backing API:
- reads only sessions where
published_at IS NOT NULL - excludes soft-deleted sessions
- returns public summary fields only
Admins can inspect published dashboards through admin APIs and can unpublish them. That admin view is intentionally broader and includes internal owner information needed for moderation.
Before general cleanup or deduplication work, these invariants should be treated as non-negotiable:
/p/[id]is public and must stay public/dashboard/[id]is private and owner-scoped- public APIs must never expose chat history
- admin routes are broader than owner routes
- publication is a state on
dashboard_sessions, not a separate entity - the builder remains the only editing surface
app/dashboard/[id]/page.tsxandapp/p/[id]/page.tsxare structurally similar but differ in access rights and controlscomponents/dashboard-preview.tsxcarries many concerns in one componentapp/builder/page.tsxholds significant orchestration state- older docs do not cleanly distinguish target-state architecture from implemented behavior
These are good refactor candidates, but only after the route/access model above is treated as fixed behavior to preserve.
This document is intentionally narrower than the other architecture files.
dashboard-architecture.mddescribes the broader product architecture and target designdocs/technical-reference.mddescribes technical internals in more detail, but parts of it reflect earlier states or planned behavior- this file describes the implemented app behavior that refactors must preserve
When the three documents disagree, treat this file as the source of truth for current route semantics, persistence shape, and access rights.