Architecture
Every request to a portal site flows through a single Astro middleware that decides, based on auth state, whether the response is a zero-JavaScript viewer page 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 receive a response with zero JavaScript. React section components are rendered server-side in Astro’s SSR pass — the browser gets plain HTML and CSS. No React runtime is shipped to the client. There are no client:* directives on any component in the viewer layout.
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.