A collaborative grocery list application built with React, TypeScript, and Vite. Features real-time synchronization, multi-user list sharing, and Progressive Web App (PWA) capabilities for offline use and app installation.
Phase 15 introduces a complete multi-user collaboration system that transforms the Grocery List app into a powerful shared shopping tool:
- 📋 Multi-List Management: Create unlimited lists for different purposes (weekly shopping, party planning, etc.)
- 👥 Smart Sharing: Share lists with family, roommates, or friends via email or shareable invite links
- 🔐 Permission Control: Three-tier system (owner/editor/viewer) for fine-grained access control
- 🤝 Real-Time Collaboration: See changes from all members instantly with <500ms sync latency
- 📊 Rich Analytics: View detailed statistics, activity history, and member contributions
- 🎨 Customization: Personalize lists with colors, icons, pinning, and archiving
- 💾 Export & Backup: Export lists to JSON, CSV, or plain text formats
Technical Highlights:
- 15+ new API endpoints with comprehensive validation
- 9 database migrations with rollback support
- 3,500+ lines of production-ready code
- 88+ test scenarios covering all features
- Mobile-responsive UI optimized for touch devices
- Real-time permission enforcement at API and UI levels
- ✅ Add Items: Add grocery items with name, quantity, category, and optional notes
- 🏷️ Categories: Organize items into categories (Produce, Dairy, Meat, Bakery, Pantry, Frozen, Beverages, Other)
- 📝 Notes: Add optional notes to items (brand preferences, location in store, etc.)
- 💰 Price Tracking: Add prices to items in 10+ currencies with inline editing
- 💵 Budget Management: Set list budgets and track spending with real-time progress
- 📊 Budget Alerts: Visual indicators and alerts when approaching or exceeding budget
- 📋 List Templates: Quick-start your list with 9 pre-built templates (Weekly Groceries, Party Supplies, Quick Dinner, etc.)
- 🔍 Template Search: Find templates by name, description, or individual items
- ✅ Mark as Gotten: Toggle items as gotten/not gotten
- ✅ Delete Items: Remove items from the list
- ✅ View List: See all items with customizable sorting
- 🔍 Search: Search for items by name with real-time filtering
- 🎛️ Filter: Toggle visibility of gotten items and filter by categories
- 📊 Results Counter: See the number of items matching your filters
- 🔄 Sort: Sort items by name, quantity, date, or category (ascending/descending)
- ⚡ Bulk Operations: Mark all items as gotten or delete all gotten items at once
- 📋 Multiple Lists: Create and manage unlimited grocery lists
- 👥 Share with Users: Share lists with other users via email
- 🔐 Permission Levels: Three roles (owner, editor, viewer) for fine-grained access control
- 🔗 Invite Links: Generate shareable links with optional expiration dates
- 🤝 Real-time Collaboration: See changes from all list members instantly
- 👤 Member Management: Add, remove, and update member permissions
- 🔄 Transfer Ownership: Transfer list ownership to another member
- 📑 Duplicate Lists: Clone lists with all items for reuse
- 🚪 Leave Lists: Leave shared lists you no longer need
- 📊 List Statistics: View detailed analytics and activity history
- 📋 Activity Trail: Complete audit log of all list actions
- 🎨 List Customization: Customize lists with colors and icons
- 📌 Pin Lists: Pin favorite lists to the top
- 📦 Archive Lists: Archive old lists without deleting them
- 💾 Export Lists: Export to JSON, CSV, or plain text formats
- 🖨️ Print Lists: Print-friendly list formatting
- 🍳 Recipe Management: Create and store unlimited recipes with ingredients
- 📝 Ingredient Lists: Add ingredients with quantities, units, and categories
- ⏱️ Cooking Details: Track prep time, cook time, servings, and difficulty level
- 🌍 Cuisine Types: Categorize recipes by cuisine (Italian, Mexican, Asian, etc.)
- 📸 Recipe Images: Add images to your recipes via URL
- 🔍 Recipe Search: Search recipes by name, description, or ingredients
- 🎯 Filter & Sort: Filter by difficulty, cuisine type; sort by name, date, cooking time
- 📅 Weekly Meal Planner: Visual calendar view for planning 7 days of meals
- 🍽️ 4 Meal Types: Plan breakfast, lunch, dinner, and snacks for each day
- ✅ Cooking Tracker: Mark meals as cooked to track your progress
- 🛒 Auto Shopping Lists: Generate shopping lists from your meal plans
- 🎯 Smart Aggregation: Automatically combine duplicate ingredients
- 📚 Recipe Collections: Organize recipes into collections (e.g., "Italian Favorites", "Quick Meals")
- 🌐 Recipe Sharing: Make recipes public for others to discover and duplicate
- 👤 Recipe Attribution: See author names on public recipes
- 🔄 Recipe Duplication: Copy any recipe (yours or public) to customize it
- 📊 Meal Overview: See all planned meals for the week at a glance
- 🔢 Servings Override: Adjust servings per meal when planning
- 📝 Meal Notes: Add notes to individual meal plans
Learn More:
- Recipe Integration Guide - Complete user guide
- Recipe API Reference - API documentation
- Phase 26 Complete - Implementation details
- 🔐 JWT Authentication: Secure token-based authentication
- 👤 User Accounts: Register and login with email/password
- 🔄 Auto Token Refresh: Seamless session management
- 🛡️ Rate Limiting: Brute-force protection on auth endpoints
- 🔒 Password Security: bcrypt hashing with 12 rounds
- 👥 Multi-User Support: Each user has isolated data
- 📱 Install as App: Install on home screen (Android, iOS, Desktop)
- 🔌 Works Offline: Full functionality without internet connection
- 🔄 Background Sync: Automatically syncs changes when online (Android, Desktop)
- 🔔 Push Notifications: Get notified of list updates (Android, Desktop)
- ⚡ Fast Loading: Instant load times with intelligent caching
- 🎨 Native Feel: Full-screen mode, looks like a native app
- 🔄 Auto Updates: Automatically updates to latest version
- 💾 Offline Queue: Changes queue locally and sync when reconnected
Learn More:
- PWA User Guide - Complete installation and usage guide
- Quick Start Guide - Get started in minutes
- FAQ - Frequently asked questions
- Browser Support - Compatibility matrix
- 🔄 Real-time Sync: Changes sync automatically via Zero across all devices and users
- 💾 Persistent: Data stored in PostgreSQL with local caching
- 📱 Responsive: Mobile-first design that works on all screen sizes
- 🔌 Offline Support: Works offline and syncs when reconnected
- ⚡ Performance: Optimized queries with database indexes
- 🔍 Type Safety: Fully typed with TypeScript
The Grocery List app includes comprehensive price tracking and budget management features to help you stay on top of your grocery spending. Track individual item prices, set budgets for your shopping lists, and get real-time insights into your spending patterns.
Budget management features help you:
- Track prices for individual grocery items
- Set and monitor budgets for each shopping list
- See real-time spending totals and budget progress
- Get alerts when approaching or exceeding your budget
- View detailed price statistics and spending insights
- Support for multiple currencies
You can add prices to items when creating new items:
- Fill in the item name, quantity, and category as usual
- Enter the price in the Price field (optional)
- Select your currency from the dropdown (defaults to USD)
- Click "Add Item" to add the item with its price
Example:
- Item: Apples
- Quantity: 5
- Price: 12.50 USD
- The total cost for this item will be calculated automatically (5 × $12.50 = $62.50)
Supported Currencies:
- USD - US Dollar ($)
- EUR - Euro (€)
- GBP - British Pound (£)
- CAD - Canadian Dollar (CA$)
- AUD - Australian Dollar (A$)
- JPY - Japanese Yen (¥)
- INR - Indian Rupee (₹)
- CNY - Chinese Yuan (¥)
- MXN - Mexican Peso ($)
- BRL - Brazilian Real (R$)
Notes:
- Price field is optional - items without prices won't affect budget calculations
- Prices are stored per unit (the price you enter is multiplied by quantity)
- You can use any currency for each item independently
- Decimal prices are supported (e.g., 2.99, 10.50)
You can edit prices inline on existing items:
- Find the item you want to update
- Click the Edit Price button (pencil icon) next to the price
- Enter the new price in the input field
- Select a different currency if needed
- Click the Save button (checkmark) to save changes
- Or click Cancel (X) to discard changes
Features:
- Real-time validation ensures valid price formats
- Changes sync immediately across all devices
- Budget tracker updates automatically when prices change
- Edit multiple items' prices independently
- All list members with editor permission can update prices
List owners and editors can set budgets through List Management:
- Click the "Manage List" button (gear icon) at the top of the page
- Go to the "General" tab
- Find the "Budget" section
- Enter your budget amount (e.g., 150.00)
- Select your budget currency from the dropdown
- Click "Save" or "Update Budget"
- The budget tracker will appear on your list showing spending progress
Budget Features:
- Each list can have its own independent budget
- Budget currency is separate from item currencies (mixed currency support)
- Budget is optional - lists without budgets won't show the budget tracker
- Only owners and editors can set or modify budgets
- Budget amounts are stored with the list and visible to all members
Once a budget is set, the Budget Tracker component automatically appears at the top of your shopping list, providing real-time spending insights:
The tracker shows:
- Total Budget: Your set budget amount with currency symbol
- Total Spending: Sum of all item prices (quantity × unit price) in your items
- Remaining Budget: How much you have left to spend
- Budget Progress Bar: Visual indicator of spending progress with color-coded alerts
The progress bar uses color coding to show your budget status:
-
Green (0-70% used): You're well within budget
- "Great! You're under budget"
- Plenty of budget remaining
-
Yellow/Orange (70-90% used): Approaching your limit
- "You're approaching your budget limit"
- Consider reviewing items or adjusting budget
-
Red (90-100% used): Near or over budget
- "Warning: You're very close to your budget!"
- Time to cut back or increase budget
-
Dark Red (Over 100%): Over budget
- "Alert: You've exceeded your budget!"
- Current spending exceeds set budget
The tracker displays detailed statistics:
- Percentage Used: Shows what percentage of budget is spent (e.g., "85.3% of budget used")
- Total Items: Count of items with prices (e.g., "12 items with prices")
- Average Price: Average cost per item with price (e.g., "Average: $8.50 per item")
- Items Without Prices: Count of items that don't have prices set
The tracker provides contextual alerts based on your spending:
Under Budget (< 70%):
✓ Great! You're under budget
You have $45.00 remaining of your $150.00 budget
Approaching Limit (70-90%):
⚠ You're approaching your budget limit
You have $18.75 remaining of your $150.00 budget
Close to Limit (90-100%):
⚠ Warning: You're very close to your budget!
You have $7.50 remaining of your $150.00 budget
Over Budget (> 100%):
⚠ Alert: You've exceeded your budget!
You are $15.00 over your $150.00 budget
Additional insights shown in the tracker:
- Total Spending Breakdown: See spending by category (if items are categorized)
- Items with vs without Prices: Track which items still need pricing
- Real-time Updates: All statistics update instantly as you add, edit, or remove items
- Multi-currency Support: Displays all amounts in the budget's currency
The app supports 10 major world currencies for maximum flexibility:
| Currency | Code | Symbol | Example |
|---|---|---|---|
| US Dollar | USD | $ | $12.50 |
| Euro | EUR | € | €12.50 |
| British Pound | GBP | £ | £12.50 |
| Canadian Dollar | CAD | CA$ | CA$12.50 |
| Australian Dollar | AUD | A$ | A$12.50 |
| Japanese Yen | JPY | ¥ | ¥1250 |
| Indian Rupee | INR | ₹ | ₹925 |
| Chinese Yuan | CNY | ¥ | ¥85 |
| Mexican Peso | MXN | $ | $235 |
| Brazilian Real | BRL | R$ | R$65 |
Currency Features:
- Each item can have its own currency
- Budget can be set in any supported currency
- Currency symbols are displayed correctly for each locale
- Automatic formatting based on currency (e.g., 2 decimals for USD, 0 for JPY)
- Currency conversion is not automatic - all amounts are treated independently
Note: The app does not perform automatic currency conversion. If you use multiple currencies in a single list, budget tracking will sum amounts as-is without conversion. For accurate budget tracking, it's recommended to use a single currency per list.
-
Add Prices to All Items: The budget tracker only includes items with prices. Add prices to all items for accurate totals.
-
Use Consistent Currency: Stick to one currency per list to avoid confusion and ensure accurate budget tracking.
-
Update Prices Regularly: If you're using estimated prices, update them with actual prices when shopping to track real spending.
-
Set Realistic Budgets: Base your budget on historical spending patterns. Review and adjust as needed.
-
Review Before Shopping: Check the budget tracker before heading to the store to see if you're within budget.
-
Start with Estimates: If you don't know exact prices, add estimates and update them later. Some tracking is better than none.
-
Use Notes for Price Sources: Add notes to items indicating where prices came from (e.g., "Price from last week", "Online price").
-
Track Price Changes: Edit item prices when you notice changes at the store to build a price history.
-
Set Buffer Room: Set your budget 10-15% lower than your actual limit to account for price fluctuations and impulse purchases.
-
Categories Help: Organize items by category to see which categories consume most of your budget.
-
Communicate Budget: Make sure all list editors know the budget limit before adding items.
-
Assign Responsibility: Designate one person to track and update prices for accuracy.
-
Review Together: Review the budget tracker with all members before shopping trips.
-
Use for Planning: Use budget limits to guide discussions about what to buy vs what to skip.
-
Respect Viewer Access: Viewers can see budgets and prices but cannot edit them - perfect for shoppers who need to stay within limits.
Weekly Grocery Shopping:
- Set a weekly budget (e.g., $150)
- Add all planned items with estimated prices
- Adjust quantities if over budget
- Track actual prices when shopping
- Compare actual vs estimated spending
Special Event Planning:
- Set a total event budget
- Create categories for different event needs
- Track spending across categories
- Make adjustments to stay within budget
- Share with co-organizers for transparency
Monthly Budgeting:
- Create separate lists for each week
- Set consistent weekly budgets
- Track spending trends over time
- Identify items that are getting more expensive
- Plan better for future months
Budget-Conscious Shopping:
- Set a strict budget limit
- Add desired items with prices
- Remove or reduce quantities of expensive items until under budget
- Use the budget tracker as a guide while shopping
- Stay disciplined with the visual feedback
The Grocery List app can be installed on your device for a native app experience!
Android (Chrome):
- Open the app in Chrome
- Tap the install banner or menu (⋮) → "Add to Home screen"
- Tap "Install"
- Icon appears on your home screen
iOS (Safari):
- Open the app in Safari
- Tap the Share button (□↑)
- Scroll and tap "Add to Home Screen"
- Tap "Add"
- Icon appears on your home screen
Desktop (Chrome/Edge):
- Open the app in Chrome or Edge
- Click the install icon (⊕) in the address bar
- Click "Install"
- App opens in standalone window
Benefits of Installing:
- Access from home screen like any app
- Works offline with full functionality
- Faster loading with intelligent caching
- Background sync (Android, Desktop)
- Push notifications (Android, Desktop)
- Full-screen native app experience
- No app store required
Detailed Instructions:
- PWA User Guide - Complete installation guide for all platforms
- Quick Start Guide - Get started in 2 minutes
- Browser Support - Check if your browser supports installation
- TypeScript: Type-safe JavaScript
- React 18: UI framework with hooks
- Vite: Fast build tool and dev server with PWA plugin
- pnpm: Efficient package manager
- Zero: Real-time sync and collaboration framework
- PostgreSQL: Database backend for Zero
- zero-cache: Local caching server for offline support
- Service Workers: Enable offline mode and background sync
- Web App Manifest: Make app installable on devices
grocery/
├── src/
│ ├── components/
│ │ ├── AddItemForm.tsx # Form to add new items
│ │ ├── GroceryItem.tsx # Single item display
│ │ ├── GroceryList.tsx # List of all items
│ │ ├── ListManagement.tsx # List management modal
│ │ ├── ListSelector.tsx # Dropdown for switching lists
│ │ ├── ListStats.tsx # Statistics display
│ │ ├── PermissionBadge.tsx # Permission level indicator
│ │ ├── SearchFilterBar.tsx # Search and filter controls
│ │ ├── SortControls.tsx # Sorting controls
│ │ ├── BulkOperations.tsx # Bulk action buttons
│ │ ├── LoginForm.tsx # User login form
│ │ ├── RegisterForm.tsx # User registration form
│ │ ├── UserProfile.tsx # User profile menu
│ │ └── ListSkeleton.tsx # Loading skeletons
│ ├── hooks/
│ │ ├── useGroceryItems.ts # Custom hooks for items
│ │ ├── useAuth.ts # Authentication hook
│ │ └── useLists.ts # List management hook
│ ├── utils/
│ │ ├── listExport.ts # Export functions (JSON, CSV, Text)
│ │ ├── api.ts # API client with interceptors
│ │ └── tokenRefresh.ts # Token refresh logic
│ ├── types.ts # TypeScript type definitions
│ ├── zero-store.ts # Zero-based data store
│ ├── zero-schema.ts # Zero schema definition
│ ├── App.tsx # Main app component
│ ├── App.css # App styles
│ ├── main.tsx # App entry point
│ └── index.css # Global styles
├── server/
│ ├── auth/
│ │ ├── routes.ts # Authentication routes
│ │ ├── controller.ts # Auth logic
│ │ ├── middleware.ts # Auth middleware
│ │ └── utils.ts # JWT utilities
│ ├── lists/
│ │ ├── routes.ts # List management routes
│ │ ├── controller.ts # List operations
│ │ └── middleware.ts # List permission checks
│ ├── invites/
│ │ ├── routes.ts # Invite link routes
│ │ └── controller.ts # Invite operations
│ ├── activities/
│ │ ├── routes.ts # Activity routes
│ │ └── controller.ts # Activity retrieval
│ ├── middleware/
│ │ ├── listPermissions.ts # Permission enforcement
│ │ ├── rateLimiter.ts # Rate limiting
│ │ ├── errorHandler.ts # Error handling
│ │ └── validateRequest.ts # Request validation
│ ├── migrations/
│ │ ├── 001_add_authentication.sql
│ │ ├── 002_add_lists.sql
│ │ ├── 003_add_list_sharing.sql
│ │ ├── 004_migrate_to_lists.sql
│ │ ├── 005_add_list_activities.sql
│ │ ├── 006_add_list_customization.sql
│ │ ├── 007_add_invite_links.sql
│ │ ├── 008_add_list_archive.sql
│ │ ├── 009_add_list_pins.sql
│ │ └── rollback/ # Rollback scripts
│ ├── db/
│ │ ├── schema.sql # Complete database schema
│ │ └── pool.ts # Database connection pool
│ ├── config/
│ │ ├── env.ts # Environment configuration
│ │ ├── db.ts # Database configuration
│ │ └── rateLimitConfig.ts # Rate limit settings
│ ├── types/
│ │ └── index.ts # Shared TypeScript types
│ └── index.ts # Express server entry point
├── docs/
│ ├── LIST_SHARING_TESTS.md # List sharing test scenarios
│ ├── PERMISSION_TESTS.md # Permission test scenarios
│ ├── REALTIME_TESTS.md # Real-time sync tests
│ └── SECURITY.md # Security best practices
├── specs/
│ └── requirements.md # Detailed requirements
├── .env.example # Environment template
├── docker-compose.yml # PostgreSQL setup
├── package.json
├── tsconfig.json
├── vite.config.ts
└── index.html
This application uses Zero for real-time collaborative synchronization across multiple devices and users. Zero provides:
- Real-time Sync: Changes propagate instantly across all connected clients
- Offline Support: Works offline and automatically syncs when reconnected
- Conflict Resolution: Handles concurrent edits gracefully
- Type Safety: Fully typed queries with TypeScript
- Local-First: Fast, responsive UI with local caching via zero-cache
Zero replaces the localStorage-based sync with a robust, production-ready synchronization system backed by PostgreSQL.
The app includes a comprehensive offline-first system with intelligent conflict resolution:
- Offline Queue: Changes are queued locally when offline and synced automatically when reconnected
- Automatic Conflict Resolution: Most conflicts are resolved automatically using intelligent strategies
- Manual Resolution UI: Complex conflicts can be resolved manually with clear diff views
- Persistent Queue: Offline changes survive browser restarts and device reboots
- Retry with Exponential Backoff: Failed syncs are retried automatically with smart delays
- Sync Status Indicator: Always-visible indicator shows connection and queue status
- Last-Write-Wins: Most recent change wins (fastest, for non-critical fields)
- Prefer-Gotten: Prefers version where item is marked as "gotten" (prevents frustrating reverts)
- Field-Level-Merge: Intelligently merges different field changes
- Manual Resolution: User chooses when automatic resolution isn't possible
User makes change → Online? → Direct sync
↓ Offline
Queue locally
↓
Network reconnects
↓
Process queue
↓
Detect conflicts
↓
┌─── Auto-resolve? ───┐
↓ Yes ↓ No
Apply merge Show conflict UI
↓ ↓
Sync complete ← User resolves
- Offline Indicator: Shows when offline with pending change count
- Syncing Progress: Visual feedback during synchronization
- Conflict Notifications: Clear alerts when manual resolution needed
- No Data Loss: All changes are preserved, even during conflicts
-
User Guide: OFFLINE_CONFLICT_RESOLUTION_GUIDE.md
- What conflicts are and why they happen
- How automatic resolution works
- How to manually resolve conflicts
- Tips for avoiding conflicts
-
Technical Documentation:
- Architecture - System design and data flow
- API Reference - Complete API documentation
- Best Practices - Development guidelines
import { useOfflineQueue } from './utils/offlineQueue';
function MyComponent() {
const { pendingCount, failedCount, retryFailed } = useOfflineQueue();
return (
<div>
{pendingCount > 0 && <span>{pendingCount} changes queued</span>}
{failedCount > 0 && (
<button onClick={retryFailed}>Retry Failed ({failedCount})</button>
)}
</div>
);
}Performance:
- Queue processing: 100-500ms per item
- Conflict detection: <10ms per item
- Storage overhead: ~1-5KB per queued mutation
- Supports 50+ concurrent users, 500+ items per list
- Node.js 20+
- pnpm (install with
npm install -g pnpm) - Docker and Docker Compose (for PostgreSQL)
The application uses environment variables for configuration. You need to set up .env files for both the client and server.
Copy the example file and configure it:
cp .env.example .envEdit .env and configure the following variables:
Client Variables (accessible in browser):
VITE_API_URL- Backend API URL (default:http://localhost:3001)VITE_ZERO_SERVER- Zero server URL (default:http://localhost:4848)VITE_AUTH_ENABLED- Enable authentication features (default:false)
Server Variables (server-side only):
DATABASE_URL- PostgreSQL connection stringJWT_SECRET- Secret for signing JWT access tokensJWT_REFRESH_SECRET- Secret for signing JWT refresh tokensJWT_EXPIRES_IN- Access token expiration (default:15m)JWT_REFRESH_EXPIRES_IN- Refresh token expiration (default:7d)PORT- Server port (default:3001)NODE_ENV- Environment mode (developmentorproduction)CORS_ORIGIN- Allowed CORS origins (default:http://localhost:3000)
Zero Cache Variables:
ZERO_UPSTREAM_DB- PostgreSQL connection for zero-cacheZERO_REPLICA_FILE- Path to zero-cache's local replicaZERO_AUTH_SECRET- Secret for zero-cache authentication
For development, you can use the default values, but for production you MUST generate secure secrets:
# Generate JWT_SECRET
openssl rand -base64 32
# Generate JWT_REFRESH_SECRET (use a different value)
openssl rand -base64 32
# Generate ZERO_AUTH_SECRET
openssl rand -base64 32Copy these generated values into your .env file.
.envfiles contain sensitive secrets - NEVER commit them to git- The
.env.examplefiles are templates and are safe to commit - Use
.env.localfor local overrides (git-ignored) - In production, use your hosting platform's secret management system
-
Install dependencies:
pnpm install
-
Configure environment variables (see Environment Configuration above):
cp .env.example .env # Edit .env with your configuration -
Start PostgreSQL database:
docker compose up -d
This starts a PostgreSQL container for Zero's backend storage.
-
Start zero-cache server:
pnpm zero:dev
The zero-cache server handles real-time sync between clients and the database.
-
In a separate terminal, start the development server:
pnpm dev
-
Open your browser to
http://localhost:3000
Quick Start (All-in-One):
pnpm dev:fullThis command starts PostgreSQL, zero-cache, and the Vite dev server all at once.
With Authentication Server:
pnpm dev:allThis starts PostgreSQL, zero-cache, the authentication API server, and the Vite dev server.
pnpm dev- Start Vite development server onlypnpm dev:full- Start PostgreSQL, zero-cache, and Vite dev serverpnpm dev:all- Start all services including authentication APIpnpm zero:dev- Start zero-cache server onlypnpm server:dev- Start authentication API server in development modepnpm build- Build for productionpnpm preview- Preview production buildpnpm type-check- Run TypeScript type checking
When deploying to production, follow these security best practices:
-
Generate Strong Secrets:
# Generate all required secrets openssl rand -base64 32 # For JWT_SECRET openssl rand -base64 32 # For JWT_REFRESH_SECRET openssl rand -base64 32 # For ZERO_AUTH_SECRET
-
Set Production Environment Variables:
NODE_ENV=production- Enables production optimizationsDATABASE_URL- Use secure PostgreSQL connection with SSLCORS_ORIGIN- Set to your actual frontend domain(s)VITE_API_URL- Set to your production API URLVITE_ZERO_SERVER- Set to your production zero-cache URLVITE_AUTH_ENABLED=true- Enable authentication in production
-
Security Checklist:
- Use HTTPS for all connections (frontend, API, database)
- Enable SSL/TLS for database connections:
?sslmode=require - Store secrets in a secure secret manager (not in code or .env files)
- Use environment-specific secrets (never reuse dev secrets in prod)
- Implement rate limiting on authentication endpoints
- Enable CORS only for specific, trusted domains
- Regularly rotate JWT secrets (every 90 days recommended)
- Monitor failed login attempts and implement account lockouts
- Use strong password requirements (handled by bcrypt with 10+ rounds)
- Keep dependencies updated for security patches
For production, use a managed PostgreSQL service and configure SSL:
# Example production DATABASE_URL with SSL
DATABASE_URL=postgresql://user:[email protected]:5432/grocery_db?sslmode=require- Frontend (Vite): Vercel, Netlify, Cloudflare Pages
- Backend API: Railway, Render, Fly.io, AWS ECS
- Database: Supabase, Neon, Railway, AWS RDS
- Zero Cache: Deploy alongside backend API or as separate service
The Grocery List app includes a robust authentication system to support multi-user functionality with secure user accounts. Authentication is optional and can be toggled with the VITE_AUTH_ENABLED environment variable.
The authentication system provides:
- JWT-Based Authentication: Secure token-based authentication using JSON Web Tokens
- Access & Refresh Tokens: Short-lived access tokens (15 minutes) and long-lived refresh tokens (7 days)
- Password Security: Passwords hashed with bcrypt (12 rounds) before storage
- Rate Limiting: Brute-force protection on authentication endpoints
- Email Validation: Ensures valid email format during registration
- Password Requirements: Enforces strong password policies
- Profile Management: Users can update their name, email, and password
- Protected Routes: Middleware to protect authenticated endpoints
- Token Refresh: Automatic token refresh mechanism for seamless UX
- Multi-User Support: Each user has their own isolated grocery list
First, ensure PostgreSQL is running and initialize the authentication schema:
# Start PostgreSQL database
pnpm db:up
# Initialize database schema (creates users and refresh_tokens tables)
pnpm db:initThe schema includes:
userstable: Stores user accounts (email, password_hash, name)refresh_tokenstable: Optional token storage for revocation (future use)grocery_items.user_id: Links items to specific usersliststable: Stores grocery lists with owner informationlist_memberstable: Manages list sharing with permission levels (owner, editor, viewer)
Note: The pnpm db:init command runs all migrations including list sharing setup. If you need to run migrations manually, see the Database Migrations section.
Copy the example environment file and configure authentication settings:
cp .env.example .envEdit .env to configure the authentication system (see "Environment Variables for Auth" section below).
Set VITE_AUTH_ENABLED=true in your .env file to enable authentication features in the UI.
Configure these variables in your .env file:
# Enable authentication features in the UI
VITE_AUTH_ENABLED=true
# Backend API URL where auth server is running
VITE_API_URL=http://localhost:3001# Database connection string
DATABASE_URL=postgresql://grocery:grocery@localhost:5432/grocery_db
# JWT Access Token Secret (REQUIRED - Generate with: openssl rand -base64 32)
JWT_ACCESS_SECRET=your-super-secret-jwt-key-change-this-in-production
# JWT Refresh Token Secret (REQUIRED - Must be different from access secret)
JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this-in-production
# Token expiration times
JWT_ACCESS_EXPIRY=15m # Access token validity (15 minutes recommended)
JWT_REFRESH_EXPIRY=7d # Refresh token validity (7 days recommended)
# Server configuration
PORT=3001 # Auth API server port
NODE_ENV=development # Environment mode
CORS_ORIGIN=http://localhost:3000 # Allowed frontend origin
# Security settings
BCRYPT_ROUNDS=10 # Bcrypt hashing rounds (10-12 recommended)
RATE_LIMIT_WINDOW_MS=900000 # Rate limit window (15 minutes)
RATE_LIMIT_MAX_REQUESTS=100 # Max requests per windowIMPORTANT - Secret Generation:
For production, you MUST generate strong, unique secrets:
# Generate JWT_ACCESS_SECRET
openssl rand -base64 32
# Generate JWT_REFRESH_SECRET (use a different value!)
openssl rand -base64 32
# Generate ZERO_AUTH_SECRET
openssl rand -base64 32Never use default values in production! Store secrets securely using your hosting platform's secret management system.
The authentication API server runs separately from the Vite development server.
pnpm dev:allThis starts:
- PostgreSQL database
- zero-cache server (real-time sync)
- Authentication API server (port 3001)
- Vite development server (port 3000)
# Terminal 1: Start PostgreSQL
pnpm db:up
# Terminal 2: Start zero-cache
pnpm zero:dev
# Terminal 3: Start authentication API server
pnpm server:dev
# Terminal 4: Start Vite frontend
pnpm devCheck the health endpoint:
curl http://localhost:3001/healthExpected response:
{
"success": true,
"status": "healthy",
"timestamp": "2025-10-26T...",
"database": {
"connected": true
}
}Visit http://localhost:3001/api to see all available authentication endpoints.
- User submits email, password, and name
- Server validates input (email format, password strength, name length)
- Server checks if email is already registered
- Password is hashed using bcrypt (12 rounds)
- User account is created in database
- JWT access and refresh tokens are generated
- User data and tokens are returned to client
POST /api/auth/register
Request Body:
{
"email": "[email protected]",
"password": "SecurePass123",
"name": "John Doe"
}Password Requirements:
- Minimum 8 characters
- At least one uppercase letter (A-Z)
- At least one lowercase letter (a-z)
- At least one number (0-9)
Success Response (201 Created):
{
"success": true,
"message": "User registered successfully",
"data": {
"user": {
"id": "uuid-here",
"email": "[email protected]",
"name": "John Doe",
"created_at": "2025-10-26T..."
},
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
}Error Responses:
400 Bad Request: Invalid input (email format, password strength, name length)409 Conflict: Email already registered429 Too Many Requests: Rate limit exceeded (5 attempts per 15 minutes)500 Internal Server Error: Server error
curl -X POST http://localhost:3001/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "SecurePass123",
"name": "John Doe"
}'- User submits email and password
- Server validates input format
- Server looks up user by email (case-insensitive)
- Server compares password with stored hash using bcrypt
- If valid, updates user's last login timestamp
- Generates new JWT access and refresh tokens
- Returns user data and tokens
POST /api/auth/login
Request Body:
{
"email": "[email protected]",
"password": "SecurePass123"
}Success Response (200 OK):
{
"success": true,
"message": "Login successful",
"data": {
"user": {
"id": "uuid-here",
"email": "[email protected]",
"name": "John Doe",
"created_at": "2025-10-26T..."
},
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
}Error Responses:
400 Bad Request: Missing email or password401 Unauthorized: Invalid email or password429 Too Many Requests: Rate limit exceeded (5 attempts per 15 minutes)500 Internal Server Error: Server error
Security Note: The server returns the same error message for invalid email and invalid password to prevent email enumeration attacks.
curl -X POST http://localhost:3001/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "SecurePass123"
}'Access tokens expire after 15 minutes. Use the refresh token to obtain a new access token without requiring the user to log in again.
POST /api/auth/refresh
Request Body:
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}Success Response (200 OK):
{
"success": true,
"message": "Token refreshed successfully",
"data": {
"accessToken": "new-access-token...",
"refreshToken": "new-refresh-token..."
}
}Include the access token in the Authorization header:
curl -X GET http://localhost:3001/api/auth/me \
-H "Authorization: Bearer your-access-token-here"Get Current User:
GET /api/auth/me
Headers: Authorization: Bearer <accessToken>
Update Profile:
PATCH /api/auth/profile
Headers: Authorization: Bearer <accessToken>
Body: { "name": "New Name", "email": "[email protected]" }
Change Password:
POST /api/auth/change-password
Headers: Authorization: Bearer <accessToken>
Body: { "currentPassword": "old", "newPassword": "New123" }
Logout:
POST /api/auth/logout
(Client-side: Delete tokens from storage)
The authentication system implements multiple layers of security:
- Bcrypt Hashing: Passwords hashed with 12 rounds (recommended: 10-12)
- Never Stored Plain Text: Only password hashes are stored
- Strong Password Requirements: Minimum 8 characters, mixed case, numbers
- Password Change: Requires current password verification
- Short-Lived Access Tokens: 15-minute expiration reduces attack window
- Long-Lived Refresh Tokens: 7-day expiration balances security and UX
- Secure JWT Algorithm: Uses HS256 (HMAC SHA-256)
- Token Verification: All protected routes verify token signature and expiration
- Separate Secrets: Different secrets for access and refresh tokens
- Authentication Endpoints: 5 requests per 15 minutes (register, login)
- General Endpoints: 20 requests per 15 minutes (profile updates)
- IP-Based: Tracks requests per IP address
- Prevents Brute Force: Blocks rapid authentication attempts
- Email Format Validation: Regex-based email validation
- SQL Injection Prevention: Parameterized queries with pg library
- XSS Prevention: Input sanitization with express-validator
- CSRF Protection: Token-based authentication (no cookies)
- Connection Pooling: Efficient database connection management
- SSL/TLS: Use
?sslmode=requirefor production database connections - Prepared Statements: All queries use parameterized values
- Foreign Key Constraints: CASCADE deletion for data integrity
- X-Content-Type-Options: nosniff (prevents MIME sniffing)
- X-Frame-Options: DENY (prevents clickjacking)
- X-XSS-Protection: Enabled
- Strict-Transport-Security: Enforces HTTPS in production
- CORS Configuration: Restricts origins to trusted domains
- Secret Management: Never commit secrets to git
- Environment Variables: Sensitive config stored in .env files
- Production Secrets: Generate strong, unique secrets with openssl
- Secret Rotation: Rotate JWT secrets every 90 days (recommended)
- Generic Error Messages: Don't reveal whether email exists
- No Stack Traces: Production mode hides detailed errors
- Logging: Server-side logging for security events
- Graceful Failures: Fallback to generic errors
Symptoms:
Error: connect ECONNREFUSED 127.0.0.1:5432
Solutions:
-
Ensure PostgreSQL is running:
pnpm db:up docker ps # Verify postgres container is running -
Check DATABASE_URL in .env matches docker-compose.yml credentials
-
Verify PostgreSQL is listening on port 5432:
docker compose logs postgres
Symptoms:
Warning: Using default JWT secret - change in production!
Solutions:
-
Generate secure secrets:
openssl rand -base64 32
-
Update .env file with generated secrets:
JWT_ACCESS_SECRET=<generated-secret-1> JWT_REFRESH_SECRET=<generated-secret-2>
-
Restart the auth server:
pnpm server:dev
Symptoms:
{
"success": false,
"error": "Invalid token",
"message": "Access token expired"
}Solutions:
-
Use the refresh token to get a new access token:
curl -X POST http://localhost:3001/api/auth/refresh \ -H "Content-Type: application/json" \ -d '{"refreshToken": "your-refresh-token"}'
-
If refresh token is also expired, user must log in again
-
Check token expiration settings in .env:
JWT_ACCESS_EXPIRY=15m # Adjust as needed JWT_REFRESH_EXPIRY=7d # Adjust as needed
Symptoms:
{
"success": false,
"error": "Too many requests",
"message": "Too many authentication attempts. Please try again later."
}Solutions:
-
Wait 15 minutes before retrying
-
For development, increase rate limits in .env:
RATE_LIMIT_MAX_REQUESTS=100 # Increase for development -
For testing, disable rate limits temporarily in server/auth/routes.ts
Symptoms:
{
"success": false,
"error": "Validation error",
"message": "Password must contain at least one uppercase letter"
}Solutions: Ensure password meets all requirements:
- Minimum 8 characters
- At least one uppercase letter (A-Z)
- At least one lowercase letter (a-z)
- At least one number (0-9)
Example valid password: SecurePass123
Symptoms:
Access to XMLHttpRequest at 'http://localhost:3001/api/auth/login'
from origin 'http://localhost:3000' has been blocked by CORS policy
Solutions:
-
Verify CORS_ORIGIN in .env matches your frontend URL:
CORS_ORIGIN=http://localhost:3000
-
For multiple origins, use comma-separated values:
CORS_ORIGIN=http://localhost:3000,http://localhost:5173
-
Restart the auth server after changing CORS_ORIGIN
Symptoms:
ERROR: relation "users" does not exist
Solutions:
-
Initialize the database schema:
pnpm db:init
-
If above fails, manually run the schema file:
psql -h localhost -U grocery -d grocery_db -f server/db/schema.sql # Password: grocery (from docker-compose.yml) -
Verify tables were created:
psql -h localhost -U grocery -d grocery_db -c "\dt"
Symptoms:
Error: listen EADDRINUSE: address already in use :::3001
Solutions:
-
Find and kill the process using port 3001:
# Linux/Mac lsof -ti:3001 | xargs kill -9 # Or change the port in .env PORT=3002
-
Verify no other server is running:
ps aux | grep node
Symptoms:
- User can log in successfully
- Items don't appear or aren't associated with user
Solutions:
-
Verify grocery_items table has user_id column:
psql -h localhost -U grocery -d grocery_db \ -c "\d grocery_items" -
If user_id column is missing, run the schema migration:
pnpm db:init
-
Check frontend is sending access token with grocery item requests
If you encounter issues not covered here:
-
Check server logs for detailed error messages:
# Server logs appear in the terminal running pnpm server:dev -
Check database logs:
docker compose logs postgres
-
Verify environment variables are loaded:
# In server/config/env.ts, enable DEBUG mode DEBUG_DB=true -
Test API endpoints directly with cURL to isolate frontend vs backend issues
-
Check the /health endpoint to verify all services are running:
curl http://localhost:3001/health
Symptoms:
{
"success": false,
"error": "User not found",
"message": "No user exists with email: [email protected]"
}Solutions:
- Verify the email address is correct (check for typos)
- Ensure the user has registered an account with that exact email
- Email matching is case-insensitive, but the account must exist
- Have the user register first, then try adding them again
Symptoms:
{
"success": false,
"error": "Forbidden",
"message": "You do not have permission to perform this action"
}Solutions:
-
Check your permission level - only owners can share lists:
# View your permission for a list curl -X GET http://localhost:3001/api/lists/<list-id> \ -H "Authorization: Bearer <your-token>"
-
If you need to share the list, ask the owner to either:
- Share it with the new user for you
- Transfer ownership to you
Symptoms:
ERROR: Cannot remove the last owner from a list. Transfer ownership first or delete the list.
Solutions:
-
Add another user as owner before removing yourself:
# First: Promote another member to owner curl -X PUT http://localhost:3001/api/lists/<list-id>/members/<member-id> \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <your-token>" \ -d '{"permission": "owner"}' # Then: Remove yourself or change your permission
-
Or delete the list entirely if you no longer need it:
curl -X DELETE http://localhost:3001/api/lists/<list-id> \ -H "Authorization: Bearer <your-token>"
Symptoms:
- User A adds an item, but User B doesn't see it
- Changes only appear after page refresh
- "Sync status" shows disconnected
Solutions:
-
Check Zero cache server is running:
# Should see zero-cache process ps aux | grep zero-cache
-
Verify WebSocket connection in browser console:
// Should see WebSocket connection // Look for: "WebSocket connected" or similar
-
Check firewall settings allow WebSocket connections (port 4848)
-
Restart zero-cache server:
pnpm zero:dev
-
Clear browser cache and reconnect
Symptoms:
- Owner shares list with you
- List doesn't appear in your list selector
- API returns 404 for the list
Solutions:
-
Refresh the page to load new lists
-
Check you're logged in with the correct account
-
Verify the owner used your correct email address
-
Check the list wasn't deleted by the owner
-
Manually query your lists to see if it appears:
curl -X GET http://localhost:3001/api/lists \ -H "Authorization: Bearer <your-token>"
Symptoms:
- You have editor permission
- Cannot add/edit/delete items
- Buttons are disabled or missing
Solutions:
-
Verify your permission level:
- Go to "Manage List" > "General" tab
- Check "Your Role" shows "editor" or "owner"
-
Check if the list was deleted:
curl -X GET http://localhost:3001/api/lists/<list-id> \ -H "Authorization: Bearer <your-token>"
-
Ask the owner to check your permission:
- Owner should see you in the Members tab
- Permission should be set to "editor"
-
Try logging out and back in to refresh permissions
Symptoms:
ERROR: relation "lists" does not exist
ERROR: relation "list_members" does not exist
Solutions:
-
Run the list sharing migration:
psql -h localhost -U grocery -d grocery_db \ -f server/migrations/003_add_list_sharing.sql
-
If migration fails, check existing data:
# Check if tables exist psql -h localhost -U grocery -d grocery_db -c "\dt"
-
For a fresh start (WARNING: deletes all data):
pnpm db:reset pnpm db:init
-
Verify migration completed:
psql -h localhost -U grocery -d grocery_db -c " SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('lists', 'list_members'); "
Symptoms:
- Slow loading with many list members
- UI becomes sluggish
- High database query times
Solutions:
- Limit list membership to active users only
- Remove inactive members to improve performance
- Consider splitting into multiple lists for different groups
- Check database indexes are created:
psql -h localhost -U grocery -d grocery_db -c " SELECT indexname, tablename FROM pg_indexes WHERE tablename IN ('lists', 'list_members'); "
Symptoms:
- Owner changes member permission
- Member still has old permissions
- Changes don't sync
Solutions:
-
Have the affected user log out and back in
-
Clear browser cache and local storage:
// In browser console localStorage.clear(); sessionStorage.clear(); location.reload();
-
Verify the change in database:
psql -h localhost -U grocery -d grocery_db -c " SELECT user_id, permission_level FROM list_members WHERE list_id = '<list-id>'; "
-
Check for caching issues with Zero:
# Restart zero-cache to clear cache pnpm zero:dev
If you encounter issues not covered here:
-
Check Server Logs:
# API server logs # (in terminal running pnpm server:dev) # Zero cache logs # (in terminal running pnpm zero:dev)
-
Check Database State:
# View all lists psql -h localhost -U grocery -d grocery_db -c "SELECT * FROM lists;" # View all list members psql -h localhost -U grocery -d grocery_db -c "SELECT * FROM list_members;"
-
Test API Endpoints Directly:
# Health check curl http://localhost:3001/health # Lists health check curl http://localhost:3001/api/lists/health
-
Enable Debug Mode:
# In .env file DEBUG=true LOG_LEVEL=debug -
Check Browser Console: Look for JavaScript errors or network failures
The Grocery List app includes powerful list sharing and collaboration features that allow multiple users to work together on grocery lists with fine-grained permission controls.
List sharing enables you to:
- Create multiple grocery lists for different purposes (weekly shopping, party supplies, etc.)
- Share lists with other users via email
- Control what each member can do with permission levels
- Collaborate in real-time with instant synchronization
- Manage list members and their access levels
- Track who added items and when
- Click the "New List" button in the list selector dropdown
- Enter a name for your list (e.g., "Weekly Shopping", "Party Supplies")
- Click "Create" to create the list
- You become the owner of the list automatically
Via API:
curl -X POST http://localhost:3001/api/lists \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-access-token>" \
-d '{
"name": "Weekly Shopping"
}'Response:
{
"success": true,
"data": {
"list": {
"id": "uuid-here",
"name": "Weekly Shopping",
"ownerId": "user-uuid",
"createdAt": 1729900000000,
"updatedAt": 1729900000000,
"memberCount": 1,
"currentUserPermission": "owner"
}
}
}- Click the list selector dropdown at the top of the page
- Select a list from your lists (owned lists and shared lists)
- The view updates to show items from the selected list
Only list owners can share lists and invite new members.
- Open the list you want to share
- Click the "Manage List" button (gear icon)
- Go to the "Members" tab
- Enter the email address of the person you want to invite
- Select a permission level (Editor or Viewer)
- Click "Send Invitation"
The user must have an account with that email address to be added to the list.
Via API:
curl -X POST http://localhost:3001/api/lists/<list-id>/members \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-access-token>" \
-d '{
"userId": "user-uuid-to-add",
"permission": "editor"
}'Response:
{
"success": true,
"data": {
"member": {
"id": "member-uuid",
"listId": "list-uuid",
"userId": "user-uuid",
"userEmail": "[email protected]",
"userName": "John Doe",
"permission": "editor",
"addedAt": 1729900000000,
"addedBy": "owner-uuid"
}
}
}The app supports three permission levels for list access:
- Full Control: Complete access to all list features
- What Owners Can Do:
- Add, edit, and delete items
- Share the list with other users
- Manage list members (add, remove, change permissions)
- Rename the list
- Delete the list (permanently removes for all members)
- Who Gets This: The user who created the list
- Special Notes:
- Each list must have at least one owner
- Ownership can be transferred or shared with other users
- Only owners can see the "Danger Zone" tab
- Can Modify Items: Full access to manage grocery items
- What Editors Can Do:
- Add new items to the list
- Edit existing items (name, quantity, category, notes)
- Mark items as gotten/not gotten
- Delete items from the list
- View all list members
- Use bulk operations (mark all gotten, delete all gotten)
- What Editors Cannot Do:
- Share the list with others
- Remove list members
- Change permissions
- Rename or delete the list
- Use Case: Family members, roommates, or close collaborators who actively shop together
- Read-Only Access: Can view items but cannot make changes
- What Viewers Can Do:
- View all items in the list
- See item details (name, quantity, category, notes, gotten status)
- View list members
- Search and filter items
- Export or print the list
- What Viewers Cannot Do:
- Add, edit, or delete items
- Mark items as gotten
- Share the list
- Modify list settings
- Use Case: People who need to see the list but shouldn't modify it (e.g., a shopper following instructions)
- Click "Manage List" on the list you want to view
- Go to the "Members" tab
- See all members with their:
- Name and email
- Permission level
- Join date
- Who invited them
Via API:
curl -X GET http://localhost:3001/api/lists/<list-id> \
-H "Authorization: Bearer <your-access-token>"Owners only can change member permissions.
- Go to "Manage List" > "Members" tab
- Find the member you want to update
- Use the permission dropdown next to their name
- Select the new permission level (Editor or Viewer)
- The change takes effect immediately
Via API:
curl -X PUT http://localhost:3001/api/lists/<list-id>/members/<member-id> \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-access-token>" \
-d '{
"permission": "viewer"
}'Owners only can remove members from a list.
- Go to "Manage List" > "Members" tab
- Find the member you want to remove
- Click the remove button (×) next to their name
- Confirm the removal
- The user immediately loses access to the list
Important: You cannot remove yourself if you are the only owner. Transfer ownership first or delete the list.
Via API:
curl -X DELETE http://localhost:3001/api/lists/<list-id>/members/<member-id> \
-H "Authorization: Bearer <your-access-token>"When multiple users are working on the same list, the app provides seamless real-time collaboration:
- Item Changes: When anyone adds, edits, or deletes an item, all other users see the change instantly
- Status Updates: When someone marks an item as gotten, everyone sees the update in real-time
- Member Changes: When the owner adds or removes members, the list updates for everyone
- No Refresh Needed: All changes happen automatically without page refresh
The app uses Zero's built-in conflict resolution:
- Last Write Wins: If two users edit the same item simultaneously, the last change is applied
- Automatic Merge: Zero handles concurrent edits gracefully
- No Data Loss: All changes are preserved and synced
- Work Offline: Add, edit, or delete items without internet connection
- Local Queue: Changes are stored locally until connection is restored
- Auto-Sync: When back online, all changes sync automatically
- Conflict Handling: Zero resolves any conflicts that occurred while offline
Owners and Editors can rename lists.
- Click "Manage List" on the list
- Go to the "General" tab
- Edit the list name in the text field
- Click "Rename"
- All members see the updated name immediately
Via API:
curl -X PUT http://localhost:3001/api/lists/<list-id> \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-access-token>" \
-d '{
"name": "Updated List Name"
}'Owners only can delete lists.
Warning: Deleting a list is permanent and cannot be undone. All items and member access will be removed.
- Click "Manage List" on the list
- Go to the "Danger Zone" tab (owners only)
- Click "Delete List"
- Confirm by typing the list name exactly as shown
- Click "Yes, Delete Permanently"
Via API:
curl -X DELETE http://localhost:3001/api/lists/<list-id> \
-H "Authorization: Bearer <your-access-token>"- Only share lists with people you trust
- Use Viewer permission for untrusted users who only need to see the list
- Regularly review list members and remove inactive users
- Don't share your account credentials with others - use list sharing instead
- Create separate lists for different purposes (weekly shopping, special events, etc.)
- Use descriptive list names (e.g., "Thanksgiving Dinner" instead of "List 1")
- Archive or delete old lists to keep your list organized
- Assign appropriate permissions based on user roles
- Use Editor permission for active collaborators who help with shopping
- Use Viewer permission for people who just need to reference the list
- Communicate with list members outside the app for complex planning
- Review and consolidate duplicate items added by multiple users
All list endpoints require authentication. Include your access token in the Authorization header:
Authorization: Bearer <your-access-token>
Create a List
POST /api/lists
Content-Type: application/json
Authorization: Bearer <token>
Body: {
"name": "Weekly Shopping",
"color": "#4caf50", // Optional: Hex color code
"icon": "🛒" // Optional: Emoji or icon
}
Response: {
"success": true,
"data": {
"list": {
"id": "uuid",
"name": "Weekly Shopping",
"ownerId": "user-uuid",
"color": "#4caf50",
"icon": "🛒",
"createdAt": 1729900000000,
"updatedAt": 1729900000000,
"memberCount": 1,
"currentUserPermission": "owner",
"isArchived": false,
"isPinned": false
}
}
}Get All Lists
GET /api/lists
Authorization: Bearer <token>
Response: {
"success": true,
"data": {
"lists": [
{
"id": "uuid",
"name": "Weekly Shopping",
"ownerId": "user-uuid",
"memberCount": 3,
"currentUserPermission": "owner",
"isArchived": false,
"isPinned": true,
...
}
]
}
}Get Specific List with Members
GET /api/lists/:id
Authorization: Bearer <token>
Response: {
"success": true,
"data": {
"list": {
"id": "uuid",
"name": "Weekly Shopping",
"members": [
{
"id": "member-uuid",
"userId": "user-uuid",
"userEmail": "[email protected]",
"userName": "John Doe",
"permission": "editor",
"addedAt": 1729900000000,
"addedBy": "owner-uuid"
}
],
...
}
}
}Update List
PUT /api/lists/:id
Content-Type: application/json
Authorization: Bearer <token>
Body: {
"name": "Monthly Shopping",
"color": "#ff5722",
"icon": "🛍️"
}
Response: {
"success": true,
"data": { "list": { ... } }
}Delete List (Owner only)
DELETE /api/lists/:id
Authorization: Bearer <token>
Response: {
"success": true,
"message": "List deleted successfully"
}Add Member to List (Owner only)
POST /api/lists/:id/members
Content-Type: application/json
Authorization: Bearer <token>
Body: {
"userId": "user-uuid",
"permission": "editor" // "owner", "editor", or "viewer"
}
Response: {
"success": true,
"data": {
"member": {
"id": "member-uuid",
"listId": "list-uuid",
"userId": "user-uuid",
"userEmail": "[email protected]",
"userName": "John Doe",
"permission": "editor",
"addedAt": 1729900000000,
"addedBy": "owner-uuid"
}
}
}Update Member Permission (Owner only)
PUT /api/lists/:id/members/:userId
Content-Type: application/json
Authorization: Bearer <token>
Body: {
"permission": "viewer"
}
Response: {
"success": true,
"data": { "member": { ... } }
}Remove Member (Owner only)
DELETE /api/lists/:id/members/:userId
Authorization: Bearer <token>
Response: {
"success": true,
"message": "Member removed successfully"
}Generate Invite Link (Owner only)
POST /api/lists/:id/invite
Content-Type: application/json
Authorization: Bearer <token>
Body: {
"expiresInDays": 7 // Optional: 1-365 days, default 7
}
Response: {
"success": true,
"data": {
"inviteToken": "32-char-token",
"expiresAt": "2025-11-02T12:00:00Z",
"inviteUrl": "http://yourapp.com/invite/32-char-token"
}
}Get Invite Details (Public)
GET /api/invites/:token
Response: {
"success": true,
"data": {
"listId": "uuid",
"listName": "Weekly Shopping",
"ownerName": "Alice Johnson",
"memberCount": 5,
"expiresAt": "2025-11-02T12:00:00Z"
}
}Accept Invite
POST /api/invites/:token/accept
Authorization: Bearer <token>
Response: {
"success": true,
"message": "You have joined the list",
"data": {
"listId": "uuid",
"listName": "Weekly Shopping"
}
}Revoke Invite Link (Owner only)
DELETE /api/lists/:id/invite
Authorization: Bearer <token>
Response: {
"success": true,
"message": "Invite link revoked successfully"
}Leave a List
POST /api/lists/:id/leave
Authorization: Bearer <token>
Response: {
"success": true,
"message": "You have left the list"
}Transfer Ownership (Owner only)
POST /api/lists/:id/transfer
Content-Type: application/json
Authorization: Bearer <token>
Body: {
"newOwnerId": "user-uuid",
"confirmation": true
}
Response: {
"success": true,
"message": "Ownership transferred successfully",
"data": { "list": { ... } }
}Duplicate List
POST /api/lists/:id/duplicate
Content-Type: application/json
Authorization: Bearer <token>
Body: {
"name": "Copy of Weekly Shopping" // Optional
}
Response: {
"success": true,
"data": {
"list": {
"id": "new-uuid",
"name": "Copy of Weekly Shopping",
...
}
}
}Archive List (Owner only)
POST /api/lists/:id/archive
Authorization: Bearer <token>
Response: {
"success": true,
"message": "List archived successfully",
"data": { "list": { ... } }
}Unarchive List (Owner only)
POST /api/lists/:id/unarchive
Authorization: Bearer <token>
Response: {
"success": true,
"message": "List unarchived successfully",
"data": { "list": { ... } }
}Pin List
POST /api/lists/:id/pin
Authorization: Bearer <token>
Response: {
"success": true,
"message": "List pinned successfully"
}Unpin List
DELETE /api/lists/:id/unpin
Authorization: Bearer <token>
Response: {
"success": true,
"message": "List unpinned successfully"
}Get List Statistics
GET /api/lists/:id/stats
Authorization: Bearer <token>
Response: {
"success": true,
"data": {
"stats": {
"totalItems": 25,
"gottenItems": 10,
"pendingItems": 15,
"completionRate": 40,
"categoryBreakdown": {
"Produce": 8,
"Dairy": 5,
...
},
"recentActivity": {
"lastItemAdded": "2025-10-26T10:30:00Z",
"lastItemCompleted": "2025-10-26T11:00:00Z",
"activeMembers": 3
}
}
}
}Get List Activities
GET /api/lists/:id/activities?limit=50&offset=0
Authorization: Bearer <token>
Response: {
"success": true,
"data": {
"activities": [
{
"id": "activity-uuid",
"listId": "list-uuid",
"userId": "user-uuid",
"userName": "John Doe",
"userEmail": "[email protected]",
"actionType": "item_added",
"targetType": "item",
"targetId": "item-uuid",
"targetName": "Apples",
"details": { "quantity": 5 },
"createdAt": "2025-10-26T10:30:00Z"
}
],
"total": 150,
"limit": 50,
"offset": 0
}
}Activity Types:
list_created- List was createdlist_renamed- List name was changedlist_deleted- List was deletedmember_added- Member was added to listmember_removed- Member was removed from listpermission_changed- Member permission was updateditem_added- Item was added to listitem_updated- Item was modifieditem_deleted- Item was removeditem_completed- Item was marked as gottenitem_uncompleted- Item was marked as not gottenownership_transferred- List ownership was transferredlist_duplicated- List was cloned
All endpoints may return the following error responses:
400 Bad Request
{
"success": false,
"error": "Validation error",
"message": "Invalid request data",
"details": [...]
}401 Unauthorized
{
"success": false,
"error": "Unauthorized",
"message": "Invalid or expired token"
}403 Forbidden
{
"success": false,
"error": "Forbidden",
"message": "You do not have permission to perform this action"
}404 Not Found
{
"success": false,
"error": "Not found",
"message": "List not found or you don't have access"
}500 Internal Server Error
{
"success": false,
"error": "Internal server error",
"message": "An unexpected error occurred"
}See the Authentication section for details on obtaining access tokens.
The app includes pre-built list templates to help you quickly create grocery lists for common shopping scenarios. Templates come with pre-populated items that you can customize after creating the list.
Available Templates:
- Weekly Groceries (16 items) - Essential items for a week of meals including produce, meat, dairy, and pantry staples
- Party Supplies (14 items) - Everything you need for hosting a party including snacks, drinks, and disposables
- Breakfast Essentials (17 items) - Start your day right with breakfast staples like eggs, bacon, cereal, and coffee
- Healthy Snacks (14 items) - Nutritious snacks for the whole family including fruits, nuts, and protein bars
- BBQ Cookout (19 items) - Fire up the grill with burgers, hot dogs, sides, and BBQ essentials
- Baking Basics (15 items) - Stock up on baking essentials like flour, sugar, butter, and chocolate chips
- Quick Dinner (15 items) - Everything you need for fast weeknight meals including pasta, rice, and proteins
- Coffee & Tea Station (13 items) - Stock up your home coffee and tea bar with beans, milk, syrups, and treats
- Camping Trip (21 items) - All the food essentials for your outdoor adventure including s'mores ingredients
How to Use Templates:
- Click the list selector dropdown at the top of the page
- Click "Use a Template" button
- Browse available templates or search for specific items
- Click on a template card to preview its contents
- Review the preview showing the first 5 items (and item count)
- Click "Use This Template" to create a new list with all the items
- The new list is created with all template items pre-populated
- Customize the list by adding, removing, or editing items as needed
Template Search & Filtering:
The template selector includes a powerful search feature:
- Search by template name (e.g., "breakfast", "party")
- Search by template description (e.g., "weeknight meals", "outdoor")
- Search by item name (e.g., "marshmallows" finds Camping Trip template)
- Clear search with the X button or "Clear Search" button
- No results message appears when search doesn't match any templates
Template Features:
- Each template includes practical items with appropriate quantities
- Items are pre-categorized for easy shopping (Produce, Dairy, Meat, etc.)
- Many items include helpful notes (e.g., "5 lb bag", "Pre-made", "For s'mores")
- Templates create a new list that you fully own and can customize
- All list features work with template-created lists (sharing, exporting, etc.)
Tips for Using Templates:
- Use templates as a starting point and customize to your needs
- Templates are great for recurring shopping trips (weekly groceries)
- Combine multiple templates by creating separate lists
- Edit quantities and notes after creating from a template
- Remove items you don't need or add new ones
- Save time by not having to enter common items manually
- Enter the item name in the text field
- Enter the quantity (default is 1)
- Select a category from the dropdown (default is "Other")
- Optionally, add notes in the textarea (e.g., "Organic", "Brand: XYZ", "Aisle 3")
- Click "Add Item"
Each item is automatically assigned a color-coded badge based on its category, making it easy to visually organize your shopping list.
Notes Field:
- The notes field is optional and can be left blank
- Use it to add extra context like:
- Brand preferences ("Brand: Organic Valley")
- Location in store ("Aisle 5, bottom shelf")
- Special instructions ("Get the ripe ones")
- Alternatives ("Or get pears if apples not available")
- Notes are displayed in a collapsible section on each item (see "Viewing Notes" below)
If an item has notes, you'll see a notes icon (📋) next to the category badge:
- Click the notes icon to expand and view the notes
- The icon changes to 📝 when notes are visible
- Click again to collapse the notes
- Notes are displayed in a highlighted section below the item details
- The notes section slides in smoothly with an animation
Click the checkbox next to an item to toggle its "gotten" status. Gotten items will have a strikethrough style.
Click the trash icon (🗑️) next to an item to delete it.
The app includes powerful bulk operations to help you manage multiple items at once:
Mark All as Gotten:
- Click the "✓ Mark All Gotten" button to mark all items as gotten at once
- The button shows how many items will be affected (e.g., "Mark All Gotten (5)")
- A confirmation dialog will appear before proceeding
- Disabled when all items are already marked as gotten
- Great for quickly marking everything after a shopping trip!
Delete All Gotten Items:
- Click the "🗑️ Delete All Gotten" button to remove all gotten items from the list
- The button shows how many items will be deleted (e.g., "Delete All Gotten (3)")
- A warning confirmation dialog will appear before proceeding
- This action cannot be undone, so use with caution
- Disabled when there are no gotten items to delete
- Perfect for cleaning up your list after shopping!
Both bulk operations:
- Work with the entire list (not just filtered items)
- Include confirmation dialogs to prevent accidental actions
- Are disabled when not applicable (buttons are grayed out)
- Show real-time counts of affected items
- Sync changes immediately across all devices
The app includes powerful search, filter, and sort capabilities to help you organize and find items:
Search by Name:
- Type in the search box at the top of the list to filter items by name
- Search is case-insensitive and matches partial names
- Results update in real-time as you type (debounced for performance)
- Clear the search box to show all items again
Show/Hide Gotten Items:
- Use the "Show gotten items" toggle to filter out items you've already gotten
- Checked: Shows all items (both gotten and not gotten)
- Unchecked: Only shows items that haven't been gotten yet
Filter by Category:
- Click on category chips to filter which categories are shown
- Active categories (shown) are fully colored and opaque
- Inactive categories (hidden) are grayed out and semi-transparent
- Click again to toggle categories on/off
- All categories are shown by default
- Combine category filters with search and gotten status for powerful filtering
Sort Options:
- Sort by Name: Sort items alphabetically (A-Z or Z-A)
- Sort by Quantity: Sort items by quantity (lowest to highest or highest to lowest)
- Sort by Date: Sort items by creation date (newest first or oldest first)
- Sort by Category: Sort items by category name alphabetically (Bakery, Beverages, Dairy, etc.)
- Click the arrow button (↑/↓) to toggle between ascending and descending order
- Sorting is applied after filtering, so you can combine search/filter with any sort option
Filter and Sort Combinations:
- Search, category filters, gotten filter, and sort work together seamlessly
- For example: search for "apple", show only Produce category, hide gotten items, and sort by quantity to see how many apples you still need to buy
- Sort by category to group all items of the same type together (all Dairy items, then all Produce items, etc.) for easier shopping
- The results counter shows how many items match your current filters (e.g., "Showing 3 of 10 items")
Results Counter:
- Displays at the top of the list when filters are active
- Shows the number of visible items vs. total items
- Updates automatically as you add, remove, or modify items
- Helps you quickly see how many items match your search and filter criteria
You can export your grocery lists to various formats for backup, sharing, or printing.
Export Options:
-
JSON Format - Machine-readable format for backup or data portability
- Click "Manage List" > "Export" > "Export as JSON"
- Contains all list data including items, metadata, and statistics
- Perfect for backing up your lists or migrating data
-
CSV Format - Spreadsheet-compatible format
- Click "Manage List" > "Export" > "Export as CSV"
- Opens in Excel, Google Sheets, or any spreadsheet software
- Columns: Name, Quantity, Category, Notes, Gotten Status
- Ideal for data analysis or integration with other tools
-
Plain Text - Simple text format
- Click "Manage List" > "Export" > "Export as Text"
- Clean, readable format for sharing or printing
- Organized by category with checkboxes
- Great for quick reference or sending via email
-
Print - Print-optimized layout
- Click "Manage List" > "Export" > "Print List"
- Opens browser print dialog
- Formatted for paper with clean layout
- Perfect for taking to the store
Export Features:
- All exports include list name, creation date, and member count
- CSV and JSON exports preserve all item metadata
- Exports work offline (uses cached data)
- All list members with viewer permission or higher can export
Open the app in multiple browser tabs or on different devices and watch changes sync in real-time! All users see updates instantly thanks to Zero's real-time synchronization.
Collaboration Features:
- Changes appear within 50-500ms across all devices
- No page refresh needed - updates happen automatically
- Works with multiple lists simultaneously
- Offline changes sync when connection is restored
- Permission changes take effect immediately
- Activity trail shows who made each change
The application uses PostgreSQL with a comprehensive schema that supports authentication, list sharing, and real-time collaboration.
The following migrations are included in the server/migrations/ directory:
- 001_add_authentication.sql - Creates users and refresh_tokens tables
- 002_add_lists.sql - Creates lists table for organizing grocery items
- 003_add_list_sharing.sql - Adds list_members table and sharing functionality
- 004_migrate_to_lists.sql - Migrates existing items to list-based structure
- 005_add_list_activities.sql - Creates activity/audit trail system
- 006_add_list_customization.sql - Adds color, icon, and archive fields
- 007_add_invite_links.sql - Creates invite link system with expiration
- 008_add_list_archive.sql - Adds archive functionality (if not in 006)
- 009_add_list_pins.sql - Adds user-specific list pinning
All migrations include:
- Forward migration (applies changes)
- Rollback scripts (reverts changes)
- Data integrity constraints
- Performance indexes
- Helper functions and triggers
The easiest way to set up the database is to use the initialization script:
# Initialize database with all migrations
pnpm db:initThis runs all migrations in order and sets up the complete schema.
To run migrations manually:
# Run a specific migration
psql -h localhost -U grocery -d grocery_db -f server/migrations/001_add_authentication.sql
# Run all migrations in order
psql -h localhost -U grocery -d grocery_db -f server/migrations/001_add_authentication.sql
psql -h localhost -U grocery -d grocery_db -f server/migrations/002_add_lists.sql
psql -h localhost -U grocery -d grocery_db -f server/migrations/003_add_list_sharing.sql
psql -h localhost -U grocery -d grocery_db -f server/migrations/004_migrate_to_lists.sqlPassword: grocery (from docker-compose.yml)
Check that all tables were created successfully:
# List all tables
psql -h localhost -U grocery -d grocery_db -c "\dt"
# Expected tables:
# - users
# - refresh_tokens
# - lists
# - list_members
# - grocery_itemsCheck that indexes were created:
# List all indexes
psql -h localhost -U grocery -d grocery_db -c "\di"Rollback scripts are available in server/migrations/rollback/:
# Rollback list sharing (WARNING: removes lists and list_members tables)
psql -h localhost -U grocery -d grocery_db \
-f server/migrations/rollback/003_remove_list_sharing.sql
# Rollback authentication (WARNING: removes users and auth tables)
psql -h localhost -U grocery -d grocery_db \
-f server/migrations/rollback/001_add_authentication_rollback.sqlWarning: Rollbacks will delete data. Use with caution!
To completely reset the database (deletes all data):
# Stop database
docker compose down -v
# Start database fresh
docker compose up -d
# Wait for PostgreSQL to start
sleep 5
# Re-run migrations
pnpm db:initThe complete database schema includes:
Users & Authentication:
users- User accounts with authentication credentialsrefresh_tokens- JWT refresh tokens for secure token rotation
List Management:
lists- Grocery lists with owner, customization (color, icon), archive statuslist_members- Junction table for sharing with permission levels (owner/editor/viewer)grocery_items- Individual items linked to lists and userslist_activities- Audit trail of all list actions (items, members, changes)invite_links- Shareable invite links with expiration dateslist_pins- User-specific list pinning for favorites
Helper Functions:
user_has_list_access(user_id, list_id)- Check if user can access listget_user_list_permission(user_id, list_id)- Get user's permission levelupdate_list_access_time(user_id, list_id)- Track last access timeensure_list_owner_membership()- Auto-add owner to list_membersprevent_last_owner_removal()- Prevent removing sole ownerlog_list_activity()- Automatically log activities on list/item changes
Views:
user_lists_with_details- Lists with member counts, permissions, and customizationlist_members_with_details- Members with full user informationlist_activity_summary- Aggregated activity statistics per list
Indexes:
- Performance-optimized indexes on all foreign keys and frequently queried columns
- Composite indexes for permission checking and activity retrieval
- Unique constraints on email addresses and invite tokens
For the complete schema definition, see server/db/schema.sql.
The app uses Zero for real-time collaborative synchronization, providing:
- ✅ Local-first architecture with zero-cache
- ✅ Real-time sync across devices and users
- ✅ Offline-first with automatic sync when reconnected
- ✅ Type-safe queries with TypeScript
- ✅ Conflict-free collaborative editing
- ✅ PostgreSQL backend for persistence
// Core Item Type
interface GroceryItem {
id: string; // UUID
name: string; // Item name
quantity: number; // Quantity to buy
gotten: boolean; // Whether item is gotten
category: Category; // Item category
notes: string; // Optional notes/description
userId: string; // User who created the item
listId: string; // List this item belongs to
createdAt: number; // Timestamp
}
// List Types
interface GroceryList {
id: string; // UUID
name: string; // List name
ownerId: string; // User who created the list
color: string; // Hex color code (e.g., "#4caf50")
icon: string; // Emoji or icon (e.g., "🛒")
isArchived: boolean; // Whether list is archived
createdAt: number; // Timestamp
updatedAt: number; // Last modified timestamp
}
interface ListWithMembers extends GroceryList {
members: ListMember[]; // Array of list members
memberCount: number; // Total member count
currentUserPermission: ListPermission; // Current user's permission
isPinned: boolean; // Whether user has pinned this list
}
// Member & Permission Types
interface ListMember {
id: string; // UUID
listId: string; // List UUID
userId: string; // User UUID
userEmail: string; // User's email
userName: string; // User's display name
permission: ListPermission; // Access level
addedAt: number; // When added to list
addedBy: string; // User who invited them
}
type ListPermission = 'owner' | 'editor' | 'viewer';
// Invite Link Types
interface InviteLink {
id: string; // UUID
listId: string; // List UUID
token: string; // 32-character unique token
createdBy: string; // User who created invite
expiresAt: Date; // Expiration timestamp
createdAt: Date; // Creation timestamp
}
interface InviteDetails {
listId: string; // List UUID
listName: string; // List name
ownerName: string; // Owner's display name
memberCount: number; // Current member count
expiresAt: Date; // Expiration timestamp
}
// Activity & Statistics Types
interface ListActivity {
id: string; // UUID
listId: string; // List UUID
userId: string; // User who performed action
userName: string; // User's display name
userEmail: string; // User's email
actionType: ActivityType; // Type of action
targetType: string; // Type of target (list, item, member)
targetId: string; // Target UUID
targetName: string; // Target display name
details: object; // Additional action details
createdAt: Date; // Action timestamp
}
type ActivityType =
| 'list_created'
| 'list_renamed'
| 'list_deleted'
| 'member_added'
| 'member_removed'
| 'permission_changed'
| 'item_added'
| 'item_updated'
| 'item_deleted'
| 'item_completed'
| 'item_uncompleted'
| 'ownership_transferred'
| 'list_duplicated';
interface ListStatistics {
totalItems: number; // Total items in list
gottenItems: number; // Items marked as gotten
pendingItems: number; // Items not yet gotten
completionRate: number; // Percentage completed (0-100)
categoryBreakdown: Record<Category, number>; // Items per category
recentActivity: {
lastItemAdded: Date | null; // Last item added timestamp
lastItemCompleted: Date | null; // Last item completed timestamp
activeMembers: number; // Members active in last 7 days
};
}
// Category Type
type Category =
| 'Produce'
| 'Dairy'
| 'Meat'
| 'Bakery'
| 'Pantry'
| 'Frozen'
| 'Beverages'
| 'Other';
// User Type
interface User {
id: string; // UUID
email: string; // User email (unique)
name: string; // Display name
createdAt: Date; // Registration timestamp
lastLoginAt: Date | null; // Last login timestamp
}- Chrome/Edge: ✅
- Firefox: ✅
- Safari: ✅
- Opera: ✅
Requires modern browser with WebSocket support for real-time sync.
MIT
Feel free to submit issues and pull requests!