This guide explains how to add a new route to the Access Layer client. It covers where routes are registered, the file naming convention for page components, and how to mark a route as auth-protected.
Every route in the client is declared in a single source of truth at the top of src/App.tsx. The router is built once with createBrowserRouter([...]) and passed to <RouterProvider />. To add a new route, append a { path, element } entry to that array.
// src/App.tsx
import { createBrowserRouter, RouterProvider } from 'react-router';
import HomePage from './pages/HomePage';
import NotFoundPage from './pages/NotFoundPage';
import AboutPage from './pages/AboutPage'; // ← new import
const router = createBrowserRouter([
{
path: '/',
element: <HomePage />,
},
{
path: '/about', // ← new public route
element: <AboutPage />,
},
{
path: '*', // catch-all stays last
element: <NotFoundPage />,
},
]);Key things to know:
- Order matters only for the
*catch-all — keep it as the last entry so it does not shadow real routes. - Imports for new page components live at the top of
src/App.tsxalongside the existing ones. Use the relative'./pages/<Name>Page'path shown above, matching the existing imports. - Do not create a second router or wrap the app in another
<RouterProvider />. The router configured here is the only one. - Nested routes for sub-pages (for example
/creators/:handle/keys) are added the same way — just declare the full pattern on each entry. The current client uses flat routes only.
| What | Convention | Example |
|---|---|---|
| File location | src/pages/ |
src/pages/HomePage.tsx |
| File name | PascalCase + Page suffix |
AboutPage.tsx |
| Exported component | Default export of a function named <Name>Page |
export default function AboutPage() |
The component itself uses export default function <Name>Page() — not a named export, and not an arrow const. This keeps imports straightforward and matches every existing page in src/pages/:
src/pages/
├── HomePage.tsx // registered as '/'
├── MarketingPage.tsx // exists on disk, not yet registered
├── LandingPage.tsx // exists on disk, not yet registered
└── NotFoundPage.tsx // registered as '*' (catch-all)
A few pages exist as files but are not currently wired into the router in src/App.tsx. They are kept on disk because they are planned routes waiting on the marketplace flows to land. If you need one of them live, follow this guide to register it like any other page.
Top-level page components take no props. They own their own layout, fetching, and state. A minimal page is just a function that returns JSX:
// src/pages/AboutPage.tsx
export default function AboutPage() {
return (
<main className="min-h-screen px-6 py-16 text-white">
<h1 className="font-grotesque text-4xl font-black">
About Access Layer
</h1>
<p className="mt-4 font-jakarta text-white/70">
Access Layer is a Stellar-native creator keys marketplace.
</p>
</main>
);
}Conventions to follow:
- Default export only. Named exports break the import in
App.tsx. - One component per file. Don't lump multiple pages into a single file.
- No props. Reach for URL params via
useParams()fromreact-routerinstead of prop drilling. - Accessibility: wrap content in a single
<main>landmark and use semantic headings (<h1>for the page title). - Match the project styling. Use the existing
font-grotesque,font-jakarta, and dark-on-blue palette referenced throughout the codebase. Don't introduce new global styles for a single page. - Use the
@/alias for component imports. The project-wide path alias@/maps tosrc/(configured via Vite + TypeScript path mapping). Use it freely from any component file; reserve short relative paths like'../pages/<Name>Page'for tight sibling-file imports.
There is no existing RequireAuth / ProtectedRoute wrapper in the codebase yet. Every route registered today is public. When a feature needs auth gating, follow the pattern below — and ship the wrapper component with that feature, since the client doesn't have a standalone auth-guard component yet.
- Create a small wrapper component, conventionally
src/components/auth/RequireAuth.tsx(create theauth/subfolder if it doesn't exist yet). - Inside the wrapper, check the appropriate auth state — wagmi's
useAccountfor wallet-gated flows, orauthService.isAuthenticated()for email/login-gated flows. - While the state is resolving, render a lightweight placeholder (the existing
PendingOnboardingPlaceholdermakes a good model). - If unauthenticated, render a redirect or a connect-wallet CTA — do not render the protected page.
- Wrap the protected page's
elementwith the wrapper insideApp.tsx.
// src/components/auth/RequireAuth.tsx
import type { ReactNode } from 'react';
import { useAccount } from 'wagmi';
import { Navigate, useLocation } from 'react-router';
import PendingOnboardingPlaceholder from '@/components/common/PendingOnboardingPlaceholder';
interface RequireAuthProps {
children: ReactNode;
}
export default function RequireAuth({ children }: RequireAuthProps) {
const { isConnected, isConnecting } = useAccount();
const location = useLocation();
// Show the placeholder while a fresh wallet connection is in flight
// so the user isn't redirected to "/" mid-connect. Once it resolves,
// isConnected flips to true and we render the protected page below.
// Note: isReconnecting is intentionally NOT in this branch — a
// reconnect of a prior session keeps the user authenticated, so
// rendering the protected page during a reconnect is fine.
if (isConnecting) {
return <PendingOnboardingPlaceholder />;
}
if (!isConnected) {
// Send unauthenticated users back to the homepage while preserving
// the path they tried to reach so a future flow can deep-link them
// back here after they connect.
return <Navigate to="/" state={{ from: location.pathname }} replace />;
}
return <>{children}</>;
}Then wire the protection in src/App.tsx by wrapping the element rather than registering the page directly:
// src/App.tsx
import RequireAuth from './components/auth/RequireAuth';
import DashboardPage from './pages/DashboardPage';
const router = createBrowserRouter([
{ path: '/', element: <HomePage /> },
{
path: '/dashboard',
// Public route → element: <DashboardPage />
// Protected route → wrap in <RequireAuth>:
element: (
<RequireAuth>
<DashboardPage />
</RequireAuth>
),
},
{ path: '*', element: <NotFoundPage /> },
]);| Use case | Check |
|---|---|
| The page reads or writes Stellar assets (keys, trades, portfolio) | useAccount().isConnected from wagmi |
| The page reads or writes user-profile data via the backend REST API | authService.isAuthenticated() |
| Both | Call both. Render the placeholder until both resolve; redirect if either is false. |
Don't mix the two states in a single component without documenting which is the source of truth for that page — that's the kind of bug that's hard to spot in review.
Two pre-existing repo facts you should know before shipping a wagmi-based guard:
<Web3Provider>is not currently mounted above<App />insrc/main.tsx. As of writing this guide,main.tsxrenders<App />directly inside<StrictMode>. Any wagmi hook — includinguseAccountinsideRequireAuth— will throw at runtime because theWagmiProvidercontext is missing. Wiring<Web3Provider>here is an app-level change and should be tracked separately; reference the tracking issue in your route PR description rather than embedding the wiring fix in your route PR.- Wagmi has a transient
isConnectingstate while a fresh wallet handshake is in flight. During this windowisConnectedisfalse, but redirecting the user mid-connect would bounce them away. The example above handles this by renderingPendingOnboardingPlaceholderfor theisConnectingbranch — copy that pattern verbatim. (Note:isReconnectingis not included in that branch — a reconnect of a prior, already-authenticated session keeps the user authenticated, so rendering the protected page during a reconnect is fine and avoids a UX flash.)
If your guard only uses authService.isAuthenticated() (no wagmi hooks), neither caveat applies — the helper reads localStorage directly.
This walks a contributor end-to-end through adding AboutPage at /about. The page is public, so no RequireAuth wrapper is involved.
Add a new file at src/pages/AboutPage.tsx:
// src/pages/AboutPage.tsx
import { Link } from 'react-router';
import { Button } from '@/components/ui/button';
export default function AboutPage() {
return (
<main className="min-h-screen bg-[#06111f] px-6 py-16 text-white">
<h1 className="font-grotesque text-5xl font-black tracking-tight">
About Access Layer
</h1>
<p className="mt-6 max-w-2xl font-jakarta text-lg text-white/70">
Access Layer is a Stellar-native creator keys marketplace built on
the open AccessLayer protocol.
</p>
<div className="mt-8">
<Button asChild>
<Link to="/">Back to marketplace</Link>
</Button>
</div>
</main>
);
}Add the import and a new entry to the router array in src/App.tsx:
// src/App.tsx
import HomePage from './pages/HomePage';
import NotFoundPage from './pages/NotFoundPage';
import AboutPage from './pages/AboutPage'; // ← added
const router = createBrowserRouter([
{ path: '/', element: <HomePage /> },
{ path: '/about', element: <AboutPage /> }, // ← added
{ path: '*', element: <NotFoundPage /> },
]);Open src/pages/HomePage.tsx, import Link, and add a <Link> to the new route:
import { Link } from 'react-router';
// inside the JSX you return
<Link to="/about" className="font-jakarta text-amber-300 hover:underline">
About this project
</Link>pnpm dev # visit http://localhost:5173/about
pnpm lint
pnpm buildIf pnpm build succeeds and /about renders the page with a working "Back to marketplace" link, you are done.
| File | Purpose |
|---|---|
src/App.tsx |
The single source of truth for routing — the only place routes are registered. |
src/pages/ |
Folder where every page component lives. One file per page, PascalCase + Page suffix, default export. |
src/components/auth/RequireAuth.tsx |
The recommended wrapper for auth-protected pages. Create it the first time a protected route is added. |
src/main.tsx |
Mounts <App /> inside React's createRoot. Currently does not wrap <App /> in Web3Provider — must be updated the first time a wagmi-based route guard lands. |
src/providers/Web3Provider.tsx |
Provides WagmiProvider + QueryClientProvider. Any auth wrapper that uses useAccount only works after this provider is mounted above the router in main.tsx. |
CONTRIBUTING.md |
High-level project conventions — read this alongside this guide. |
docs/api-layer.md |
Sibling guide covering how to add backend/API endpoints. |