Skip to content

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.

FileURLWhat it is
index.tsx/Landing page
directory.tsx/directorySearchable, filterable member directory
alumni.$id.tsx/alumni/:idOne member's profile
feed.tsx/feedCommunity announcements
auth.tsx/authSign in and sign up
_authenticated/my-profile.tsx/my-profileThe signed-in member's own profile
_authenticated/settings.tsx/settingsAnnouncement preferences
_authenticated/admin.tsx/adminModeration 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.

tsx
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.

Internal engineering documentation.