SameBoat Frontend is a lightweight Vite + React 19 single-page application scaffold. It focuses on fast local iteration (HMR), strict TypeScript, and a minimal baseline you can extend (routing, API clients, state management) without vendor lock-in.
Live App: https://sameboatplatform.org/
Also available via Netlify default domain: https://app-sameboat.netlify.app/
Note: adopting a dedicated subdomain like https://app.sameboatplatform.org is a common production practice. It cleanly separates marketing/docs (root) from the application, simplifies cookie scoping and CSP, and scales well as you add more subdomains. You can adopt it later without code changes.
- Week 3 Progress Checklist:
docs/week-3-plan-checklist.md - Week 4 Draft Plan:
docs/week-4-plan-draft.md - Changelog:
CHANGELOG.md(see [Unreleased] section for in-flight changes) - Developer Workflow Checklist:
docs/developer-workflow-checklist.md
Pre-1.0 releases use 0.x.y; breaking changes may occur between minor bumps. Each meaningful change should append a line under [Unreleased] in the changelog; on tagging a release, move those lines into a dated section.
| Layer | Choice | Notes |
|---|---|---|
| Build dev server | Vite 7 | HMR + fast SWC transforms |
| UI library | React 19 + Chakra UI (selective) | React for core + incremental Chakra adoption (cards, forms) |
| Transpilation | SWC via @vitejs/plugin-react-swc |
Faster than Babel for local dev |
| Language | TypeScript ^5.8.0 (strict) | noUncheckedSideEffectImports, verbatimModuleSyntax enforced |
| Linting | ESLint flat config + plugins | React Hooks, Refresh, a11y, import sorting |
| Conventional commits | commitlint + Husky hooks | Enforces type(scope?): subject style |
| CI | GitHub Actions | Lint, type, tests; coverage (PRs, ≥50%); changelog check; build |
| Release utility | Custom script npm run release |
Bumps version + moves [Unreleased] in CHANGELOG |
src/
main.tsx # Entry – mounts <App />
App.tsx # App shell (wraps providers + routes)
routes/ # Route configuration + ProtectedRoute + layout transitions
state/auth/ # Auth store (Zustand) + useAuth adapter + AuthEffects (bootstrap/visibility)
pages/ # Page-level React components
components/ # Reusable UI bits (AuthForm, FormField, Footer, UserSummary, RuntimeDebugPanel, GlobalRouteTransition)
lib/ # Shared utilities (api.ts, health.ts)
theme.ts # Chakra theme customization (dark-mode default, brand tokens)
public/ # Static assets served at root (/favicon, /vite.svg)
HealthCheckCard centralizes backend liveness/health polling with:
- Configurable interval via prop or
VITE_HEALTH_REFRESH_MS. - Minimum skeleton duration to reduce UI flicker.
- Manual refresh button.
- Status + message extraction from Spring Boot Actuator style responses.
- Stable polling loop (no re-subscribe on status changes).
Usage:
import HealthCheckCard from './components/HealthCheckCard';
export default function Home() {
return (
<div>
{/* other content */}
<HealthCheckCard />
</div>
);
}If you need a one-off health check somewhere else, prefer reusing this component to avoid duplicate intervals.
Add components under src/components/ and import into pages or App.tsx.
- Install deps:
npm install - Start dev server:
npm run dev(default http://localhost:5173) - Type-first build:
npm run build(runstsc -b && vite build) - Preview production bundle:
npm run preview - Bundle analyze (optional):
npm run analyze(generates a visual report of bundle composition) - Lint before commit:
npm run lint - Bundle analysis:
npm run analyzegeneratesdist/bundle-stats.htmland opens it.
- Initial app JS (gzip) target: ≤ 250 kB (soft gate; no CI block yet).
- Use the bundle analyzer to spot large libs and consider:
- code-splitting via dynamic
import() - lighter alternatives to heavy packages
- tree-shaking-friendly import paths
- code-splitting via dynamic
- (Optional) Auto hooks: Husky runs lint/tests/commitlint pre-push / commit.
All variables must be prefixed with VITE_ for exposure to the client bundle.
| Variable | Purpose | Notes |
|---|---|---|
VITE_API_BASE_URL |
Override backend origin (dev) | Fallback to relative / (proxy) when unset |
VITE_DEBUG_AUTH |
Enable verbose auth console diagnostics | Any truthy value enables extra logs |
VITE_DEBUG_AUTH_BOOTSTRAP |
Additional bootstrap heartbeat logs | Aids diagnosing early 401 noise |
VITE_HEALTH_REFRESH_MS |
Interval (ms) between health pings | Defaults to 30000; must be > 1000 |
VITE_APP_VERSION |
Build/app version string | Shown in footer when present |
VITE_COMMIT_HASH |
Git commit hash (fallback if version missing) | Truncated to 7 chars in footer |
VITE_FEEDBACK_URL |
External feedback / issue link | Defaults to repo issue creation URL |
You can create a .env.example enumerating these for contributors. Local overrides belong in .env.local (git‑ignored).
src/lib/api.ts exports a small generic api<T> wrapper around fetch.
All requests automatically include credentials: 'include' so the backend's
SBSESSION (httpOnly) cookie-based session flows transparently.
Structured error JSON (shape: { error, message }) is surfaced via err.cause.
Example:
const health = await api<HealthResponse>("/actuator/health");Add narrowers / runtime guards in src/lib/* (e.g., health.ts).
Use src/lib/env.ts to gate dev/test/prod behavior consistently across the app:
isDev()– true for Vite development buildsisProd()– true for Vite production buildsisTest()– true when running tests (Vitest)
Recommended patterns:
- Place
if (isProd()) return null;at the very top of dev-only components, before any React hooks, to prevent effects from running in production bundles. - At call-sites, conditionally render dev-only components with
{isDev() && <DevPanel />}for clarity and better tree-shaking.
- Health:
/actuator/health(public; no credentials). - Version:
/api/version(public; optional for display). - Auth (cookie session; always include credentials):
POST /api/auth/loginPOST /api/auth/registerPOST /api/auth/logoutGET /api/me(bootstrap + subsequent session checks)
Important: Do NOT call bare /me from the client. Always use /api/me to avoid ambiguity and align with backend policy and CSP/proxy rules.
The authentication layer uses a Zustand store exposed via a stable useAuth() hook. A thin AuthProvider wrapper mounts AuthEffects, which performs exactly one bootstrap fetch to /api/me to hydrate the session user (cookie-based) and listens for visibilitychange to refresh with a 30s cooldown. React 19 StrictMode double-mount is handled by a module-level guard inside AuthEffects to avoid duplicate bootstrap.
- On app mount,
AuthProvidercallsGET /api/meonce to determine if a session cookie is present and valid. - If the user is not logged in, the backend returns
401. This is expected and handled silently:- Before bootstrap completes, a
401is treated as “not signed in” (no error UI/state). - After bootstrap, subsequent
401s (e.g., expired session) surface as an auth error for the user to act on.
- Before bootstrap completes, a
- In dev tools, you will still see a red 401 entry in the Network/Console—that’s an intentional server response for unauthenticated users, and not an application error. The client catches and handles it without throwing.
- Extra logs only appear if you opt-in via
VITE_API_DEBUG_AUTH.
- Store + effects:
src/state/auth/store.ts,src/state/auth/effects.tsxwith a thin wrapper provider atsrc/state/auth/auth-context.tsx(mounts effects). - Actions:
login,register,refresh,logout,clearError. - Status:
statuscycles throughidle | loading | authenticated | errorwhilebootstrappedgates initial redirects. - Error Mapping: Server error payload (
{ error, message }) is normalized inerrors.tsto friendly messages (e.g.BAD_CREDENTIALS,EMAIL_EXISTS,VALIDATION_ERROR). - User Normalization: Responses may return
{ user: {...} }or raw user; normalization flattensroleintoroles[], surfacesdisplayName. - Protected Routes:
ProtectedRouteblocks redirect untilbootstrappedtrue; shows a Chakra spinner with framer-motion transitions. - Environment Debug Flags:
VITE_DEBUG_AUTH,VITE_DEBUG_AUTH_BOOTSTRAPtoggle verbose logging & heartbeats (useful for analyzing early 401 patterns).
- Forms rewritten using Chakra
FormControl,Input,FormErrorMessage, andButton. - Field-level client validation (email format, password length) surfaces inline errors while backend errors appear in a Chakra
Alert. - Redirect logic preserves original route (
location.state.from) after successful auth.
- On first mount: run
refresh()to attempt session hydration. - If unauthenticated (401) during bootstrap: treat as normal (no noisy error state).
- Post-bootstrap 401s (e.g. expiry): surface mapped error and allow UI to re-auth.
- A fail-safe timeout flips
bootstrappedafter 5s if the network call hangs.
Components import only useAuth() (adapter). Internals can evolve (selectors, middleware) without changing call sites, as long as the returned shape remains the same. See RFC: docs/rfcs/zustand-auth-store.md.
RuntimeDebugPanel (dev-only) provides an at-a-glance status overlay:
- Collapsible floating panel (bottom-right) with reduced-motion respect.
- Displays auth state (
status,bootstrapped,lastFetched), active user, and API base. - Probes
/actuator/health&/api/meevery 15s; maintains last 25 probe history entries. - Cumulative error count badge; copy buttons for API base & user ID.
- Manual refresh & force-refresh controls for diagnosing bootstrap issues.
Remove or disable this panel for production builds (guarded by isProd()). In this codebase, both RuntimeDebugPanel and UserSummary are DEV-only:
RuntimeDebugPanelshort-circuits at the top of the component ifisProd()and is rendered only whenisDev().UserSummaryis rendered only whenisDev().
This ensures they do not appear or run any effects in production, preventing unintended network probes (e.g., /api/me).
- Test runner: Vitest (
npm run test) - Environment: jsdom
- Libraries:
@testing-library/react,@testing-library/user-event,@testing-library/jest-dom
npm run test # Single run
npm run test:coverage # Run with v8 provider; thresholds (50%) enforced locally
npx vitest watch # Interactive watch (coverage summary on exit)Coverage thresholds (initial baseline): lines/functions/statements/branches ≥ 50%. PRs run coverage; CI fails below.
All test files live under src/__tests__/ (*.test.tsx for component/routes logic).
- Protected route access & redirect
- Client-side form validation for login/register (email format + password length)
- Import the component under test.
- Wrap with
AuthProvider+ a memory router if route context needed. - Mock network as required (e.g.
vi.spyOn(global, 'fetch')returning a resolvedResponse). - Use semantic queries (
getByRole,findByText) overquerySelector. - Assert side-effects (redirect) via
history(MemoryRouter entries) or presence of destination content.
vi.spyOn(global, "fetch").mockResolvedValueOnce(
new Response(
JSON.stringify({
id: "u1",
email: "a@b.com",
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
)
);- Successful login → redirected to
/me. - BAD_CREDENTIALS response →
<Alert>shows friendly message. - SESSION_EXPIRED path (simulate
/me401 after initial bootstrap) → redirect preserved.
- Keep auth store interactions synchronous except for actual network calls.
- Use
await waitFor(...)only for async UI transitions; prefer immediate assertions when possible. - Avoid leaking fetch mocks between tests – reset with
vi.restoreAllMocks()inafterEach.
- TypeScript clean (build runs
tsc -b). - ESLint passes (
npm run lint). - Tests pass; coverage ≥ thresholds.
- Changelog updated when source/docs change (
npm run changelog:check). - Conventional commit style enforced (Husky + commitlint).
- CI replicates local gates: lint → type → tests → changelog check → build.
- Dependency audit runs in CI: High/Critical vulnerabilities fail; Moderate/Low are reported but non-blocking (runtime deps only). See
docs/security/dependency-audit.md.
- Branch from
main(feat/*,fix/*). - Make changes; keep PRs small & focused.
- Ensure
npm run lint && npm run buildsucceeds. - Open PR; CI must be green before merge.
For extended guidelines see
CONTRIBUTING.md(to be added).
npm install
npm run devnpm run build- dev – start Vite dev server
- build – production build
- preview – preview production build locally
- test – run Vitest suite
- test:coverage – run Vitest with @vitest/coverage-v8 (thresholds enforced)
- analyze – build in analyze mode and generate
dist/bundle-stats.html(auto-opens) - release – run automated version + changelog update script
- changelog:check – enforce changelog entry presence
- lint / lint:fix – run (and optionally fix) ESLint
This project was migrated from ArchILLtect/sameboat-frontend to sameboat-platform/frontend.
If you previously cloned the old repository, update your git remote:
git remote set-url origin https://github.com/sameboat-platform/frontend.git
git fetch origin --pruneCI badge & links have been updated to the new organization path.
Track these as guidelines (non-blocking) and iterate once stable. Use a bundle analyzer locally and Lighthouse for quick checks.
- JavaScript (gzipped)
- Initial JS on home route ≤ 250 KB
- Largest single JS chunk ≤ 150 KB
- Initial JS requests ≤ 5
- CSS (gzipped): initial route ≤ 50 KB
- Web Vitals (lab targets)
- LCP ≤ 2.5s (desktop), ≤ 4.0s (simulated mid‑tier mobile)
- CLS ≤ 0.10
- TBT ≤ 200 ms (desktop lab)
- Images: avoid render‑blocking images on initial route; lazy‑load non‑critical assets
Later, you can add a non-failing CI job to post bundle/Lighthouse deltas on PRs and convert to hard gates once the app stabilizes.
- Local coverage artifacts (coverage/) are ignored by git; the CI workflow uploads coverage as PR artifacts and updates the badge on main after merge.
- If dependency audit fails on High/Critical, prefer updating direct deps. If needed, use
overridesinpackage.jsonto force patched transitives:As a last resort, use{ "overrides": { "pkg@^1": "1.2.3" } }patch-packageand remove once upstream publishes a fix. Temporary allowlists viaaudit-cishould have an expiration and a tracking issue.