# ByRachel — Clothing Business Management A full-featured web application for managing a small clothing business. Built with Next.js, optimized for deployment on a single VPS with Docker. ## Features - **Product Catalog** — Products with variants (size/color), images, traffic light inventory system - **Sales Management** — Full sales workflow: create, confirm, deliver, cancel, returns - **Purchase Orders** — Supplier purchases with receive/confirm/cancel workflow - **Inventory** — Movement-based stock tracking, restock suggestions, adjustments - **Customers** — Customer management with tags and sale history - **Suppliers** — Supplier management with product associations - **Categories** — Hierarchical categories with color coding - **Analytics** — Sales trends, revenue/COGS/profit KPIs, best sellers, seasonality, cost evolution, inventory health - **Reports** — PDF generation for sales, purchases, inventory, customers, brand, analytics - **Public Catalog** — Customer-facing product catalog at `/catalogo` - **Settings** — Brand configuration, pricing rules, password management, TOTP 2FA - **Audit Logging** — All critical operations are logged with before/after data ## Tech Stack | Layer | Technology | |-------|-----------| | Framework | Next.js 16 (App Router) | | Language | TypeScript (strict mode) | | Styling | Tailwind CSS 4 | | UI Components | shadcn/ui (Radix UI primitives) | | Database | SQLite (better-sqlite3) | | ORM | Drizzle ORM | | Authentication | JWT (jose) + TOTP (otplib) + Argon2 password hashing | | Charts | Recharts | | PDF Generation | pdf-lib (client-side) | | Image Processing | Sharp | | Testing | Vitest | | Drag & Drop | @dnd-kit | | Validation | Zod | | Container | Docker (multi-stage build) | ## Prerequisites - **Node.js 22+** and npm (for development) - **Docker & Docker Compose** (for container deployment) - **A domain name** (for production HTTPS, optional for local use) ## Quick Start (Docker) The fastest way to get ByRachel running: ```bash # 1. Clone the repository git clone byrachel cd byrachel # 2. Create environment file cp .env.example .env # Edit .env and change JWT_SECRET to a random secure value: # openssl rand -base64 32 # 3. Start the application docker compose up -d # 4. Open in browser # http://localhost:3000 # You'll be redirected to /setup to create your admin account ``` ## Development Setup ```bash # 1. Clone and install git clone byrachel cd byrachel npm install # 2. Set up environment cp .env.example .env # 3. Initialize the database npm run db:push # 4. (Optional) Seed sample data npm run seed # 5. Start development server npm run dev # Open http://localhost:3000 ``` ### Development Commands | Command | Description | |---------|-------------| | `npm run dev` | Start development server with hot reload | | `npm run build` | Create production build | | `npm run start` | Start production server | | `npm run test` | Run all tests | | `npm run test:watch` | Run tests in watch mode | | `npm run test:coverage` | Run tests with coverage report | | `npm run db:push` | Push schema changes to database | | `npm run db:generate` | Generate migration files | | `npm run db:migrate` | Run pending migrations | | `npm run db:studio` | Open Drizzle Studio (database GUI) | | `npm run seed` | Seed database with sample data | ## Environment Variables | Variable | Description | Default | Required | |----------|-------------|---------|----------| | `DATABASE_URL` | SQLite database file path. Dev: `./data/db.sqlite`, Docker: `/app/data/db.sqlite` | `./data/db.sqlite` | Yes | | `JWT_SECRET` | Secret key for JWT signing. **MUST be changed in production.** Generate with: `openssl rand -base64 32` | `change-me-to-a-256-bit-secret` | Yes | | `UPLOAD_DIR` | Directory for uploaded images. Dev: `./public/uploads`, Docker: `/app/public/uploads` | `./public/uploads` | Yes | | `STORAGE_ADAPTER` | Storage backend. Currently only `local` (filesystem) is supported | `local` | Yes | | `NODE_ENV` | Node environment: `development` or `production` | `production` | Yes | | `NEXT_TELEMETRY_DISABLED` | Disable Next.js telemetry. Set to `1` to disable | `1` | No | ## API Routes Reference All API routes are under `/api/`. Admin routes require a valid JWT cookie (except `/api/health` and `/api/auth/*`). ### Authentication | Method | Route | Description | |--------|-------|-------------| | POST | `/api/auth/setup` | Initial admin account creation (first run only) | | POST | `/api/auth/login` | Login with email/password | | POST | `/api/auth/verify-totp` | Verify TOTP code | | GET | `/api/auth/me` | Get current user info | | POST | `/api/auth/logout` | Logout (clear session) | | PUT | `/api/auth/change-password` | Change password | | POST | `/api/auth/regenerate-recovery-codes` | Generate new recovery codes | ### Products | Method | Route | Description | |--------|-------|-------------| | GET/POST | `/api/products` | List / Create products | | GET/PUT/DELETE | `/api/products/[id]` | Get / Update / Delete product | | POST | `/api/products/[id]/duplicate` | Duplicate a product | | GET/POST | `/api/products/[id]/images` | List / Upload product images | | DELETE | `/api/products/[id]/images/[imageId]` | Delete product image | | PUT | `/api/products/[id]/images/reorder` | Reorder product images | | GET/POST | `/api/products/[id]/variants` | List / Create variants | | PUT/DELETE | `/api/products/[id]/variants/[variantId]` | Update / Delete variant | | GET | `/api/products/[id]/variants/[variantId]/movements` | Get variant movements | ### Categories | Method | Route | Description | |--------|-------|-------------| | GET/POST | `/api/categories` | List / Create categories | | PUT/DELETE | `/api/categories/[id]` | Update / Delete category | ### Sales | Method | Route | Description | |--------|-------|-------------| | GET/POST | `/api/sales` | List / Create sales | | GET/PUT/DELETE | `/api/sales/[id]` | Get / Update / Delete sale | | POST | `/api/sales/[id]/confirm` | Confirm sale | | POST | `/api/sales/[id]/deliver` | Mark sale as delivered | | POST | `/api/sales/[id]/cancel` | Cancel sale | | POST | `/api/sales/[id]/return` | Process sale return | ### Purchases | Method | Route | Description | |--------|-------|-------------| | GET/POST | `/api/purchases` | List / Create purchases | | GET/PUT/DELETE | `/api/purchases/[id]` | Get / Update / Delete purchase | | POST | `/api/purchases/[id]/receive` | Receive purchase items | | POST | `/api/purchases/[id]/confirm` | Confirm purchase | | POST | `/api/purchases/[id]/cancel` | Cancel purchase | ### Inventory | Method | Route | Description | |--------|-------|-------------| | GET | `/api/inventory` | Get inventory status | | POST | `/api/inventory/restock` | Restock variants | | POST | `/api/inventory/adjust` | Manual stock adjustment | | GET | `/api/inventory/movements` | List stock movements | ### Customers | Method | Route | Description | |--------|-------|-------------| | GET/POST | `/api/customers` | List / Create customers | | GET/PUT/DELETE | `/api/customers/[id]` | Get / Update / Delete customer | | GET/POST | `/api/customers/[id]/tags` | List / Add customer tags | | DELETE | `/api/customers/[id]/tags/[tagId]` | Remove customer tag | | GET | `/api/customer-tags` | List all customer tags | ### Suppliers | Method | Route | Description | |--------|-------|-------------| | GET/POST | `/api/suppliers` | List / Create suppliers | | GET/PUT/DELETE | `/api/suppliers/[id]` | Get / Update / Delete supplier | | GET | `/api/suppliers/[id]/products` | Get supplier's products | ### Analytics | Method | Route | Description | |--------|-------|-------------| | GET | `/api/analytics/summary` | Sales summary KPIs | | GET | `/api/analytics/sales-trend` | Revenue/units over time | | GET | `/api/analytics/categories` | Sales by category | | GET | `/api/analytics/products` | Product performance | | GET | `/api/analytics/customers` | Customer analytics | | GET | `/api/analytics/cost-evolution` | Cost price evolution | | GET | `/api/analytics/inventory-health` | Inventory health metrics | | GET | `/api/analytics/seasonality` | Seasonal sales patterns | ### Reports (PDF Generation) | Method | Route | Description | |--------|-------|-------------| | GET | `/api/reports/sales` | Sales report data | | GET | `/api/reports/sales/[id]` | Single sale report | | GET | `/api/reports/sales/profits` | Profit report | | GET | `/api/reports/purchases` | Purchases report data | | GET | `/api/reports/purchases/[id]` | Single purchase report | | GET | `/api/reports/inventory` | Inventory report | | GET | `/api/reports/customers` | Customers report | | GET | `/api/reports/customers/[id]` | Single customer report | | GET | `/api/reports/brand` | Brand/catalog report | | GET | `/api/reports/analytics/best-sellers` | Best sellers report | | GET | `/api/reports/analytics/low-rotation` | Low rotation report | ### Settings | Method | Route | Description | |--------|-------|-------------| | GET/PUT | `/api/settings/brand` | Get / Update brand settings | | GET/PUT | `/api/settings/pricing` | Get / Update pricing settings | ### Admin | Method | Route | Description | |--------|-------|-------------| | GET | `/api/admin/backup` | Download full backup (tar.gz) | ### Other | Method | Route | Description | |--------|-------|-------------| | GET | `/api/health` | Health check (public, no auth) | | POST | `/api/upload` | Upload image file | ## Backup & Restore ### Automated Backup (Admin Endpoint) Admin users can download a full backup from the app: ``` GET /api/admin/backup ``` This returns a `tar.gz` archive containing: - `data/` — SQLite database files (main DB, WAL, SHM) - `public/uploads/` — All uploaded images **Requirements**: Must be authenticated as admin. Download over HTTPS only. ### Manual SQLite Backup SQLite with WAL mode requires a safe backup procedure to avoid corruption: ```bash # Option 1: Using SQLite .backup command (safest) docker compose exec app sh -c 'sqlite3 /app/data/db.sqlite ".backup /app/data/backup.db"' # Then copy backup.db from the container # Option 2: Copy WAL-safe (stop writes first) # 1. Temporarily stop the application docker compose stop app # 2. Copy the database files docker compose cp app:/app/data/db.sqlite ./backup/db.sqlite docker compose cp app:/app/data/db.sqlite-wal ./backup/db.sqlite-wal docker compose cp app:/app/data/db.sqlite-shm ./backup/db.sqlite-shm # 3. Restart the application docker compose start app # Option 3: Using docker volume backup docker run --rm -v byrachel_db-data:/data -v $(pwd)/backup:/backup alpine \ tar czf /backup/byrachel-db-$(date +%Y%m%d).tar.gz -C /data . ``` ### Image Volume Backup ```bash docker run --rm -v byrachel_uploads:/uploads -v $(pwd)/backup:/backup alpine \ tar czf /backup/byrachel-uploads-$(date +%Y%m%d).tar.gz -C /uploads . ``` ### Restore Procedure ```bash # 1. Stop the application docker compose stop app # 2. Restore database docker run --rm -v byrachel_db-data:/data -v $(pwd)/backup:/backup alpine \ sh -c 'rm -rf /data/* && tar xzf /backup/byrachel-db-YYYYMMDD.tar.gz -C /data' # 3. Restore uploads docker run --rm -v byrachel_uploads:/uploads -v $(pwd)/backup:/backup alpine \ sh -c 'rm -rf /uploads/* && tar xzf /backup/byrachel-uploads-YYYYMMDD.tar.gz -C /uploads' # 4. Start the application docker compose start app # 5. Verify docker compose ps # Check health status curl http://localhost:3000/api/health # Should return {"status":"ok"} ``` ## Production Deployment ### Docker Deployment ```bash # 1. Clone on your server git clone byrachel cd byrachel # 2. Configure environment cp .env.example .env nano .env # Set JWT_SECRET to a random value: openssl rand -base64 32 # 3. Build and start docker compose up -d --build # 4. Verify docker compose ps docker compose logs -f app ``` ### Nginx Proxy Manager Setup [Nginx Proxy Manager](https://nginxproxymanager.com/) is the recommended reverse proxy for HTTPS and domain routing. #### 1. Install Nginx Proxy Manager ```bash # Add to your docker-compose.yml or run separately docker run -d \ --name nginx-proxy-manager \ -p 80:80 -p 81:81 -p 443:443 \ -v ./npm-data:/data \ -v ./npm-le:/etc/letsencrypt \ jc21/nginx-proxy-manager:latest ``` #### 2. Configure Proxy Host 1. Open NPM admin: `http://your-server-ip:81` 2. Default login: `admin@example.com` / `changeme` 3. Go to **Hosts → Proxy Hosts → Add Proxy Host** 4. Configure: - **Domain Names**: `yourdomain.com` - **Scheme**: `http` - **Hostname**: `byrachel-app` (Docker container name) or your server's Docker IP - **Port**: `3000` - **Common Settings**: - Enable: `Block Common Exploits` - Enable: `Websockets Support` - **SSL Tab**: - SSL Certificate: Request a new Let's Encrypt certificate - Enable: `Force SSL` - Enable: `HTTP/2 Support` #### 3. Docker Network (recommended) If both NPM and ByRachel are in the same Docker network: ```yaml # docker-compose.yml services: app: # ... existing config ... networks: - proxy-net networks: proxy-net: external: true ``` Then in NPM, use the container name `byrachel-app` as the hostname. ### HTTPS Configuration HTTPS is configured through Nginx Proxy Manager: 1. **Request Certificate**: In the Proxy Host SSL tab, select "Request a new SSL Certificate" 2. **Let's Encrypt**: NPM handles Let's Encrypt automatically 3. **Force SSL**: Enable to redirect HTTP → HTTPS 4. **Auto-renewal**: Certificates renew automatically ### Domain Configuration 1. **DNS Record**: Create an A record pointing your domain to your server's IP ``` Type: A Name: yourdomain.com (or @) Value: YOUR_SERVER_IP TTL: 300 ``` 2. **Wait for DNS propagation** (usually minutes, up to 48h) 3. **Configure in NPM**: Add the domain in the Proxy Host settings (see above) ### Auto-restart & Log Management #### Auto-restart The Docker Compose configuration includes `restart: unless-stopped`, which means: - Container restarts automatically on crash - Container starts on Docker daemon startup (server reboot) - Container only stays stopped if you explicitly `docker compose stop` For additional server-level auto-start, create a systemd service: ```ini # /etc/systemd/system/byrachel.service [Unit] Description=ByRachel Docker Compose Requires=docker.service After=docker.service [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=/path/to/byrachel ExecStart=/usr/bin/docker compose up -d ExecStop=/usr/bin/docker compose down TimeoutStartSec=0 [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl enable byrachel.service sudo systemctl start byrachel.service ``` #### Log Management ```bash # View live logs docker compose logs -f app # View last 100 lines docker compose logs --tail=100 app # Configure log rotation (in docker-compose.yml, add to app service): # logging: # driver: json-file # options: # max-size: "10m" # max-file: "3" ``` ## SQLite → PostgreSQL Migration Plan ByRachel uses Drizzle ORM, which supports multiple database dialects. Migration to PostgreSQL is possible when needed: ### Steps 1. **Install PostgreSQL driver**: ```bash npm install pg npm install @types/pg ``` 2. **Update Drizzle config** (`drizzle.config.ts`): ```typescript export default defineConfig({ schema: './src/lib/db/schema.ts', out: './src/lib/db/migrations', dialect: 'postgresql', // Change from 'sqlite' dbCredentials: { host: process.env.PGHOST, port: Number(process.env.PGPORT), user: process.env.PGUSER, password: process.env.PGPASSWORD, database: process.env.PGDATABASE, }, }); ``` 3. **Update database connection** (`src/lib/db/index.ts`): - Replace `drizzle(betterSqlite3(...))` with `drizzle(pgPool)` or `drizzle(nodePostgres(...))` 4. **Update environment variables**: ```env DATABASE_URL=postgresql://user:password@localhost:5432/byrachel ``` 5. **Generate and run migrations**: ```bash npm run db:generate npm run db:migrate ``` 6. **Data migration**: Export data from SQLite and import into PostgreSQL using a migration script. ### Considerations - SQLite uses `integer` primary keys with auto-increment — PostgreSQL uses `serial` or `identity` - SQLite `boolean` (integer 0/1) maps directly to PostgreSQL `boolean` - SQLite `text` timestamps work in both, but PostgreSQL has native `timestamp` types - Test thoroughly in a staging environment before migrating production data ## First-Run Setup ### 1. Initial Admin Account 1. Open the app in your browser (e.g., `http://localhost:3000`) 2. You'll be redirected to `/setup` (only available when no users exist) 3. Fill in: - **Email**: Admin email address - **Password**: Strong password (min 8 characters) - **Name**: Admin display name (optional) 4. Click "Create Account" ### 2. TOTP Configuration After creating the account, TOTP (Time-based One-Time Password) setup begins automatically: 1. A **QR code** is displayed on screen 2. Open your authenticator app (Google Authenticator, Authy, 1Password, etc.) 3. Scan the QR code (or enter the secret key manually) 4. Enter the 6-digit code from your authenticator app to verify 5. **Save your recovery codes** in a secure location — these are your backup if you lose access to your authenticator ### 3. Brand Configuration After logging in for the first time: 1. Navigate to **Settings** (gear icon in sidebar) 2. Configure your brand: - **Store Name**: Your business name - **Logo**: Upload your logo image - **Tagline**: Short description - **Colors**: Primary and secondary brand colors - **Contact Info**: Phone, email, WhatsApp - **Address**: Business address - **Footer Text**: Custom footer for catalog pages 3. Click "Save Settings" 4. To publish your public catalog, check "Publish Catalog" in settings ## Known Limitations - **SQLite write concurrency**: SQLite handles concurrent reads well but writes are serialized. For a single-admin small business, this is not an issue. If you need multi-writer support, migrate to PostgreSQL (see migration plan above). - **No built-in email**: The app does not send emails. Password recovery uses local recovery codes instead of email-based reset. - **Single admin**: The app supports admin and operator roles, but the initial setup creates only one admin user. Additional users must be created manually via the database. - **No automated backups**: The backup endpoint provides on-demand backups. Set up cron jobs or external tools for scheduled backups. - **Local file storage only**: Images are stored on the local filesystem. For multi-instance deployments, a shared storage solution (S3, etc.) would be needed. - **No real-time updates**: The app uses standard HTTP requests. Changes made in one browser tab are not reflected in other tabs until refresh. - **Catalog is read-only for customers**: The public catalog at `/catalogo` is for browsing only. Customers cannot place orders through the app. ## License Private — All rights reserved.