---
inclusion: always
---

# Technology Stack: Gearious (Web)

A working example of a tech-stack steering file. Abridged for sharing.

This file exists so implementation choices stay consistent without re-deriving them each session.

**Scope:** Web application only. Don't add mobile-specific coupling.

## Stack

| Layer | Technology | Notes |
| --- | --- | --- |
| Framework | Next.js (App Router) | Latest stable. **App Router only — no Pages Router.** |
| UI | React | Bundled with Next.js. |
| Styling | Tailwind CSS | **Utility classes only. No custom CSS files or class names.** |
| Components | shadcn/ui | Installed per-component via CLI. |
| Language | TypeScript | **Strict mode throughout.** |
| Server state | TanStack Query | All server data fetching, caching, and mutations. |
| Client state | Zustand | UI state only — not server data. |
| Drag & drop | dnd-kit | Trip Planner bag assignment. |
| Backend | Next.js API Routes | Co-located. No separate service. |
| Database / Auth / Storage | Supabase | Postgres, Auth, and Storage. Row-level security on every table. |
| Hosting | Vercel | Zero-config Next.js deploy. |
| Source control | GitHub | Source of truth. |

Third-party services for email and payments are added when those features exist — not before.

## Non-Negotiable Architectural Decisions

- **App Router with RSC for public pages**; Client Components for interactive surfaces.
- **Full-stack in one codebase.** Next.js API routes are the backend.
- **Supabase is the infrastructure layer** — database, auth, and storage. No separate auth service or object store.
- **Row Level Security is the primary data-isolation mechanism**, enforced at the database on every table. Not application-layer filtering.
- **The balance engine runs client-side** as a pure function — no API call on every drag. Results sync to the server after the interaction completes.
- **Two environments only:** local and production. No staging.
- **Optimistic updates** — UI updates immediately, the API confirms in the background, and state rolls back on error.

## Auth

- Enforce a strong password policy **server-side** (identity provider is the source of truth). Mirror the same rules in application code for inline feedback — never rely on the client as enforcement.
- Re-verify identity before destructive account actions.
- Sign-in error copy must never reveal whether the email or the password was wrong.
- OAuth-only accounts shouldn't be shown password-change UI, and the API must reject those requests even if the UI is bypassed.

## Data & Weight Conventions

These prevent the most damaging class of bug, so they are absolute:

- **All weights are stored and transmitted as integer grams.** Never decimal, float, lbs, or kg in the database or in any request/response body. Unit conversion happens *only* at the client display layer, in one shared helper, nowhere else.
- **All primary keys are UUID.** Never integer sequences.
- **Hard deletes only** — no soft-delete patterns.
- Every table has `created_at` and `updated_at`, with `updated_at` maintained by trigger.
- User-owned rows cascade from the profile. Don't invent a second ownership model.
- Sensitive personal fields (body weight) are encrypted at the application layer in addition to database at-rest encryption. Keys stay server-only. **Never log them, never send them to the client.**

## The Balance Engine

- A **pure function**. No side effects, no API calls, no React, no imports from the rest of the codebase. It must stay unit-testable in isolation.
- Called on every planner render. Keep it fast enough to feel instant. Don't add async or fetching to it.
- Backpack weight is tracked separately from bike load.
- Thresholds and formulas live in the project, not in this public copy. Don't invent new ones in a session — read the source of truth in code and tests.

## API Conventions

- Every authenticated route reads the session and returns **401** on a missing/invalid session.
- **A valid id belonging to another user returns 404, never 403** — never confirm that another user's resource exists.
- Validate the request body before any database call; invalid input returns **400**.
- Errors use one shape, via a shared helper:
  ```json
  { "error": "Human-readable message", "code": "OPTIONAL_CODE" }
  ```
- Form validation errors are per-field. Reserve a form-level banner for errors that aren't about one field.
- Toasts are for mutation failures and rollbacks. Don't use a toast for a field the user can still see.

## Naming Conventions

| Item | Convention | Example |
| --- | --- | --- |
| React components | PascalCase `.tsx` | `WeightDashboard.tsx` |
| Non-component modules | kebab-case `.ts` | `balance-engine.ts` |
| API routes | `route.ts` (Next.js standard) | `app/api/trips/[id]/route.ts` |
| Zustand stores | kebab-case, `-store` suffix | `trip-planner-store.ts` |
| TanStack Query hooks | camelCase, `use` prefix | `useTrip.ts` |
| Database tables | snake_case | `trip_items` |
| TypeScript types | PascalCase | `GearItem` |
| Env variables | SCREAMING_SNAKE_CASE | `NEXT_PUBLIC_SUPABASE_URL` |

## Environment Variables

Defined in `.env.local` locally (never committed) and in the host's project settings for production.

- Only values that are safe to expose in the browser may carry a `NEXT_PUBLIC_` prefix.
- **Never prefix a secret with `NEXT_PUBLIC_`.**
- Service-role keys, encryption keys, and payment secrets are server-only. Don't import them from Client Components.

## Testing

Minimal and focused on where a bug is most damaging.

- **Balance engine — critical.** Passing unit tests before the core loop ships. An incorrect weight or balance figure is the worst possible outcome.
- Weight utilities — high priority, unit tested.
- Auth, limits, and full user journeys — verified manually until they earn automated coverage.

## Commands

```
npm run dev        # Start dev server
npm test           # Unit tests in watch mode
npm run test:run   # Unit tests once
npm run typecheck  # TypeScript checking
npm run lint       # ESLint
```

Database changes: always add a **new** migration. Never edit an existing one. Regenerate types after schema changes.

## Deployment

Git-connected host; every push to `main` deploys to production; every PR gets a preview URL. Production secrets live only in the host's project settings, never in `.env.local`.
