Routing and rendering
The framework
The application is built on TanStack Start, which sits on top of TanStack Router and provides server-side rendering and file-based routing.
It is not Next.js. The README.md in the repository root still describes the frontend as Next.js, because that file predates the actual technology choice. The code is the authority. Patterns copied from Next.js documentation will not work here: there is no pages/ directory, no getServerSideProps, no Next-style layout files.
How routes are defined
Every file under src/routes/ becomes a route, and the filename determines the URL.
| File | URL | What it is |
|---|---|---|
index.tsx | / | Landing page |
directory.tsx | /directory | Searchable, filterable member directory |
alumni.$id.tsx | /alumni/:id | One member's profile |
feed.tsx | /feed | Community announcements |
auth.tsx | /auth | Sign in and sign up |
_authenticated/my-profile.tsx | /my-profile | The signed-in member's own profile |
_authenticated/settings.tsx | /settings | Announcement preferences |
_authenticated/admin.tsx | /admin | Moderation and administration |
src/routeTree.gen.ts is generated from this directory by the router plugin. It is rewritten on every build, so editing it by hand accomplishes nothing.
The two special files
__root.tsx is the application shell. It renders the HTML document, loads fonts, and wraps everything in the query client and the internationalization provider. It also holds a global authentication listener that invalidates cached data when someone signs in or out, which is what keeps a stale signed-out view from persisting after login.
_authenticated/route.tsx is a layout route. Its beforeLoad hook checks for a signed-in user and redirects to /auth if there is none, so every route nested beneath it requires authentication without repeating the check. It runs with server-side rendering disabled, because the session lives in browser storage and cannot be read during a server render.
A leading underscore means the segment does not appear in the URL. _authenticated/admin.tsx serves /admin, not /authenticated/admin.
Route protection is not security
The _authenticated layout, and hooks like useIsAdmin, exist to make the interface behave sensibly: unauthenticated visitors are sent to sign in, and buttons that would fail are not shown.
None of it prevents anything. A determined caller can query the database directly with their own credentials, skipping every route in this application. What stops them is row-level security in PostgreSQL. See How access control works.
Data fetching
Route components query Supabase directly through TanStack Query. There is no separate data access layer, and no repository pattern to learn.
const { data, isLoading } = useQuery({
queryKey: ["profile", id],
queryFn: async () => {
const { data, error } = await supabase
.from("profiles")
.select("*")
.eq("username", id)
.maybeSingle();
if (error) throw error;
return data;
},
});Two conventions worth following. Query keys are arrays whose first element names the resource, because invalidation matches on prefixes: invalidating ["profile"] invalidates every individual profile query. And mutations should invalidate the keys they affect rather than writing into the cache by hand, which is easier to reason about when several components read the same data.
Scroll restoration
The router is configured with scrollRestoration: true, which saves a scroll position per history entry and restores it when the user navigates back.
This is worth understanding because it has already caused one bug. Restoration is keyed to a history entry. Pressing the browser's back button returns to an existing entry, which has a saved position. A link that navigates forward creates a new entry, which has no saved position, so the page correctly starts at the top.
A control labelled "Back" that is implemented as a forward link will therefore lose the user's place, while the browser's own back button works perfectly. The fix is to perform a real history back when there is history to go back to, and fall back to a normal link when there is not.
Error handling
TanStack Start runs on h3, which converts an exception thrown inside a handler into a generic 500 response with a JSON body, losing the original error.
src/server.ts wraps the generated server entry to detect that specific response shape and render a real error page instead. vite.config.ts points the framework at this wrapper deliberately. If you change the server entry configuration, make sure this wrapping survives, or production errors become unreadable.
Server functions
Code that must run on the server, with privileges the browser does not have, is written as a TanStack Start server function.
Two pieces of middleware make this work. attachSupabaseAuth, registered globally in src/start.ts, attaches the caller's bearer token to outgoing calls. requireSupabaseAuth validates that token on the receiving side and provides the caller's identity to the handler.
src/start.ts must exist for this to hold. TanStack Start installs a default cross-site request forgery setup when the file is absent, so deleting it silently changes security behaviour rather than producing an error.
The service-role client, which bypasses row-level security, must only ever be imported lazily inside a server handler. A top-level import from a route file or a .functions.ts file would pull it into the client bundle and publish the key.