This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A production-ready Point of Sale (POS) system built with Angular 21 (standalone components) and Node.js/Express/MongoDB. Supports barcode scanning, digital scale integration, receipt printing, multi-user roles, and register management for retail environments.
# Start both frontend and backend concurrently (recommended)
npm run dev
# Or start separately:
# Terminal 1 - Backend (runs on https://localhost:3001)
cd server
npm run dev
# Terminal 2 - Frontend (runs on https://localhost:4200)
npm start# Build for production
ng build --configuration production
# Output: dist/retail-pos/
# Run tests
ng test
# Install all dependencies (root + server)
npm run install-all# Seed initial data (users, sample products)
cd server
node seed.jsDefault login: admin / admin123, manager / manager123, cashier / cashier123
See deployment/README.md for production deployment using Git-based workflow:
# On server: Initial deployment
sudo ./deployment/scripts/deploy.sh
# On server: Update from Git
sudo ./deployment/scripts/update.shStandalone Components - No NgModule, all components use standalone: true with imports: [...] in decorator
Key Architectural Patterns:
- Lazy Loading: All routes use
loadComponent()inapp.routes.ts - Signals & RxJS: Signals for local state (
signal(),computed()), RxJS for async streams - Service Injection: All services use
providedIn: 'root'for singleton pattern - Route Guards:
authGuardprotects authenticated routes,roleGuard(['admin', 'manager'])for role-based access - HTTP Interceptor:
authInterceptorautomatically adds JWT tokens to API requests - State Services:
CartStateService(cart signals),SearchStateService(shared search),RegisterService(active register)
Component Structure:
@Component({
selector: 'app-example',
standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule, ...],
templateUrl: './example.component.html',
styleUrls: ['./example.component.scss']
})
export class ExampleComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit(): void {
// Use takeUntil for RxJS cleanup
this.service.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(...);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}Important Frontend Directories:
src/app/components/- All UI components (pos, cashier, dashboard, products, etc.)src/app/services/- Business logic services (auth, cart, product, sale, etc.)src/app/guards/- Route guards (authGuard, roleGuard)src/app/interceptors/- HTTP interceptors (authInterceptor)src/app/models/- TypeScript interfaces/typessrc/assets/i18n/- Translation files (en.json, es.json)src/styles/- Global styles and theme system
RESTful API - All routes prefixed with /api/{resource}
Database: MongoDB with Mongoose ORM
Authentication: JWT tokens, 7-day expiration
Middleware Stack:
protect- Verifies JWT and attaches user toreq.usercheckPermission(['permission'])- Validates user has required permissions- CORS configured to allow all origins (restrict in production)
- Body parser for JSON/urlencoded
- Request logging middleware (logs method, path, body)
API Structure:
server/
├── index.js # Express app, middleware, route registration
├── config/database.js # MongoDB connection
├── models/ # Mongoose schemas
│ ├── User.js # username, role, permissions, active
│ ├── Product.js # sku, barcode, price, stock, category, requiresScale, incompleteInfo
│ ├── Category.js # name, color, icon, parent (hierarchical)
│ ├── Sale.js # saleNumber, items[], total, paymentMethod, cashier, register, status
│ ├── Cart.js # user, items[], register (session-specific)
│ ├── Register.js # name, status, openedBy, openingBalance, currentBalance
│ ├── Provider.js # name, contact info
│ └── PrintTemplate.js # receipt templates
├── routes/ # API endpoints
│ ├── auth.js # POST /login, /register
│ ├── products.js # CRUD + GET /barcode/:code
│ ├── categories.js # CRUD
│ ├── sales.js # CRUD + PUT /:id/cancel, GET /reports/summary
│ ├── carts.js # Session cart management
│ ├── registers.js # Open/close register operations
│ ├── users.js # User management (admin/manager only)
│ ├── providers.js # Supplier management
│ └── templates.js # Print template management
└── middleware/
└── auth.js # protect, checkPermission
Environment Variables (server/.env):
PORT=3001MONGODB_URI=mongodb://admin:productdb2025@localhost:27017/products?authSource=adminJWT_SECRET=your_jwt_secret(min 32 chars in production)JWT_EXPIRE=7dNODE_ENV=developmentorproduction
Product - Core inventory item
sku: Unique product codebarcode: Barcode for scanning (indexed)name,description,category,providerprice,cost,stockrequiresScale: Boolean - prompts for weight entryincompleteInfo: Boolean - flagged for later completion (used in quick product creation)active,available: Boolean - control visibility
Sale - Transaction record
saleNumber: Auto-incremented unique identifieritems[]: Array of{ product, quantity, price, subtotal }subtotal,discount,tax,totalpaymentMethod: 'cash', 'card', 'transfer', 'mixed'cashier,register: Referencesstatus: 'completed', 'cancelled'isInternal: Boolean - internal transfer flag
Cart - Session-based cart (per user per register)
- Linked to
userandregister items[]: Same structure as Sale items- Temporary storage before sale completion
Register - Cash register/POS terminal
name,locationstatus: 'open' or 'closed'openedBy: User referenceopeningBalance,currentBalance- Cashiers must open a register before processing sales
User - Authentication and authorization
username,fullName,emailrole: 'admin', 'manager', 'cashier', 'employee'permissions[]: Array of permission stringsactive: Boolean - can be disabled without deletion
Roles:
admin- Full system accessmanager- Management, reports, inventorycashier- POS, sales processingemployee- Limited access
Permissions:
sales- Process transactionsrefunds- Cancel/refund salesdiscounts- Apply discountsreports- View sales reportsinventory- Manage products/categoriesusers- User managementsettings- System configuration
Implementation:
- Frontend:
roleGuard(['admin', 'manager'])in routes - Backend:
checkPermission(['inventory', 'settings'])middleware
SCSS with Neumorphic Design
Import theme variables: @use "../../../styles/theme" as *;
Key Variables:
- Colors:
$blue-primary,$green-success,$red-danger,$neumorphic-bg,$text-primary - Shadows:
$shadow-neumorphic,$shadow-neumorphic-inset - Sizing:
$touch-target-sm: 30px,$touch-target-md: 34px - Breakpoints:
$breakpoint-sm: 480px,$breakpoint-md: 768px,$breakpoint-lg: 1024px
Mixins:
@include card- Neumorphic card style@include button-primary- Primary button style@include respond-to(md)- Responsive media queries
Example:
@use "../../../styles/theme" as *;
.my-component {
@include card;
padding: $spacing-4;
button {
@include button-primary;
min-height: $touch-target-md;
}
@include respond-to(md) {
padding: $spacing-2;
}
}Files: src/assets/i18n/en.json, es.json
Structure: Hierarchical JSON keys
{
"POS": {
"TITLE": "Point of Sale",
"SEARCH_PLACEHOLDER": "Search products..."
}
}Usage in Templates:
<h1>{{ 'POS.TITLE' | translate }}</h1>
<input [placeholder]="'POS.SEARCH_PLACEHOLDER' | translate">Always add both English and Spanish when creating new UI text.
- Manual input: Text field in POS
- Camera scanning:
html5-qrcodelibrary (mobile-friendly) - Hardware scanners: Work as keyboard input
- Product lookup:
GET /api/products/barcode/:barcode
- Web Serial API (Chrome/Edge 89+ only, requires HTTPS)
ScaleService.connectScale()to pair device- Products with
requiresScale: truetrigger weight entry modal - Real-time weight display, manual fallback
- QZ Tray integration for physical printers
- Customizable print templates (stored in
PrintTemplatemodel) - Signing endpoint:
POST /api/sign(usesserver/private-key.pem) - Template variables:
{{storeName}},{{items}},{{total}}, etc.
- When barcode not found in POS, prompt for quick creation
- Creates product with
incompleteInfo: trueflag - Dashboard shows incomplete products card for follow-up editing
- Allows uninterrupted cashier workflow
- Cashiers must open register before processing sales
- Opening balance recorded
- Tracks all transactions on that register
- Auto-close at midnight option (systemd service:
posdic-auto-close.service)
- Generate:
ng generate component components/my-feature --standalone - Add route in
app.routes.tswith lazy loading - Create service if needed:
ng generate service services/my-feature - Define TypeScript interfaces in
src/app/models/ - Add translations in
en.jsonandes.json - Import theme:
@use "../../../styles/theme" as *;
- Create/update Mongoose model in
server/models/MyModel.js - Create route handler in
server/routes/myresource.js - Register route in
server/index.js:app.use('/api/myresource', myresourceRoutes) - Use middleware:
router.get('/', protect, async (req, res) => { ... }) - Return consistent JSON:
res.json({ data })orres.status(400).json({ message })
Sales involve: Cart → Sale → Register → Receipt
- Cart State:
CartStateServicemanages cart items (signals) - Sale Creation:
SaleService.createSale(saleData) - Register Update: Backend updates register balance
- Receipt:
ReceiptGeneratorService.generateReceipt(sale)+ QZ Tray print
Development:
- Frontend:
src/environments/environment.ts(orenvironment.development.ts)apiUrl: "https://localhost:3001/api"
- Backend:
server/.envMONGODB_URI=mongodb://admin:...@localhost:27017/products
Production:
- Frontend:
src/environments/environment.prod.tsapiUrl: "/api"(relative, nginx proxy)- Set via file replacement in
angular.json
- Backend:
/var/www/posdic/backend/.envon server- Update
MONGODB_URI,JWT_SECRET,NODE_ENV=production
- Update
@ViewChild('barcodeInput') barcodeInput!: ElementRef;
focusBarcode(): void {
setTimeout(() => this.barcodeInput?.nativeElement.focus(), 100);
}// Child
@Output() addItem = new EventEmitter<{ value: number }>();
confirmAdd(): void {
this.addItem.emit({ value: this.currentValue });
}
// Parent template
<app-calculator (addItem)="onCalculatorAdd($event)"></app-calculator><div class="modal-overlay" *ngIf="showModal" (click)="closeModal()">
<div class="modal-content" (click)="$event.stopPropagation()">
<!-- Modal content -->
</div>
</div>.modal-overlay {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: $z-modal; // 1050
display: flex;
align-items: center;
justify-content: center;
}this.toastService.show("Success message", "success");
this.toastService.show("Error occurred", "error");
this.toastService.show("Warning", "warning");- JWT tokens stored in localStorage, sent via Authorization header
- Password hashing with bcryptjs (backend)
- CORS configured permissively in dev, restrict in production
- Change default passwords after seeding database
- Strong JWT_SECRET (min 32 random characters)
- MongoDB authentication enabled
- HTTPS required for camera/scale features (Web APIs requirement)
- Route guards prevent unauthorized access on frontend
- Middleware enforces permissions on backend
Development:
- Self-signed certificates in
certs/andserver/certs/ - Angular serves on HTTPS:
ng serve --ssl --ssl-key certs/localhost-key.pem --ssl-cert certs/localhost-cert.pem - Configured in
angular.jsonserve options - Backend uses HTTPS in
server/index.js(reads certs)
Production:
- Use Let's Encrypt via
certbot --nginx -d yourdomain.com - Nginx config in
deployment/nginx/posdic.conf - HTTP redirects to HTTPS
- Backend runs on HTTP (nginx handles SSL termination)
MongoDB Connection Issues:
- Verify MongoDB running:
mongoshormongo - Check credentials in
server/.env - Ensure database exists and auth enabled
Build Errors:
- Clear Angular cache:
rm -rf .angular/cache - Delete and reinstall:
rm -rf node_modules package-lock.json && npm install - Check Angular CLI version:
ng version
Camera Scanner Not Working:
- Requires HTTPS (browser security)
- Check browser permissions (camera access)
html5-qrcodelibrary must be installed
Scale Not Connecting:
- Chrome/Edge only (Web Serial API)
- HTTPS required
- Check USB connection and permissions
API 401 Unauthorized:
- Token expired (7 days default)
- Re-login to get new token
- Check
authInterceptoris applied
- Initial seed includes ~16,994 grocery products (mentioned in README)
server/seed.jscreates users, sample categories, products- Run once after fresh database setup
- Frontend configured with
--host 0.0.0.0 --disable-host-checkinpackage.jsonstart script - Allows access from network devices (tablets, phones)
- Update
environment.tswith server IP for LAN clients - Windows firewall rules:
setup-firewall.ps1(Administrator)
- Backend implements fuzzy search with Levenshtein distance
- Tolerates misspellings in product search
- 300ms debounce on search input (prevents excessive API calls)
- POS component auto-focuses barcode input for scanner compatibility
- Uses
@ViewChild+setTimeoutpattern
- Sales can be marked
isInternal: truefor inventory transfers - Does not affect revenue reports (filtered out in analytics)
active: false- Product exists but hidden (soft delete)available: false- Out of stock or temporarily unavailableincompleteInfo: true- Needs additional details (quick creation)
Critical Configuration Files:
angular.json- Angular build config, output path isdist/retail-pospackage.json- Frontend scripts (start,dev,build)server/package.json- Backend dependencies and scriptsserver/.env- Backend environment variables (not in Git)src/environments/- Frontend API URLs (different per environment)src/app/app.routes.ts- All application routes with guardsserver/index.js- Express app entry point, middleware, routes
Deployment:
deployment/- All deployment scripts, nginx configs, systemd servicesdeployment/scripts/deploy.sh- Initial Git-based deploymentdeployment/scripts/update.sh- Update from Git repo- See
deployment/README.mdfor complete deployment guide