# Costco Grocery List - AI Agent Instructions ## Architecture Overview This is a full-stack grocery list management app with **role-based access control (RBAC)**: - **Backend**: Node.js + Express + PostgreSQL (port 5000) - **Frontend**: React 19 + TypeScript + Vite (port 3000/5173) - **Deployment**: Docker Compose with separate dev/prod configurations ## Mobile-First Design Principles **CRITICAL**: All UI components MUST be designed for both mobile and desktop from the start. **Responsive Design Requirements**: - Use relative units (`rem`, `em`, `%`, `vh/vw`) over fixed pixels where possible - Implement mobile breakpoints: `480px`, `768px`, `1024px` - Test layouts at: 320px (small phone), 375px (phone), 768px (tablet), 1024px+ (desktop) - Avoid horizontal scrolling on mobile devices - Touch targets minimum 44x44px for mobile usability - Use `max-width` with `margin: 0 auto` for content containers - Stack elements vertically on mobile, use flexbox/grid for larger screens - Hide/collapse navigation into hamburger menus on mobile - Ensure modals/dropdowns work well on small screens **Common Patterns**: ```css /* Mobile-first approach */ .container { padding: 1rem; max-width: 100%; } @media (min-width: 768px) { .container { padding: 2rem; max-width: 800px; margin: 0 auto; } } ``` ### Key Design Patterns **Dual RBAC System** - Two separate role hierarchies: **1. System Roles** (users.role column): - `system_admin`: Access to Admin Panel for system-wide management (stores, users) - `user`: Regular system user (default for new registrations) - Defined in [backend/models/user.model.js](backend/models/user.model.js) - Used for Admin Panel access control **2. Household Roles** (household_members.role column): - `admin`: Can manage household members, change roles, delete household - `user`: Can add/edit items, mark as bought (standard member permissions) - Defined per household membership - Used for household-level permissions (item management, member management) **Important**: Always distinguish between system role and household role: - **System role**: From `AuthContext` or `req.user.role` - controls Admin Panel access - **Household role**: From `activeHousehold.role` or `household_members.role` - controls household operations **Middleware chain pattern** for protected routes: ```javascript // System-level protection router.get("/stores", auth, requireRole("system_admin"), controller.getAllStores); // Household-level checks done in controller router.post("/lists/:householdId/items", auth, controller.addItem); ``` - `auth` middleware extracts JWT from `Authorization: Bearer ` header - `requireRole` checks system role only - Household role checks happen in controllers using `household.model.js` methods **Frontend route protection**: - ``: Requires authentication, redirects to `/login` if no token - ``: Requires system_admin role for Admin Panel - Household permissions: Check `activeHousehold.role` in components (not route-level) - Example in [frontend/src/App.jsx](frontend/src/App.jsx) **Multi-Household Architecture**: - Users can belong to multiple households - Each household has its own grocery lists, stores, and item classifications - `HouseholdContext` manages active household selection - All list operations are scoped to the active household ## Database Schema **PostgreSQL server runs externally** - not in Docker Compose. Connection configured in [backend/.env](backend/.env) via standard environment variables. **Core Tables**: **users** - System users - `id` (PK), `username`, `password` (bcrypt), `name`, `display_name` - `role`: `system_admin` | `user` (default: `viewer` - legacy) - System-level authentication and authorization **households** - Household entities - `id` (PK), `name`, `invite_code`, `created_by`, `created_at` - Each household is independent with own lists and members **household_members** - Junction table (users ↔ households) - `id` (PK), `household_id` (FK), `user_id` (FK), `role`, `joined_at` - `role`: `admin` | `user` (household-level permissions) - One user can belong to multiple households with different roles **items** - Master item catalog - `id` (PK), `name`, `default_image`, `default_image_mime_type`, `usage_count` - Shared across all households, case-insensitive unique names **stores** - Store definitions (system-wide) - `id` (PK), `name`, `default_zones` (JSONB array) - Managed by system_admin in Admin Panel **household_stores** - Stores available to each household - `id` (PK), `household_id` (FK), `store_id` (FK), `is_default` - Links households to stores they use **household_lists** - Grocery list items per household - `id` (PK), `household_id` (FK), `store_id` (FK), `item_id` (FK) - `quantity`, `bought`, `custom_image`, `custom_image_mime_type` - `added_by`, `modified_on` - Scoped to household + store combination **household_list_history** - Tracks quantity contributions - `id` (PK), `household_list_id` (FK), `quantity`, `added_by`, `added_on` - Multi-contributor tracking (who added how much) **household_item_classifications** - Item classifications per household/store - `id` (PK), `household_id`, `store_id`, `item_id` - `item_type`, `item_group`, `zone`, `confidence`, `source` - Household-specific overrides of global classifications **item_classification** - Global item classifications - `id` (PK), `item_type`, `item_group`, `zone`, `confidence`, `source` - System-wide defaults for item categorization **Legacy Tables** (deprecated, may still exist): - `grocery_list`, `grocery_history` - Old single-household implementation **Important patterns**: - No formal migration system - schema changes are manual SQL - Items use case-insensitive matching (`ILIKE`) to prevent duplicates - JOINs with `ARRAY_AGG` for multi-contributor queries (see [backend/models/list.model.v2.js](backend/models/list.model.v2.js)) - All list operations require `household_id` parameter for scoping - Image storage: `bytea` columns for images with separate MIME type columns ## Development Workflow ### Local Development ```bash # Start all services with hot-reload against LOCAL database docker-compose -f docker-compose.dev.yml up # Backend runs nodemon (watches backend/*.js) # Frontend runs Vite dev server with HMR on port 3000 ``` **Key dev setup details**: - Volume mounts preserve `node_modules` in containers while syncing source code - Backend uses `Dockerfile` (standard) with `npm run dev` override - Frontend uses `Dockerfile.dev` with `CHOKIDAR_USEPOLLING=true` for file watching - Both connect to **external PostgreSQL server** (configured in `backend/.env`) - No database container in compose - DB is managed separately ### Production Build ```bash # Local production build (for testing) docker-compose -f docker-compose.prod.yml up --build # Actual production uses pre-built images docker-compose up # Pulls from private registry ``` ### CI/CD Pipeline (Gitea Actions) See [.gitea/workflows/deploy.yml](.gitea/workflows/deploy.yml) for full workflow: **Build stage** (on push to `main`): 1. Run backend tests (`npm test --if-present`) 2. Build backend image with tags: `:latest` and `:` 3. Build frontend image with tags: `:latest` and `:` 4. Push both images to private registry **Deploy stage**: 1. SSH to production server 2. Upload `docker-compose.yml` to deployment directory 3. Pull latest images and restart containers with `docker compose up -d` 4. Prune old images **Notify stage**: - Sends deployment status via webhook **Required secrets**: - `REGISTRY_USER`, `REGISTRY_PASS`: Docker registry credentials - `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_KEY`: SSH deployment credentials ### Backend Scripts - `npm run dev`: Start with nodemon - `npm run build`: esbuild compilation + copy public assets to `dist/` - `npm test`: Run Jest tests (currently no tests exist) ### Frontend Scripts - `npm run dev`: Vite dev server (port 5173) - `npm run build`: TypeScript compilation + Vite production build ### Docker Configurations **docker-compose.yml** (production): - Pulls pre-built images from private registry - Backend on port 5000, frontend on port 3000 (nginx serves on port 80) - Requires `backend.env` and `frontend.env` files **docker-compose.dev.yml** (local development): - Builds images locally from Dockerfile/Dockerfile.dev - Volume mounts for hot-reload: `./backend:/app` and `./frontend:/app` - Named volumes preserve `node_modules` between rebuilds - Backend uses `backend/.env` directly - Frontend uses `Dockerfile.dev` with polling enabled for cross-platform compatibility **docker-compose.prod.yml** (local production testing): - Builds images locally using production Dockerfiles - Backend: Standard Node.js server - Frontend: Multi-stage build with nginx serving static files ## Configuration & Environment **Backend** ([backend/.env](backend/.env)): - Database connection variables (host, user, password, database name) - `JWT_SECRET`: Token signing key - `ALLOWED_ORIGINS`: Comma-separated CORS whitelist (supports static origins + `192.168.*.*` IP ranges) - `PORT`: Server port (default 5000) **Frontend** (environment variables): - `VITE_API_URL`: Backend base URL **Config accessed via**: - Backend: `process.env.VAR_NAME` - Frontend: `import.meta.env.VITE_VAR_NAME` (see [frontend/src/config.ts](frontend/src/config.ts)) ## Authentication Flow 1. User logs in → backend returns `{token, userId, role, username}` ([backend/controllers/auth.controller.js](backend/controllers/auth.controller.js)) - `role` is the **system role** (`system_admin` or `user`) 2. Frontend stores in `localStorage` and `AuthContext` ([frontend/src/context/AuthContext.jsx](frontend/src/context/AuthContext.jsx)) 3. `HouseholdContext` loads user's households and sets active household - Active household includes `household.role` (the **household role**) 4. Axios interceptor auto-attaches `Authorization: Bearer ` header ([frontend/src/api/axios.js](frontend/src/api/axios.js)) 5. Backend validates JWT on protected routes ([backend/middleware/auth.js](backend/middleware/auth.js)) - Sets `req.user = { id, role, username }` with **system role** 6. Controllers check household membership/role using [backend/models/household.model.js](backend/models/household.model.js) 7. On 401 "Invalid or expired token" response, frontend clears storage and redirects to login ## Critical Conventions ### Security Practices - **Never expose credentials**: Do not hardcode or document actual values for `JWT_SECRET`, database passwords, API keys, or any sensitive configuration - **No infrastructure details**: Avoid documenting specific IP addresses, domain names, deployment paths, or server locations in code or documentation - **Environment variables**: Reference `.env` files conceptually - never include actual contents - **Secrets in CI/CD**: Document that secrets are required, not their values - **Code review**: Scan all changes for accidentally committed credentials before pushing ### Backend - **No SQL injection**: Always use parameterized queries (`$1`, `$2`, etc.) with [backend/db/pool.js](backend/db/pool.js) - **Password hashing**: Use `bcryptjs` for hashing (see [backend/controllers/auth.controller.js](backend/controllers/auth.controller.js)) - **CORS**: Dynamic origin validation in [backend/app.js](backend/app.js) allows configured origins + local IPs - **Error responses**: Return JSON with `{message: "..."}` structure ### Frontend - **Mixed JSX/TSX**: Some components are `.jsx` (JavaScript), others `.tsx` (TypeScript) - maintain existing file extensions - **API calls**: Use centralized `api` instance from [frontend/src/api/axios.js](frontend/src/api/axios.js), not raw axios - **Role checks**: Access role from `AuthContext`, compare with constants from [frontend/src/constants/roles.js](frontend/src/constants/roles.js) - **Navigation**: Use React Router's `` for redirects, not `window.location` (except in interceptor) ## Common Tasks **Add a new protected route**: 1. Backend: Add route with `auth` middleware (+ `requireRole(...)` if system role check needed) 2. Frontend: Add route in [frontend/src/App.jsx](frontend/src/App.jsx) wrapped in `` (and `` for Admin Panel) **Access user info in backend controller**: ```javascript const { id, role } = req.user; // Set by auth middleware (system role) const userId = req.user.id; ``` **Check household permissions in backend controller**: ```javascript const householdRole = await household.getUserRole(householdId, userId); if (!householdRole) return res.status(403).json({ message: "Not a member of this household" }); if (householdRole !== 'admin') return res.status(403).json({ message: "Household admin required" }); ``` **Check household permissions in frontend**: ```javascript const { activeHousehold } = useContext(HouseholdContext); const householdRole = activeHousehold?.role; // 'admin' or 'user' // Allow all members except viewers (no viewer role in households) const canManageItems = householdRole && householdRole !== 'viewer'; // Usually just check if role exists // Admin-only actions const canManageMembers = householdRole === 'admin'; ``` **Query grocery items with contributors**: Use the JOIN pattern in [backend/models/list.model.v2.js](backend/models/list.model.v2.js) - aggregates user names via `household_list_history` table. ## Testing **Backend**: - Jest configured at root level ([package.json](package.json)) - Currently **no test files exist** - testing infrastructure needs development - CI/CD runs `npm test --if-present` but will pass if no tests found - Focus area: API endpoint testing (use `supertest` with Express) **Frontend**: - ESLint only (see [frontend/eslint.config.js](frontend/eslint.config.js)) - No test runner configured - Manual testing workflow in use **To add backend tests**: 1. Create `backend/__tests__/` directory 2. Use Jest + Supertest pattern for API tests 3. Mock database calls or use test database