| ← 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:
- 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. - Route protection applies only to the staff consoles. The public pages, the scan-landing pages, and
/dashboardrender for everyone (the dashboard handles the unauthenticated state itself); only the/admin,/dev, and/supersubtrees 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 (QueryClientProvider → ApolloProvider → AuthProvider → OrganizationProvider → TooltipProvider), 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
- Add new routes in
src/App.tsx. It is the single source of truth; keep this document in sync in the same change (out-of-date docs are treated as a bug). - Auth is a modal, not a route. Do not add
/login,/register, or/verify-emailroutes — open the relevant modal instead. The only standalone auth URL is/reset-password. - Public vs guarded. New public/landing pages go at the top level alongside the scan-landing routes. Staff-only screens belong under
/admin,/dev, or/superso they inherit the appropriate guard and layout. - Placeholders. Use
<ComingSoon feature="…" />for a guarded-but-unbuilt screen so the navigation entry resolves to a real route instead ofNotFound.
Planned / aspirational routes
These routes are not defined in
src/App.tsxtoday. 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.
- Object management as routes —
/objects,/objects/:id,/objects/:id/edit,/objects/:id/history. Object workflows are currently surfaced through the dashboard and scan-landing pages rather than dedicated routes. - Spatial views —
/spatial,/spatial/:containerIdfor browsing spatial hierarchies. - Organization management as routes —
/organizations,/organizations/:id,/organizations/:id/settings,/organizations/:id/members. Organization context exists viaOrganizationProvider, but there are no dedicated organization routes yet. - User profile / settings routes —
/profile,/settings. Profile actions (e.g. “Change password”) currently live in dialogs (UserProfile.tsx,ChangePasswordDialog.tsx), not routes. - In-app scanner route — a
/scanpage for an in-browser QR/NFC scanner. Native scanning lives in the iOS/Android apps; the web entry point today is the Gateway redirect to the scan-landing routes above.
Related Documentation
- Authentication — modal-based auth flows and password recovery
- resolveIdentifier Endpoint — scan resolution that drives the scan-landing routes
- Gateway URL Structure — the
/scan-errortoken contract