Where the code lives
Every file below is in jodapp-web. The tables are grouped the way the pages are, so a reader who has just finished one page can open the files it described.
The cookie and the API client
What JodApp Web knows about a session describes these.
| File | What it holds |
|---|---|
app/auth/auth-session-cookies.js | authSessionCookies: the user session cookie name, jodapp_session_id, and hasSessionCookie(request), the one question the middleware asks before it calls Rails |
app/auth/auth-admin-session-cookies.js | The admin twin, with teamjod_session_id |
app/api/ky-client.js | createApiClient, and the two clients it builds once per process: apiClient for the user API and teamApiClient for the Team API. The beforeRequest hook that adds X-CSRF-Token in the browser, credentials: 'include', the conversion of every failure into an ApiError, and the choice of host: the public host in the browser, the private host on Node |
app/api/identities-user-sessions-api.js | identitiesUserSessionsApi: create for login, current for the read, destroy for logout, destroyAll for sign out everywhere |
app/api/team-identities-admin-sessions-api.js | teamIdentitiesAdminSessionsApi, the admin twin |
app/errors/api-error.js | ApiError, with status and code from every Rails error, and status: 0 for a network failure |
Reading the session
Reading the session describes these.
| File | What it holds |
|---|---|
app/auth/auth-user-session-middleware.js | authUserSession: the middleware that stores getIdentitiesUserSessionsCurrent() in router context and sets Cache-Control: private, no-store, and the context key the root loader and the guard read |
app/auth/auth-admin-session-middleware.js | authAdminSession, the admin twin, with getTeamIdentitiesAdminSessionsCurrent() |
app/roots/public-root.jsx | The root route for jodapp.com. Exports the session middleware, the root loader that returns { identitiesUserSession }, the shouldRevalidate that reloads after an action fails with 401 or 403, and the error boundary |
app/roots/team-root.jsx | The root route for teamjod.app, returning { identitiesAdminSession } |
app/hooks/use-identities-user-session.js | useIdentitiesUserSession(), which reads the root's route data and nothing else |
app/hooks/use-identities-admin-session.js | useIdentitiesAdminSession(), the admin twin |
Changing a session
Changing a session describes these.
| File | What it holds |
|---|---|
app/routes/login-page.jsx | The user login page and its clientAction, which switches on error.code and redirects with replace() |
app/routes/employers/employers-login-page.jsx | The employer login page, the same shape with its own default target and guest paths |
app/routes/team/team-login-page.jsx | The admin login page, the same shape |
app/routes/logout.js | The /logout resource route: a clientAction that calls DELETE /identities/user_sessions/current and returns replace('/') |
app/routes/logout-everywhere.js | The /logout/everywhere resource route, calling DELETE /identities/user_sessions |
app/routes/team/team-logout.js, app/routes/team/team-logout-everywhere.js | The admin twins, under /team/logout |
app/routes/identities-user/identities-user-edit-page.jsx | The account page. Its clientAction changes the name or email, which the session carries |
app/routes/talent/profile/talent-setup-page.jsx | Talent setup. Its final step moves talent_access.state to ready |
app/routes/org/org-create-account-page.jsx | Employer account creation. Its clientAction moves employer_access.state to active |
Policy routes, the guard, and the entry rules
Policy routes, the policy loader and entry rules describe these.
| File | What it holds |
|---|---|
app/routes.js, app/public.routes.js, app/talent.routes.js, app/org.routes.js, app/team.routes.js | The route tree. Every policy route is mounted here, around exactly the routes it guards |
app/routes/policies/*.jsx | The ten policy routes. Each exports middleware, a loader that returns null, and a component that renders an Outlet |
app/auth/auth-user-guard.js | authUserGuard.protect(rule): turns one entry rule into the middleware a policy exports. Reads url, throws replace() |
app/auth/auth-admin-guard.js | authAdminGuard.protect(rule), the admin twin |
app/auth/auth-entry-rules.js | authEntryRules: the eight user-side rules, userGuest to employerRequired, and USER_GUEST_PATHS |
app/auth/auth-admin-entry-rules.js | authAdminEntryRules: adminGuest, adminRequired, and ADMIN_GUEST_PATHS |
app/auth/auth-safe-redirect.js | authSafeRedirect: the redirect_to check. The only auth file both identity systems share |
app/domains/talent-profile/talent-profile-constant.js, app/domains/org-membership/org-membership-constants.js | The access state words the rules compare against: TALENT_PROFILE_ACCESS_STATE and ORG_MEMBERSHIP_ACCESS_STATE |
app/routes/employers/employers-access-unavailable-page.jsx | The holding page for company_disabled, revoked and any unknown employer state. Reads the WhatsApp link from DEFAULT_COUNTRY.social in app/utils/constants.js |
The visual layouts, and the server rendering flag
Rails is the boundary describes what these must not do.
| File | What it holds |
|---|---|
app/layouts/careers/careers-public-layout.jsx, app/layouts/org/org-public-layout.jsx, app/layouts/org-dashboard/org-dashboard-layout.jsx, app/layouts/team/team-layout.jsx | The visual layouts. They draw the navbar, the sidebar and the page frame. None exports middleware or an action, and none reads the session to decide access |
react-router.config.js | ssr: true. Every rule about Node in these pages depends on it |
The deploy variables
| Variable | Read by | Value |
|---|---|---|
VITE_API_JODAPP_URL | The browser, written into the bundle at build time | https://api.jodapp.com in production, https://api.jodapp.dev in QA |
VITE_API_TEAMJOD_URL | The browser, the same way | https://api.teamjod.app in production, https://api.jodapp.dev in QA |
API_INTERNAL_JODAPP_URL | Node only, at runtime | http://api.internal.jodapp.com in production, http://api.internal.jodapp.dev in QA |
API_INTERNAL_TEAMJOD_URL | Node only, at runtime | http://api.internal.teamjod.app in production, http://api.internal.jodapp.dev in QA |
The two API_INTERNAL_* variables have no VITE_ prefix on purpose, so Vite never writes them into the browser bundle. When one is unset, Node uses the public host, so a missing variable can never break a deploy.
Tests
| File | What it covers |
|---|---|
app/auth/auth-session-cookies.spec.js, auth-admin-session-cookies.spec.js | The cookie name matches exactly, and a user cookie never looks like an admin cookie |
app/auth/auth-user-session-middleware.spec.js, auth-admin-session-middleware.spec.js | No cookie means no call. Two callers in one request share one call. 401 returns null, and 403, 5xx and a network failure stay errors. Every response with a cookie carries Cache-Control: private, no-store |
app/auth/auth-entry-rules.spec.js, auth-admin-entry-rules.spec.js | Every row of every table on the entry rules page, including the unknown-state rows |
app/auth/auth-safe-redirect.spec.js | Outside URLs, protocol-relative paths, and the guest-loop targets are refused |
app/auth/auth-user-guard.spec.js, auth-admin-guard.spec.js | The guard reads url, passes the session to the rule, and throws replace() on a path |
app/api/ky-client.spec.js | The CSRF header is added on writes in the browser only, and never as the string undefined. A refused request is returned once, with no retry |
app/routes/team/team-login-page.spec.js, team-logout.spec.js | The clientAction shapes: form messages as 400, everything else thrown, replace() on success |
app/routes/employers/employers-access-unavailable-page.spec.jsx | One text per state, the WhatsApp link, and no employer data request |
Two navigations to test for every protected area, because they run different code paths:
| Navigation | What it proves |
|---|---|
| A document request | Node applies the policy on every matched middleware |
| A click that enters the area | The policy loader forces the .data request, and the policy runs on Node |