Architecture
Every request to a portal site flows through a single Astro middleware that decides, based on auth state, whether the response is a server-rendered viewer page with a small set of selectively hydrated islands, or a fully hydrated editor shell.
Request flow
Section titled “Request flow”HTTP request │ ▼middleware.ts (packages/core/src/middleware.ts) │ ├─ /api/media/* ──► pass through (public; session resolved but not required) │ ├─ /edit/* or /api/* ──► authenticated? ──► editor path │ │ no │ └──► redirect /edit/login │ ├─ public viewer route ──► authenticated session? ──► editor viewing viewer route │ │ no │ └──► audience cookie valid? ──► viewer path │ │ no │ └──► redirect /login │ └─ public routes (login, callbacks) ──► pass throughViewer path
Section titled “Viewer path”Viewers get server-rendered HTML by default. React section components are rendered server-side in Astro’s SSR pass — non-interactive sections ship no runtime at all, just plain HTML and CSS. The principle is minimal, intentional viewer JS — no editor code on viewer paths, and no hydration for content that doesn’t need it — not literally zero JavaScript. A deliberate, small set of islands do hydrate on the viewer path:
Navigation—client:load(scroll tracking, active-section highlighting, mobile slide-out)- Interactive sections — hydrated individually via
SectionIslandwithclient:visible, gated by a section type’sinteractiveopt-in ChatWidget—client:load- Login forms
- The full-page document-mode viewer at
/present/[...slug]— aclient:onlyisland (the one sanctioned exception to server-rendered viewer sections; pagination depends on client-side text measurement of the real DOM layout)
Everything else — non-interactive sections, layout chrome outside those islands — renders to static HTML with no client:* directive and no React runtime shipped for it.
Editor path
Section titled “Editor path”Editors reach /edit, which renders packages/core/src/pages/edit/index.astro. That page mounts the editor shell with client:load:
<EditorShell headSha={headSha} draftHeadSha={draftHeadSha} siteId={siteId} audiences={audiences} capabilities={capabilities} currentUser={currentUser} client:load/>client:load tells Astro to hydrate the component immediately on page load. The result is a fully interactive React application running in the browser — TipTap inline editing, drag-to-reorder, media library, save-to-GitHub — all driven by the same section component tree used for viewer rendering, but wrapped in editor controls.
Middleware auth check
Section titled “Middleware auth check”packages/core/src/middleware.ts runs on every request. The key branching logic:
/editor/edit/*or/api/*— requires a valid session. Missing session redirects to/edit/login(UI) or returns401(API). Certain/api/auth/*management routes additionally require theownerrole./api/media/*— publicly reachable (URLs are opaque content hashes, not guessable). Editor sessions receive draft-branch media; viewers receive published media.- Viewer routes — if no editor session, checks for a signed audience cookie issued by
/api/auth/verify-audience. Invalid or absent cookie redirects to/login. - Public routes (
/login,/edit/login, auth callbacks) — pass through with no auth check.
The locals.isEditor boolean set by middleware is the signal components use to decide whether to render editor chrome.
A third write path: the MCP connector
Section titled “A third write path: the MCP connector”The two paths above are how a site is served. Content can also be written from outside the site entirely: the platform’s MCP connector (mcp.drawn.guide) commits to the same saved draft branch in the site’s GitHub repo that the in-browser editor uses, under the same validation and optimistic-concurrency contract. The site itself sees nothing unusual — drafts written from chat show up in /edit, and publishing promotes saved to main and triggers the normal Netlify rebuild.