Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Frontend Architecture

Aero2's frontend is a single-page application (SPA) built with React 19 and Vite, served by the Cloudflare Worker.

Tech Stack

TechnologyPurpose
React 19UI framework
ViteBuild tool and dev server
React RouterClient-side routing
Tailwind CSS v4Utility-first styling
shadcn/uiComponent library (Button, Card, Dialog, etc.)
SonnerToast notifications
lucide-reactIcons
React ContextState management (AuthContext, AppContext, SidebarContext, ThemeContext)

Layout: AppShell + Sidebar

When an authenticated user navigates to /apps/:id/*, the app renders inside AppShell — a layout with a persistent sidebar and a content area.

Sidebar Components

┌──────────────────────────────┐
│ SidebarHeader (logo + brand) │
│ AppSwitcher (app dropdown)   │
│ SidebarNav (nav groups)      │
│ SidebarFooter (user menu)    │
└──────────────────────────────┘
  • SidebarHeader — Aero2 logo, collapse toggle
  • AppSwitcher — Dropdown to switch between apps. Shows "Platform" with a shield icon and indigo accent when in the dashboard app. Operators see the dashboard entry at the top of the list.
  • SidebarNav — Renders nav groups. Uses dashboardNavGroups for the dashboard app and appNavGroups for regular apps (defined in nav-config.ts).
  • SidebarFooter — Avatar, user name, settings link, sign-out
  • SidebarContext — Manages collapsed/expanded state and mobile drawer

Regular App Nav

For per-app management, the sidebar shows:

  • Overview — App metrics
  • Authentication — Sign-in Methods, Identity Providers, Sessions
  • User Management — Users, Roles & Permissions, Organizations
  • Developer — API Keys, OAuth Clients
  • Configure — Branding, Settings, Custom Domains
  • Monitor — Audit Log

Dashboard / Platform Nav

When viewing the dashboard app, the sidebar switches to a platform-focused structure with indigo accent colors:

  • Overview — Platform metrics (app count, user count)
  • Applications — All Applications
  • Authentication — Identity Providers (platform IdPs)
  • Users & Access — Users, Roles & Permissions
  • Configure — Platform Settings
  • Monitor — Audit Log

Dashboard Routes

dashboard.aero2.dev/
  /login                        -> Developer sign-in
  /signup                       -> Developer sign-up
  /                             -> Application list (after login)
  /apps/:id/overview            -> App or platform overview
  /apps/:id/users               -> App user management
  /apps/:id/auth                -> Auth method configuration
  /apps/:id/idps                -> Identity provider management
  /apps/:id/sessions            -> Session management
  /apps/:id/roles               -> Roles and permissions
  /apps/:id/clients             -> OAuth client management
  /apps/:id/api-keys            -> API key management
  /apps/:id/branding            -> Login page customization
  /apps/:id/settings            -> App settings
  /apps/:id/domains             -> Custom domain configuration
  /apps/:id/organizations       -> Organization management
  /apps/:id/audit               -> Audit logs
  /apps/:id/applications        -> All Applications (dashboard only)
  /apps/:id/platform/users      -> Platform users (dashboard only)
  /apps/:id/platform/idps       -> Platform IdPs (dashboard only)
  /apps/:id/platform/roles      -> Platform roles (dashboard only)
  /apps/:id/platform/settings   -> Platform settings (dashboard only)
  /apps/:id/platform/audit      -> Platform audit log (dashboard only)

App Layout (Per-App Subdomain)

When a user visits an application subdomain (e.g., swift-maple.aero2.dev), the SPA renders branded auth pages for end users:

  • Login form (email/password + social IdP buttons)
  • Signup form with email verification
  • MFA verification page
  • Password reset flow
  • OAuth2 authorization consent

Per-App Routes

swift-maple.aero2.dev/
  /login                        -> End-user sign-in (branded)
  /signup                       -> End-user sign-up
  /oauth2/authorize             -> OAuth2 authorization endpoint
  /oauth2/token                 -> Token endpoint
  /oauth2/userinfo              -> UserInfo endpoint
  /oauth2/jwks.json             -> JWKS endpoint
  /.well-known/openid-config..  -> OIDC discovery

Component Library

UI components come from shadcn/ui in src/frontend/components/ui/, styled with Tailwind CSS v4 utility classes:

  • Button — Multiple variants (default, secondary, destructive, outline, ghost)
  • Card — Container with Card.Content sections
  • Input — Form inputs with label and validation states
  • Dialog / Modal — Overlay dialogs with confirm/cancel
  • ConfirmDialog — Destructive action confirmation
  • Spinner — Loading indicator
  • Badge — Status labels
  • Pagination — Page navigation
  • Tooltip — Hover tooltips (used in collapsed sidebar)
  • Sonner Toaster — Toast notification system

Use cn() from @/lib/utils for conditional class merging. The @/ path alias maps to src/frontend/.

Dark Mode

Dark mode is toggled via ThemeContext, which adds/removes the .dark class on <html>. Tailwind's dark: variant handles all color switching. An anti-FOUC script in index.html applies the class before first paint.

State Management

State is managed through React Context:

  • AuthContextuser, isAuthenticated, isLoading, signOut(), session info
  • AppContextappId, appName, slug, isDashboard for the current app
  • SidebarContext — Collapsed/expanded state, mobile drawer open/close
  • ThemeContext — Light/dark mode toggle

No external state management library — React Context is sufficient for the current scope.

API Client

All API calls go through utils/api.ts:

import { api, ApiError } from "../utils/api";
 
const data = await api.get<{ users: User[] }>("/api/users");
await api.post("/api/clients", { name: "My Client" });
await api.put("/api/settings", updatedSettings);
await api.del("/api/sessions/123");
  • Auto-includes credentials: "include" and JSON content type
  • Redirects to /login on 401 (suppressible with { noAuthRedirect: true })
  • Throws ApiError with .status, .error, .details
  • Never use raw fetch() for internal API calls