← Back to Frontend Documentation Main Documentation

Frontend Routing System

Created: Tue 29 Jul 2025 07:33:22 CEST
Updated: Sun 14 Jun 2026 10:17:12 CEST - Scan landing Phase 1: added three new public, unauthenticated routes (/o/:oid, /welcome, /scan-error)
Updated: Mon 15 Jun 2026 21:28:19 CEST - Rewritten to match the actual route definitions in src/App.tsx: documents the implemented flat top-level routes (incl. scan-landing routes), modal-based auth (no /auth/* routes), and the admin/dev/super console subtrees; aspirational routes moved to a clearly marked “Planned / aspirational routes” section
Document Version: 2.0 - Documents implemented routing as defined in src/App.tsx; planned routes separated from reality
Security Classification: Internal Technical Documentation
Target Audience: Frontend Developers, Technical Leads, DevOps Engineers
Author: Paul Wisén

Overview

The Plings frontend uses React Router v6 for client-side routing. All routes are defined in a single place: src/App.tsx.

Two things distinguish the implemented routing from a “typical” React SPA layout:

  1. Authentication is modal-based, not route-based. There are no /auth/* routes. Login, registration, sign-up progress, and e-mail verification happen in dialogs (src/components/auth/AuthModal.tsx, SignupProgressModal.tsx, EmailVerificationModal.tsx) layered over whatever page the user is on. The only auth-related route is the top-level /reset-password, which is the landing page for Supabase password-recovery links (it must be a real URL because the user arrives from an e-mail). See Authentication for the auth flows.
  2. Route protection applies only to the staff consoles. The public pages, the scan-landing pages, and /dashboard render for everyone (the dashboard handles the unauthenticated state itself); only the /admin, /dev, and /super subtrees are wrapped in permission-checking guard components.

All page components are imported eagerly — there is currently no lazy loading / route-based code splitting.

Implemented Routes

This is the complete route tree as defined in src/App.tsx. Routes marked (placeholder) render the ComingSoon page component — the route exists and is guarded, but the feature behind it is not built.

/                           # Public landing page (Index)
├── /o/:oid                 # Public scan landing for a known object (ObjectLanding) — unauthenticated
├── /welcome                # Onboarding / unknown-tag landing (Welcome) — unauthenticated
├── /scan-error             # Genuineness / error landing (ScanError) — unauthenticated
├── /about                  # About page (About)
├── /dashboard              # Main application dashboard (Dashboard)
├── /reset-password         # Password reset (target of Supabase recovery e-mail links) (ResetPassword)
│
├── /admin                  # Admin console — guarded by <AdminRoute>, wrapped in <AdminLayout>
│   ├── (index)             # AdminDashboard
│   ├── /test               # AdminTest
│   ├── /classes            # Object class management (AdminClasses)
│   ├── /billing            # Billing administration (AdminBilling)
│   ├── /users              # User management (AdminUsers)
│   ├── /organizations      # Organization management (AdminOrganizations)
│   ├── /audit              # Audit log (AdminAudit)
│   ├── /transactions       # (placeholder) Transaction Resolution
│   ├── /wallets            # (placeholder) Wallet Management
│   ├── /content            # (placeholder) Content Moderation
│   ├── /support            # (placeholder) Support Center
│   ├── /marketplace        # (placeholder) Marketplace Control
│   └── /analytics          # (placeholder) Analytics Dashboard
│
├── /dev                    # Developer console — guarded by <DevRoute>, wrapped in <DevLayout>
│   ├── (index)             # DevDashboard
│   ├── /api                # API explorer (DevApi)
│   ├── /debug              # Debug tools (DevDebug)
│   ├── /performance        # Performance monitoring (DevPerformance)
│   ├── /logs               # Log viewer (DevLogs)
│   ├── /backend            # (placeholder) Backend Tools
│   ├── /database           # (placeholder) Database Explorer
│   ├── /features           # (placeholder) Feature Flags
│   ├── /system             # (placeholder) System Diagnostics
│   └── /deployment         # (placeholder) Deployment Tools
│
├── /super                  # Super admin console — guarded by <SuperAdminRoute>, wrapped in <SuperAdminLayout>
│   ├── (index)             # SuperDashboard
│   ├── /security           # Security overview (SuperSecurity)
│   ├── /security/incidents # Incident management (SuperIncidents)
│   ├── /security/hsm       # HSM management (SuperHSM)
│   ├── /emergency          # Emergency controls (SuperEmergency)
│   ├── /wallets            # (placeholder) Wallet Lifecycle Management
│   ├── /wallets/creation   # (placeholder) Wallet Creation Workflow
│   ├── /wallets/migration  # (placeholder) Migration Tools
│   ├── /config             # (placeholder) Global Configuration
│   └── /maintenance        # (placeholder) System Maintenance
│
└── *                       # Catch-all → NotFound (404 page; there is no /404 route)

Scan-landing routes (public, unauthenticated)

The three scan-landing routes are the browser-path destinations the Gateway (s.plings.io) redirects to. They are intentionally public — any browser can reach them without a session:

Route Page component Purpose
/o/:oid pages/scan/ObjectLanding Status-aware landing for a known object. Reads :oid from the path and renders the object’s public view via the publicObject query.
/welcome pages/scan/Welcome Onboarding landing for unknown / unregistered tags (and the sign-up verification origin root).
/scan-error pages/scan/ScanError Single error/genuineness landing. Reads the reason and details query params (e.g. ?reason=invalid, ?reason=system). There is one /scan-error route — a sub-path like /scan-error/invalid would fall through to NotFound.

See resolveIdentifier Endpoint and the Gateway URL Structure for the routing and error-token contract these pages consume.

Router configuration

The router setup in src/App.tsx (abridged — placeholder routes omitted, see the file for the full list):

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Index />} />
    <Route path="/o/:oid" element={<ObjectLanding />} />
    <Route path="/welcome" element={<Welcome />} />
    <Route path="/scan-error" element={<ScanError />} />
    <Route path="/about" element={<About />} />
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/reset-password" element={<ResetPassword />} />

    {/* Admin Routes */}
    <Route path="/admin" element={<AdminRoute />}>
      <Route element={<AdminLayout />}>
        <Route index element={<AdminDashboard />} />
        <Route path="users" element={<AdminUsers />} />
        {/* ... */}
      </Route>
    </Route>

    {/* Developer Routes */}
    <Route path="/dev" element={<DevRoute />}>
      <Route element={<DevLayout />}>
        <Route index element={<DevDashboard />} />
        {/* ... */}
      </Route>
    </Route>

    {/* Super Admin Routes */}
    <Route path="/super" element={<SuperAdminRoute />}>
      <Route element={<SuperAdminLayout />}>
        <Route index element={<SuperDashboard />} />
        {/* ... */}
      </Route>
    </Route>

    <Route path="*" element={<NotFound />} />
  </Routes>
</BrowserRouter>

The router lives inside the provider stack (QueryClientProviderApolloProviderAuthProviderOrganizationProviderTooltipProvider), so every route has access to auth state, the active organization, Apollo, and TanStack Query. Three debug tools (BackendSwitcher, PermissionDebugger, MobileDebugConsole) are mounted alongside the routes inside BrowserRouter and are available on every page for authorized users.

Route Protection

The three console subtrees use the layout-route pattern: a guard component renders an <Outlet /> when access is granted, so a single guard protects the whole subtree. The guards live in src/components/admin/shared/ and resolve access from useUserPermissions():

Guard Protects Grants access when On denial
AdminRoute /admin/* isPlingsAdmin or isSystemOwner Redirect to /
DevRoute /dev/* isPlingsDeveloper or isSystemOwner Redirect to /
SuperAdminRoute /super/* isSystemOwner only Inline “Access Denied” screen

All three guards share the same shape:

// src/components/admin/shared/AdminRoute.tsx (simplified)
export const AdminRoute: React.FC<AdminRouteProps> = ({ requiredAbility }) => {
  const permissions = useUserPermissions();

  const hasAdminAccess = permissions.isPlingsAdmin || permissions.isSystemOwner;

  if (permissions.loading || permissions.user === null) {
    return <LoadingScreen />; // spinner while permissions resolve
  }

  if (!hasAdminAccess) {
    return <Navigate to="/" replace />;
  }

  // Optional fine-grained checks: a route can require specific abilities
  if (requiredAbility && !permissions.abilities?.includes(requiredAbility)) {
    return <Navigate to="/admin" replace />;
  }

  return <Outlet />;
};

DevRoute is identical with isPlingsDeveloper || isSystemOwner and a fallback to /dev. SuperAdminRoute requires isSystemOwner only and, on denial, renders an inline “Access Denied” screen rather than redirecting.

Notes for Contributors

Planned / aspirational routes

These routes are not defined in src/App.tsx today. They are recorded here as direction only — do not assume they exist. When one ships, move it up into Implemented Routes in the same change.