14 KiB
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-widthwithmargin: 0 autofor 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:
/* 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
- Used for Admin Panel access control
2. Household Roles (household_members.role column):
admin: Can manage household members, change roles, delete householduser: 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
AuthContextorreq.user.role- controls Admin Panel access - Household role: From
activeHousehold.roleorhousehold_members.role- controls household operations
Middleware chain pattern for protected routes:
// 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);
authmiddleware extracts JWT fromAuthorization: Bearer <token>headerrequireRolechecks system role only- Household role checks happen in controllers using
household.model.jsmethods
Frontend route protection:
<PrivateRoute>: Requires authentication, redirects to/loginif no token<RoleGuard allowed={[ROLES.SYSTEM_ADMIN]}>: Requires system_admin role for Admin Panel- Household permissions: Check
activeHousehold.rolein components (not route-level) - Example in frontend/src/App.jsx
Multi-Household Architecture:
- Users can belong to multiple households
- Each household has its own grocery lists, stores, and item classifications
HouseholdContextmanages 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 via standard environment variables.
Core Tables:
users - System users
id(PK),username,password(bcrypt),name,display_namerole: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_atrole: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_typeadded_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_iditem_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_AGGfor multi-contributor queries (see backend/models/list.model.v2.js) - All list operations require
household_idparameter for scoping - Image storage:
byteacolumns for images with separate MIME type columns
Development Workflow
Local Development
# 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_modulesin containers while syncing source code - Backend uses
Dockerfile(standard) withnpm run devoverride - Frontend uses
Dockerfile.devwithCHOKIDAR_USEPOLLING=truefor file watching - Both connect to external PostgreSQL server (configured in
backend/.env) - No database container in compose - DB is managed separately
Production Build
# 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 for full workflow:
Build stage (on push to main):
- Run backend tests (
npm test --if-present) - Build backend image with tags:
:latestand:<commit-sha> - Build frontend image with tags:
:latestand:<commit-sha> - Push both images to private registry
Deploy stage:
- SSH to production server
- Upload
docker-compose.ymlto deployment directory - Pull latest images and restart containers with
docker compose up -d - Prune old images
Notify stage:
- Sends deployment status via webhook
Required secrets:
REGISTRY_USER,REGISTRY_PASS: Docker registry credentialsDEPLOY_HOST,DEPLOY_USER,DEPLOY_KEY: SSH deployment credentials
Backend Scripts
npm run dev: Start with nodemonnpm run build: esbuild compilation + copy public assets todist/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.envandfrontend.envfiles
docker-compose.dev.yml (local development):
- Builds images locally from Dockerfile/Dockerfile.dev
- Volume mounts for hot-reload:
./backend:/appand./frontend:/app - Named volumes preserve
node_modulesbetween rebuilds - Backend uses
backend/.envdirectly - Frontend uses
Dockerfile.devwith 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):
- Database connection variables (host, user, password, database name)
JWT_SECRET: Token signing keyALLOWED_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)
Authentication Flow
- User logs in → backend returns
{token, userId, role, username}(backend/controllers/auth.controller.js)roleis the system role (system_adminoruser)
- Frontend stores in
localStorageandAuthContext(frontend/src/context/AuthContext.jsx) HouseholdContextloads user's households and sets active household- Active household includes
household.role(the household role)
- Active household includes
- Axios interceptor auto-attaches
Authorization: Bearer <token>header (frontend/src/api/axios.js) - Backend validates JWT on protected routes (backend/middleware/auth.js)
- Sets
req.user = { id, role, username }with system role
- Sets
- Controllers check household membership/role using backend/models/household.model.js
- 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
.envfiles 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 - Password hashing: Use
bcryptjsfor hashing (see backend/controllers/auth.controller.js) - CORS: Dynamic origin validation in 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
apiinstance from frontend/src/api/axios.js, not raw axios - Role checks: Access role from
AuthContext, compare with constants from frontend/src/constants/roles.js - Navigation: Use React Router's
<Navigate>for redirects, notwindow.location(except in interceptor)
Common Tasks
Add a new protected route:
- Backend: Add route with
authmiddleware (+requireRole(...)if system role check needed) - Frontend: Add route in frontend/src/App.jsx wrapped in
<PrivateRoute>(and<RoleGuard>for Admin Panel)
Access user info in backend controller:
const { id, role } = req.user; // Set by auth middleware (system role)
const userId = req.user.id;
Check household permissions in backend controller:
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:
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 - aggregates user names via household_list_history table.
Testing
Backend:
- Jest configured at root level (package.json)
- Currently no test files exist - testing infrastructure needs development
- CI/CD runs
npm test --if-presentbut will pass if no tests found - Focus area: API endpoint testing (use
supertestwith Express)
Frontend:
- ESLint only (see frontend/eslint.config.js)
- No test runner configured
- Manual testing workflow in use
To add backend tests:
- Create
backend/__tests__/directory - Use Jest + Supertest pattern for API tests
- Mock database calls or use test database