Repository navigation
Routing
The frontend uses @tanstack/react-router with file-based routing: every URL is a file under frontend/src/routes/, and the Vite plugin generates the route tree (src/routeTree.gen.ts) from the folder structure. The routing layer does four things together:
- defines routes as files, with side-nav metadata attached to each route
- enforces authentication, status, and role requirements through pathless layout routes
- gates feature-flagged routes in
beforeLoadby throwing a router-level 404 - preserves deep-link redirects through the FAM login flow
| File | Purpose |
|---|---|
frontend/src/routes/ |
Route files — one file per route, folder layout is the URL layout |
frontend/src/routeTree.gen.ts |
Generated route tree, committed to the repo (see Vite code generation) |
frontend/src/router.tsx |
createAppRouter(context, options) factory, the browser-history router singleton, and the single Register module augmentation |
frontend/src/routerContext.ts |
AppRouterContext = { queryClient } — the router context type |
frontend/src/routes/__root.tsx |
Root route: RootLayout (title sync, devtools), AuthAwareNotFound, Layout-wrapped error component |
frontend/src/routes/_authenticated.tsx |
Pathless layout: authentication + status gate for every protected URL |
frontend/src/routes/_authenticated/_admin.tsx |
Pathless layout: ADMIN role tier |
frontend/src/routes/_authenticated/_client-access.tsx |
Pathless layout: VIEWER/SUBMITTER role tier |
frontend/src/routes/_authenticated/$.tsx |
Catch-all: one-shell 404 for unknown authenticated URLs |
frontend/src/routes/dashboard.tsx |
Post-login redirect resolver (DashboardRedirect) |
frontend/src/routing/auth.ts |
Pure decision logic: computeAuthRedirect, hasAnyRole
|
frontend/src/routing/redirectStorage.ts |
Single sessionStorage key (redirectAfterLogin) for redirect persistence |
frontend/src/routing/safeRedirect.ts |
getSafeRedirectTarget (open-redirect protection) and isOAuthCallback
|
frontend/src/routing/searchParams.ts |
Zod schema + validateWasteSearch for /search URL state; router-level searchParamCodec
|
frontend/src/routing/staticData.ts |
StaticDataRouteOption module augmentation and the NavTarget union |
frontend/src/routing/nav/useNavEntries.ts |
Projects the side nav from route staticData
|
frontend/src/routing/nav/routeMetadata.ts |
collectRouteGates (gate inheritance) and resolveRouteTitle
|
src/routes/ holds route files only. Shared routing helpers live in src/routing/ so the route folder stays a pure mapping of files to URLs and the code generator never picks up helper modules as routes.
flowchart TD
Entry["main.tsx<br/>providers + <App />"] --> App["App.tsx<br/><RouterProvider router={router} />"]
App --> Match["Router matches the URL<br/>against routeTree.gen.ts"]
Match --> BL["beforeLoad hooks run first<br/>(feature-flag gates)"]
BL -- "flag off → throw notFound()" --> NF["__root notFoundComponent<br/>AuthAwareNotFound"]
BL -- "flag on (or no gate)" --> Render["React render: __root → _authenticated"]
Render --> Auth{"_authenticated layout<br/>computeAuthRedirect"}
Auth -- "no user, known URL" --> Login["persist pathname+search<br/>navigate to /login"]
Auth -- "no user, catch-all match" --> LandingR["navigate to /<br/>(nothing persisted)"]
Auth -- "no-role / role-error status" --> Status["navigate to /no-role<br/>or /unauthorized?reason=CODE"]
Auth -- "granted" --> Tier{"Role tier for the URL"}
Tier -- "/clients" --> CA["_client-access layout<br/>any-of VIEWER / SUBMITTER"]
Tier -- "/configuration subtree" --> ADM["_admin layout<br/>ADMIN only"]
Tier -- "everything else" --> Leaf
CA -- "role denied → /unauthorized" --> Leaf
CA -- "role granted" --> Leaf["Leaf component inside<br/><Layout><Outlet /></Layout>"]
ADM -- "role denied → /unauthorized" --> Leaf
ADM -- "role granted" --> Leaf
Two properties fall out of this flow:
-
beforeLoadruns before any React gate renders. A disabled, flag-gated route 404s before authentication is even checked. See Feature-flag gates for why that ordering is deliberate. -
The shell renders once.
_authenticatedwraps its outlet in a single<Layout>; role tiers render a bare<Outlet/>. Page components never wrap themselves in<Layout>.
src/router.tsx exposes a factory and a singleton:
export const createAppRouter = (context: AppRouterContext, options?: { history?: RouterHistory }) =>
createRouter({
routeTree,
context,
...routerDefaults,
...searchParamCodec,
...options,
});
export const router = createAppRouter({ queryClient }, { history: createBrowserHistory() });- The factory exists for tests: unit tests pass
createMemoryHistory({ initialEntries })throughoptions.historyto exercise the real generated tree without a browser. - The singleton is created once at module load with browser history and the shared TanStack Query client.
App.tsxmounts it with<RouterProvider router={router} />. There is no runtime route-tree switching; all conditional logic lives in layouts andbeforeLoad.
| Option | Value | Why |
|---|---|---|
defaultPendingComponent |
Carbon Loading overlay |
One global pending indicator; no per-route <Suspense> wiring |
defaultPendingMs |
250 |
Avoids a loading flash for fast transitions |
defaultPendingMinMs |
300 |
When the overlay does show, it stays up long enough to be readable |
defaultPreload |
'intent' |
Preload on hover/focus so clicks feel instant |
defaultPreloadStaleTime |
30_000 |
Preloaded data stays fresh for 30 seconds; repeated hovers do not refetch |
defaultErrorComponent |
bare GlobalErrorPage
|
The root and _authenticated routes supply their own Layout-wrapped error components; keeping the router-level fallback bare avoids rendering a second application shell around an error |
...searchParamCodec |
see Search state | Human-readable URL search params everywhere |
Two module augmentations make navigation and route metadata fully typed:
1. Single Register (in src/router.tsx):
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}This registers the generated tree so useNavigate, Link to, and useRouterState accept only real route paths and real param names — no casts, no string typos. There must be exactly one Register in the app: the generated tree already augments the router types through its FileRoutesByPath interface, and a second Register declaration makes the two augmentations compete — the navigation to union collapses to never and every typed navigation fails to compile. The old router's separate Register was deleted at cutover for this reason; the augmentation now lives next to the router that owns it.
2. staticData typing (in src/routing/staticData.ts):
declare module '@tanstack/router-core' {
interface StaticDataRouteOption {
nav?: NavMeta;
roles?: readonly Role[];
featureFlag?: keyof FeatureFlags;
offlineOnly?: boolean;
offlineReady?: boolean;
title?: string;
}
}The augmentation targets @tanstack/router-core, not @tanstack/react-router — the react package only re-exports the core types, so augmenting it has no effect on the route staticData property.
AppRouterContext lives in its own module (src/routerContext.ts) so route files and loaders can import the context type without creating a runtime import cycle through the generated route tree.
src/routes/__root.tsx uses createRootRouteWithContext<AppRouterContext>():
-
RootLayoutkeeps the document title in sync with the active route'sstaticData(leaftitlefirst, thennav.label, then the nearest ancestor — viaresolveRouteTitle). When nothing declares a title, the document title is left untouched. It also mounts devtools in DEV builds only. -
notFoundComponent: AuthAwareNotFoundserves the explicitthrow notFound()markers (feature-flag gates, loader 404s): unauthenticated visitors are redirected to/so they can sign in; authenticated users seeNotFoundPage("Content Not Found") inside the applicationLayout. Unknown URLs are not handled here — the_authenticated/$catch-all claims them first (see 404 handling). -
errorComponentisLayout-wrapped, matching the_authenticatederror component: exactly one application shell around an error page.
src/routes/_authenticated.tsx is a pathless layout route: it has no URL of its own, but wraps every URL beneath it (/search, /clients, /reporting-units/*, /configuration/*). Folders and files prefixed with _ never contribute a URL segment.
The gate is a React layout, not a beforeLoad hook, because authentication state lives in React context (useAuth). A beforeLoad hook has no access to that context and would need a second, non-React snapshot of the auth state to make decisions — two sources of truth that can disagree. The layout reads the same context the rest of the app uses.
The decision logic is pure and testable in src/routing/auth.ts (computeAuthRedirect):
| Condition | Result |
|---|---|
isLoading |
null — layout renders the Loading overlay |
No user, not on /login
|
{ href: '/login' } — external Cognito login |
No user, already on /login
|
null (loop guard; login is external, re-entering would bounce) |
Status no-role
|
{ href: '/no-role' } |
Status role-error, not on /unauthorized
|
{ href: '/unauthorized?reason=<CODE>' } |
Status role-error, already on /unauthorized
|
null (loop guard) |
| Otherwise |
null — render the outlet |
Redirects fire from useLayoutEffect (not useEffect) so the hop commits before the browser paints — no flash of gated content, matching the guard timing the previous router generation used.
When an unauthenticated user hits a known protected URL, the layout persists pathname + search to sessionStorage under redirectAfterLogin (via persistRedirectUrl in src/routing/redirectStorage.ts) and navigates to /login. The /login path itself is never persisted: the navigate fires before the layout unmounts, and the skip preserves the real intended URL saved on the first pass. After login, /dashboard restores the target (see Redirect after login).
The redirect effect depends on primitives only (redirectHref, pathname, searchStr, booleans). A fresh object in the dependency array — for example the whole { href } decision result, recreated every render — re-fires the effect on every transition, and each new navigate() cancels the pending one, producing an infinite navigate/cancel loop that never commits the hop. This bug was caught by the routing access-matrix tests before it shipped; the primitive-deps rule is now enforced in review.
Role restrictions are pathless layouts too, one per tier:
| Layout | Route file | Required roles | Covers |
|---|---|---|---|
_client-access |
src/routes/_authenticated/_client-access.tsx |
any-of VIEWER, SUBMITTER
|
/clients |
_admin |
src/routes/_authenticated/_admin.tsx |
ADMIN |
/configuration subtree |
flowchart TD
Root["__root.tsx — all URLs"] --> Top["Top-level routes<br/>/ (landing) · /dashboard · /no-role · /unauthorized"]
Root --> Auth["_authenticated (pathless)<br/>auth + status gate"]
Auth --> CA["_client-access (pathless)<br/>any-of VIEWER / SUBMITTER"]
Auth --> ADM["_admin (pathless)<br/>ADMIN"]
Auth --> Search["/search<br/>any authenticated user"]
Auth --> RU["/reporting-units/create · /reporting-units/$ruId · /reporting-units/$ruId/$blockId<br/>any authenticated user (flag-gated)"]
Auth --> Catch["_authenticated/$ — catch-all 404"]
CA --> Clients["/clients"]
ADM --> Config["/configuration (flag-gated subtree)"]
Each tier file follows the same pattern:
- one
REQUIRED_ROLESconst — the single place the tier's role list is declared; - roles are read synchronously from auth context, and
hasAnyRole(user.roles, REQUIRED_ROLES)performs an any-of comparison onFamRole.role(user roles areFamRoleobjects, not bare strings); - a
useLayoutEffectnavigates denied users to/unauthorizedbefore first paint (no-flash, same timing discipline as the auth gate); - a
Loadingoverlay covers both the pending and the denied-but-not-yet-redirected render; -
staticData.rolesmirrors the same list as nav metadata only — the layout component is the gate. The nav projection readsstaticDatato filter menu entries without importing route components.
Adding a new role tier means adding a new _tierName.tsx layout under src/routes/_authenticated/, nesting the covered route files inside a matching _tierName/ folder, and registering the tier's label in nothing else — the file system does the wiring.
Feature-flag gates run in the route's beforeLoad and throw a router-level 404 when the flag is off:
beforeLoad: () => {
if (!featureFlags['reporting-unit-details-enabled']) throw notFound();
},Gated routes:
| Flag | Gate location | Covers |
|---|---|---|
reporting-unit-create-enabled |
reporting-units/create.tsx |
/reporting-units/create |
reporting-unit-details-enabled |
reporting-units/$ruId/index.tsx |
/reporting-units/$ruId |
reporting-unit-block-details-enabled |
reporting-units/$ruId/$blockId.tsx |
/reporting-units/$ruId/$blockId |
configuration-enabled |
_admin/configuration.tsx |
the whole /configuration subtree (one check at the section layout; children never repeat it) |
Why beforeLoad and not a layout: feature flags are synchronous environment values, safe to read anywhere; authentication is React context, which beforeLoad cannot see. Splitting them puts each check where its data lives.
Ordering trade-off, accepted deliberately: beforeLoad runs before the React auth gate, so an unauthenticated user hitting a disabled route gets the auth-aware 404 (redirect to / to sign in) rather than a login prompt for a page that does not exist. The route fails closed — the flag off means the URL is not a route, for anyone. A second consequence: an authenticated non-admin user hitting a disabled /configuration gets a 404, not /unauthorized — the flag check runs before the role tier renders. Both behaviors match "disabled means absent" semantics.
Every gated route also declares staticData.featureFlag, so the side-nav projection (useNavEntries) hides the entry while the flag is off — the menu never advertises a URL that 404s.
Two not-found surfaces, deliberately split:
flowchart TD
URL["URL requested"] --> Match{"Match result"}
Match -- "no static route matches<br/>under _authenticated" --> CatchAll["_authenticated/$ — catch-all"]
Match -- "explicit notFound() thrown<br/>flag gate or loader miss" --> RootNF["__root notFoundComponent<br/>AuthAwareNotFound"]
CatchAll -- "user unauthenticated" --> Hop["_authenticated redirects to /<br/>no redirect persisted"]
CatchAll -- "user authenticated" --> Shell404["NotFoundPage inside the one<br/>_authenticated Layout shell"]
RootNF -- "user unauthenticated" --> Hop2["redirect to / (landing)"]
RootNF -- "user authenticated" --> Shell404
-
Unknown URLs land on the
_authenticated/$splat route, which rendersNotFoundPageinside the shell — authenticated 404s keep the header and side nav. Unauthenticated visitors never see it: the parent_authenticatedlayout detects the catch-all match (routeId === '/_authenticated/$') and redirects to/without persisting a redirect target, because there is no real destination to restore after login. Static routes always outrank the splat. -
Explicit
notFound()throws (flag gates, loader 404/403 conversions) bubble to the rootAuthAwareNotFound.
The catch-all exists so 404s render in exactly one application shell; without it, unknown URLs would fall through to the root fallback and render 404 content with no shell at all.
src/routes/dashboard.tsx renders DashboardRedirect, the post-login entry point. It renders null — users never see dashboard content, only the redirect.
sequenceDiagram
participant U as User (no session)
participant AL as _authenticated layout
participant SS as sessionStorage
participant D as /dashboard — DashboardRedirect
U->>AL: GET /search?district=DFN
AL->>SS: persistRedirectUrl("/search?district=DFN")
AL->>U: navigate to /login (external Cognito)
Note over U: OAuth flow completes
U->>D: lands on /dashboard
D->>SS: readPersistedRedirect()
D->>D: getSafeRedirectTarget() validates the value
D->>SS: clearPersistedRedirect()
D->>U: navigate to /search?district=DFN (replace)
Resolution order, once per mount (guarded by a useRef so the effect runs exactly one time):
- Wait for auth to resolve — the login flow needs its session cookies committed before any authenticated navigation makes sense.
- Stored safe target → navigate to it and clear storage.
-
OAuth callback (
code+statein the query) →/search(the query params are Cognito artifacts, not filters). -
Fallback →
/search, preserving any non-OAuth query params as search state.
getSafeRedirectTarget in src/routing/safeRedirect.ts rejects a stored target that is: empty; not path-only (must start with /, must not start with //); an absolute URL from another origin (open-redirect protection); or /dashboard itself (the redirect logic lives there — a self-target would loop).
/dashboard sits at the top level, outside _authenticated, so it never renders the shell or re-triggers the auth/status gate.
The /search route owns its filter state through validateSearch:
export const Route = createFileRoute('/_authenticated/search')({
validateSearch: validateWasteSearch,
component: WasteSearchPage,
...
});The URL is the single source of truth, and the Zod schema in src/routing/searchParams.ts is the only place that decides how each parameter is typed. Consequences:
- No per-component URL sync hooks are needed; filter components read validated search state and write updates through navigation.
- Unknown keys are stripped and malformed values degrade to
undefined(lenient — a bad URL never fails the route). -
legacy=falsesurvives encoding: absent means the legacy search source, sofalsemust stay visible in the URL.
The router-level searchParamCodec (spread into createAppRouter) writes plain, human-readable URLs: strings unquoted, arrays comma-joined (district=DCC,DSC), booleans as true/false. The router's default JSON serializer quote-wraps string values, which broke request matching in tests and made URLs ugly; the codec replaces it and still parses legacy JSON-encoded URLs for compatibility.
src/pages/ReportingUnitDetails/loader.ts is the canonical example:
export const reportingUnitLoader = async ({ context, params }: {
context: AppRouterContext;
params: { ruId: string };
}): Promise<ReportingUnitDto> => {
const ruIdNum = Number(params.ruId);
if (Number.isNaN(ruIdNum)) throw notFound();
const data = await context.queryClient.ensureQueryData({
queryKey: queryKeys.reportingUnit.details(ruIdNum),
queryFn: () => service.reportingUnit.getReportingUnit(ruIdNum),
});
...
};- Loaders receive
{ context, params }; the context carries the shared query client so loaders pre-fetch through TanStack Query (ensureQueryData) and the page'suseQueryhits the cache. - Loaders only resolve data. Access checks live elsewhere: feature flags in
beforeLoad, auth and roles in layouts. A loader that also gates access would put one concern in two places. - 404/403
ApiErrorresponses becomethrow notFound()(auth-aware 404); everything else re-throws to the error components.
| Situation | Convention | Example |
|---|---|---|
| In-app navigation | Typed navigate({ to, params })
|
navigate({ to: '/reporting-units/$ruId', params: { ruId: String(ruId) } }) |
| Numeric route params | Wrap with String() — params are typed as strings |
params: { ruId: String(ruId) } |
| Runtime-composed target strings |
navigate({ href }) — the escape hatch for strings the type system cannot prove |
useListTableRowActions (row action paths templated from table config), PageTitle breadcrumbs (crumb paths built at runtime) |
href exists precisely for those two runtime-string boundaries; everywhere else, typed to navigation catches path and param-name typos at compile time.
The side nav is data projected from routes, not a hand-maintained list:
- Each route that should appear in the nav declares
staticData.nav = { label, icon, order, to }.tois a literalNavTargetunion member ('/clients' | '/search' | '/reporting-units/create' | '/configuration'), so every side-nav link is a typed TanStackLinkwith no casts. -
useNavEntrieswalks the router's route table, and for each route withnavmetadata collects gate metadata (roles,featureFlag,offlineOnly,offlineReady) from the route and its ancestor chain —staticDatais per-route and never merged by the router, so inheritance is an explicit parent walk (collectRouteGates), nearest declaration winning. A child route under_admininherits the ADMIN requirement without repeating it. - Entries are filtered (connectivity, flag truthiness, any-of role membership) and sorted by
nav.order. -
LayoutSideNavrenders the projected entries as flatSideNavLinks; the "Need Help?" footer link is the only hardcoded item.
Adding a nav-visible page means adding nav metadata to its route — no separate menu registry to keep in sync. The Configuration side-nav entry, for example, is just the staticData on _admin/configuration.tsx.
// vite.config.ts — plugin order matters
tanstackRouter({
target: 'react',
autoCodeSplitting: true,
routeFileIgnorePattern: '.*\\.(unit|e2e)\\.test\\.[jt]sx?$',
}),
react(),-
tanstackRoutermust run beforereact()so the generated tree exists when route modules are transformed. -
autoCodeSplittingkeeps each route's component in its own chunk. -
routeFileIgnorePatternkeeps colocated test files out of the route tree. - The build script runs
tsc -b tsconfig.app.json && vite build— type-check happens first, so a clean checkout needssrc/routeTree.gen.tscommitted ortscfails before the generator ever runs. That is why the generated file is checked in.
This walkthrough adds a hypothetical Announcements page (/announcements), visible in the side nav to any authenticated user.
// src/pages/Announcements/index.tsx
import { Column } from '@carbon/react';
import { type FC } from 'react';
import PageTitle from '@/components/core/PageTitle';
const AnnouncementsPage: FC = () => (
<Column lg={16} md={8} sm={4}>
<PageTitle title="Announcements" subtitle="System-wide notices and updates." />
{/* page body goes here */}
</Column>
);
export default AnnouncementsPage;No <Layout> wrapper here — _authenticated already provides the shell.
// src/routes/_authenticated/announcements.tsx
import { Notification } from '@carbon/icons-react';
import { createFileRoute } from '@tanstack/react-router';
import AnnouncementsPage from '@/pages/Announcements';
export const Route = createFileRoute('/_authenticated/announcements')({
component: AnnouncementsPage,
staticData: {
title: 'Announcements',
nav: { label: 'Announcements', icon: Notification, order: 50, to: '/announcements' },
},
});File placement picks the gates automatically:
-
src/routes/_authenticated/announcements.tsx— authenticated, any role (sits directly under the auth gate); -
src/routes/_authenticated/_admin/announcements.tsx— ADMIN only; -
src/routes/_authenticated/_client-access/announcements.tsx— VIEWER/SUBMITTER; -
src/routes/announcements.tsx— public (no gate; wrap in your own layout if it needs a shell).
The string inside createFileRoute(...) is kept in sync by the Vite plugin — start the dev server or run the build once and the generator rewrites it (and routeTree.gen.ts) to match the file's real path.
Run npm run dev (or npm run build). The plugin regenerates src/routeTree.gen.ts. Commit both files — tsc runs before vite build, so the tree must exist in clean checkouts.
// src/pages/Announcements/index.unit.test.tsx
import { RouterProvider } from '@tanstack/react-router';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import AnnouncementsPage from './index';
import { createTestRouter } from '@/config/tests/routerTestHelper';
import PageTitleProvider from '@/context/pageTitle/PageTitleProvider';
describe('AnnouncementsPage', () => {
it('shouldRenderTitleAndSubtitle_whenRendered', async () => {
render(
<RouterProvider
router={createTestRouter(() => (
<PageTitleProvider>
<AnnouncementsPage />
</PageTitleProvider>
))}
/>,
);
await waitFor(() => {
expect(screen.getByText('Announcements')).toBeDefined();
});
});
});createTestRouter builds a router over the real generated tree with memory history, so the test exercises the same matching and layouts as production.
Add the key to featureFlagsSchema in src/env.ts (strict Zod schema — unknown keys are rejected) and set its per-environment values (see Feature Flags for the governance table and environment matrix).
// src/routes/_authenticated/announcements.tsx
import { createFileRoute, notFound } from '@tanstack/react-router';
import { featureFlags } from '@/env';
export const Route = createFileRoute('/_authenticated/announcements')({
beforeLoad: () => {
if (!featureFlags['announcements-enabled']) throw notFound();
},
component: AnnouncementsPage,
staticData: {
featureFlag: 'announcements-enabled',
nav: { label: 'Announcements', icon: Notification, order: 50, to: '/announcements' },
},
});The staticData.featureFlag mirror is what keeps the side-nav entry hidden while the flag is off.
Put the beforeLoad on the section's layout route and let children inherit: _admin/configuration.tsx checks configuration-enabled once and every /configuration/* child is covered. Children never repeat a parent's flag check.
Cover both directions — flag on renders the page, flag off yields the 404 path (auth-aware: unauthenticated goes to /, authenticated renders NotFoundPage). The routing access-matrix test (src/routes/access-matrix.unit.test.tsx) is the template: it mocks featureFlags and walks the full URL × auth-state × role-state matrix, and it is the test level that caught the effect-dependency loop described under Effect dependency discipline.
| File | What is tested |
|---|---|
src/routes/__root.unit.test.tsx |
Root layout title sync, auth-aware not-found behavior |
src/routes/access-matrix.unit.test.tsx |
Full URL × auth × role × flag matrix across the layouts |
src/routing/auth.unit.test.ts |
computeAuthRedirect decision table, hasAnyRole any-of semantics |
src/routing/redirectStorage.unit.test.ts |
Persist/read/clear with storage failures |
src/routing/safeRedirect.unit.test.ts |
Open-redirect rejection rules, OAuth callback detection |
src/routing/searchParams.unit.test.ts |
Zod schema leniency, codec encoding contract |
src/routing/nav/useNavEntries.unit.test.tsx |
Nav projection: inheritance, filtering, ordering |
src/pages/ReportingUnitDetails/loader.unit.test.ts |
Loader data resolution and 404/403 conversion |
When gate behavior changes, update the corresponding unit test first; the access-matrix test is the safety net for cross-cutting regressions.
-
Route Guards — the access-control layouts and
beforeLoadgates in detail - Routes — catalog of every route file and URL
- Authentication — FAM/Cognito flow and redirect preservation
- Roles — the access-status rules the auth gate enforces
- Feature Flags — flag registry, governance, and environment matrices
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository