# Goodwalk Mobile Architecture > Developer reference for the dedicated mobile experience layer introduced in April 2026. --- ## Overview Goodwalk's mobile experience is a **first-class application layer** built within the existing SvelteKit project. It is not a responsive version of the desktop — mobile users are automatically routed into dedicated mobile routes that use purpose-built layouts, components, and navigation patterns. ``` Desktop users → /admin/* /members/* Mobile users → /admin/m/* /members/m/* ``` Both the Admin and Member mobile experiences share the same component library and design token system while keeping their routes, permissions, and business logic completely separate. --- ## Folder Structure ``` frontend/src/ ├── lib/ │ └── mobile/ │ ├── utils/ │ │ └── device.js UA detection: isMobileUA(), isMobileDevice() │ ├── stores/ │ │ └── mobile.js isMobile Svelte store + initMobileStore() │ ├── guards/ │ │ └── routeGuard.js guardMobileRoute() — client-side desktop redirect │ └── components/ │ ├── MobileAppShell.svelte │ ├── MobilePageShell.svelte │ ├── MobileHeader.svelte │ ├── MobileBottomNav.svelte │ ├── MobileSection.svelte │ ├── MobileCard.svelte │ ├── MobileListItem.svelte │ ├── MobileStatCard.svelte │ ├── MobileActionBar.svelte │ ├── MobileSearchBar.svelte │ ├── MobileFormField.svelte │ ├── MobileEmptyState.svelte │ ├── MobileFilterSheet.svelte │ └── MobileTabs.svelte │ └── routes/ ├── +layout.server.js Exposes isMobile to client via page data ├── admin/ │ └── m/ Admin mobile routes │ ├── +layout.js ssr: false (matches desktop admin) │ ├── +layout.svelte Auth + theme + nav shell │ ├── dashboard/+page.svelte │ ├── bookings/+page.svelte │ ├── customers/+page.svelte │ ├── reporting/+page.svelte │ └── settings/+page.svelte └── members/ └── m/ Members mobile routes ├── +layout.server.js Cookie auth guard (mirrors desktop) ├── +layout.svelte Auth sync + polling + nav shell ├── home/+page.svelte ├── bookings/+page.svelte ├── walks/+page.svelte ├── messages/+page.svelte └── profile/+page.svelte ``` --- ## How Mobile Routing Works ### 1. Server-side detection (hooks.server.js) On every GET request, `hooks.server.js` tests the `User-Agent` header against a mobile phone pattern: ```js const MOBILE_UA_RE = /iPhone|Android.*Mobile|IEMobile|BlackBerry|Opera Mini/i; ``` **Tablets** do NOT match this pattern (Android tablets omit "Mobile" in the UA; iPad is excluded). Tablets receive the desktop experience. If a phone-class UA is detected and the user is on a desktop route (`/admin/*` or `/members/*`), a `307` redirect is issued to the corresponding mobile entry point. ### 2. Redirect targets The redirect target is calculated relative to the browser's visible URL, accounting for subdomain rewriting: | Environment | User visits | Redirected to | |---|---|---| | Local dev | `/admin` | `/admin/m/dashboard` | | Local dev | `/members/dashboard` | `/members/m/home` | | Admin subdomain | `admin.goodwalk.co.nz/` | `admin.goodwalk.co.nz/m/dashboard` (→ internally `/admin/m/dashboard`) | | Members subdomain | `members.goodwalk.co.nz/bookings` | `members.goodwalk.co.nz/m/home` | ### 3. Redirect exclusions These paths are **never redirected** to mobile routes, even on a phone: - `*/login`, `*/verify` — auth flows work on all platforms - `*/claim`, `*/onboarding` — onboarding is currently desktop-only - Paths already under `/m/` — prevents redirect loops - Requests with `?d=1` — explicit desktop override (useful for QA, power users) ### 4. Client-side desktop guard (guardMobileRoute) Mobile layout files call `guardMobileRoute()` in `onMount`. If a desktop browser somehow lands on a `/m/` route (e.g. shared link, direct URL entry), it is sent back to the desktop equivalent. ```js // In any /admin/m/ or /members/m/ +layout.svelte: import { guardMobileRoute } from '$lib/mobile/guards/routeGuard.js'; onMount(() => { guardMobileRoute($page.url.pathname, $page.url.search); }); ``` The guard respects `?d=1` — if that param is present, the user stays on the mobile route regardless of device (for QA testing mobile on desktop). ### 5. Preventing redirect loops Loop prevention is layered: 1. `hooks.server.js` never redirects paths that already contain `/m/`. 2. Mobile layouts never redirect mobile users. 3. `?d=1` stops both server and client redirects. --- ## Auth in Mobile Routes ### Admin mobile (`/admin/m/`) - **SSR disabled** — `+layout.js` sets `ssr: false`, identical to the desktop admin. - Auth is handled entirely **client-side** in `+layout.svelte` via `getAdminAccessToken()`. - If no token is found, a login form is rendered in place of the page content. - After login, `saveAdminTokens()` is called and the layout re-renders the shell. - The admin theme preference is loaded from `localStorage` on mount. ### Members mobile (`/members/m/`) - **SSR enabled** — `+layout.server.js` runs on the server and checks the `gw_member_session` cookie. - If the cookie is missing, a server-side `307 redirect` to `/members/login?returnTo=` is issued. - After login (on the desktop login page), the session cookie is set and the member is returned to the mobile route via the `returnTo` param. - Client-side auth state is further validated in `+layout.svelte` via `isLoggedIn()`. --- ## How to Use Shared Mobile Components All components live in `src/lib/mobile/components/` and use the `--m-*` CSS custom property token layer provided by `MobileAppShell`. ### Basic page structure ```svelte Content here ``` ### Key component contracts **MobileAppShell** — set once per layout (`+layout.svelte`), never in individual pages. - Props: `variant` (`'admin'|'members'`), `navItems`, `currentPath` **MobilePageShell** — set once per page, wraps all page content. - Props: `title`, `showBack`, `backHref`, `transparent` - Slot: `header-action` for right-side icon buttons **MobileBottomNav** — rendered automatically by `MobileAppShell`. Never add it manually to a page. **MobileCard** — use `href` prop to make it a tappable link. Avoid `on:click` on cards when a simple link will do. **MobileListItem** — always provide `divider={false}` on the last item in a list. **MobileEmptyState** — always include all three variant cases in data-fetching pages: ```svelte {#if loading} {:else if error} {:else if items.length === 0} {:else} {/if} ``` **MobileFilterSheet** — use `bind:open` to control visibility. The sheet handles its own `Escape` key and overlay click dismissal. --- ## How Admin and Member Mobile Experiences Differ | Concern | Admin mobile | Members mobile | |---|---|---| | Route prefix | `/admin/m/` | `/members/m/` | | SSR | Disabled (`ssr: false`) | Enabled | | Auth | Client-side localStorage JWT | Server cookie + localStorage | | Theme | Admin theme switcher (5 variants) | Fixed green brand palette | | Nav items | Dashboard, Clients, Add Client, Reporting, Settings | Home, Book, Walks, Messages, Account | | Feature flags | `bookings_enabled`, `walks_enabled`, etc. | Same set, independently fetched | | Business logic | Manages all clients/bookings/walks | Views own data only | | Login screen | Rendered inline in the layout | Separate `/members/login` page | --- ## Extending the Mobile Shell ### Adding a new admin mobile page 1. Create `src/routes/admin/m/
/+page.svelte`. 2. Use `MobilePageShell` as the root component in the template. 3. If the page needs a back button, set `showBack` and optionally `backHref`. 4. Add an entry to `BASE_NAV` in `src/routes/admin/m/+layout.svelte` if it should appear in the bottom nav. Otherwise link to it from an existing page. ### Adding a new members mobile page 1. Create `src/routes/members/m/
/+page.svelte`. 2. Auth is handled by `+layout.server.js` — no per-page auth check needed. 3. Use `MobilePageShell` and shared components. 4. Add a nav entry in `ACTIVE_NAV` in `src/routes/members/m/+layout.svelte` if it needs a tab. ### Adding a new shared component 1. Create `src/lib/mobile/components/Mobile.svelte`. 2. Use only `--m-*` CSS custom properties — never hardcode colours. 3. Ensure all interactive elements have `min-height: var(--m-touch-min, 44px)`. 4. Annotate props with JSDoc comments at the top of the ` ``` **Do this instead:** - Keep mobile-specific logic in `src/lib/mobile/` and `src/routes/*/m/`. - Pass data down via props from the mobile layout to mobile components. - If a concern is shared (e.g. feature flags, API fetching), import from the existing shared lib (`$lib/featureSettings`, `$lib/memberApi`, etc.) — these are fine to use in mobile pages. - The mobile components themselves (`MobileCard`, `MobileSection`, etc.) should contain zero business logic. --- ## Migration Notes The following changes were made to existing files: ### `src/hooks.server.js` - Added `MOBILE_UA_RE` constant and `isMobileUA()` function. - Added `maybeMobileRedirect()` which returns a `307 Response` for mobile users on desktop routes. - The redirect fires **after** subdomain rewriting so the canonical pathname is used for matching. - Added `event.locals.isMobile` for downstream use. - The subdomain rewriter now skips paths starting with `/m/` (prevents double-prefixing mobile routes on subdomains). ### `src/routes/+layout.server.js` - Added `isMobile: locals.isMobile ?? false` to the returned data object. ### `src/routes/+layout.svelte` - Added a comment clarifying that `/admin/m/*` and `/members/m/*` paths satisfy the existing `isAdmin`/`isMembers` checks and correctly bypass the marketing site chrome. --- ## Follow-up Recommendations 1. **Members login → mobile redirect.** After a member logs in via `/members/login`, `memberApi.js` currently does `goto('/members/dashboard')`. On mobile, this client-side navigation skips `hooks.server.js` and lands the user on the desktop dashboard. Fix: detect mobile in the post-login goto (use `isMobileDevice()` from `$lib/mobile/utils/device.js`) and redirect to `/members/m/home` instead. 2. **Onboarding mobile experience.** Onboarding (`/members/onboarding`) is currently excluded from mobile redirects. Consider building a dedicated `/members/m/onboarding` flow using `MobileFormField` and `MobilePageShell` with back navigation. 3. **Admin mobile — individual client detail page.** The customers list links to `/admin/members/:id` (desktop). Build `/admin/m/customers/:id/+page.svelte` for a mobile-optimised client profile. 4. **Offline / PWA support.** The mobile shell is PWA-ready (safe area support, overscroll contain, touch optimisations). Add a `manifest.json` and service worker to make it installable. 5. **Swipe-back gesture.** iOS-style swipe-back for stacked navigation can be layered on top using Svelte's `fly` transition with a touch gesture handler on the `MobilePageShell` container. 6. **Dark mode.** The `--m-*` token layer makes adding a dark theme straightforward. Add `prefers-color-scheme: dark` overrides in `MobileAppShell`'s `