A complete technical reference for developers who want to install, extend, contribute to, or deploy BizManager.
- Architecture Overview
- Project Structure
- Local Development Setup
- Environment Variables
- Database
- Backend — API Reference
- Authentication Flow
- Role-Based Access Control
- Frontend Architecture
- Adding a New Feature
- Production Deployment
- Backup Automation
- Contributing
Browser (http://localhost:5173)
│
│ React 18 + Vite + Tailwind CSS
│ Zustand (state management)
│ axios (HTTP client with token-refresh interceptor)
│
│ Vite dev-server proxies /api → http://localhost:5000
│
▼
Express.js API (http://localhost:5000)
│
├── helmet (security headers)
├── cors (origin whitelist via FRONTEND_URL)
├── morgan (HTTP request logging)
├── express-rate-limit (auth: 20 req/15 min, api: 200 req/min)
├── express-validator (input validation)
│
├── JWT Authentication
│ access token → 15 m TTL (signed with JWT_SECRET)
│ refresh token → 7 d TTL (signed with JWT_REFRESH_SECRET)
│ stored in SQLite refresh_tokens table
│
├── Role-Based Authorization (admin / manager / employee)
│
▼
SQLite — better-sqlite3 (synchronous, embedded)
└── backend/database/bizmanager.db
Key design choices:
- SQLite — zero-config, file-based database. Perfect for single-server / local deployments. No separate database process needed.
- Synchronous DB layer —
better-sqlite3is fully synchronous, which simplifies the Express route handlers (noasync/awaitneeded for queries). - JWT with refresh tokens — short-lived access tokens (15 min) plus long-lived refresh tokens (7 days) stored server-side. Tokens are rotated on every refresh.
- Vite proxy — in development the frontend's
/apirequests are proxied by Vite's dev server, so no CORS configuration is needed during development (CORS only matters for production deployments).
bizmanager/
├── README.md
├── start-linux.sh # one-click startup for Linux/macOS
├── start-windows.bat # one-click startup for Windows
├── docs/
│ ├── USER_GUIDE.md # end-user documentation
│ └── DEVELOPER_GUIDE.md # this file
│
├── backend/
│ ├── server.js # Express app entry point
│ ├── package.json
│ ├── .env.example # template for environment variables
│ ├── scripts/
│ │ └── init-env.js # cross-platform .env initialiser
│ ├── database/
│ │ ├── schema.sql # DDL — all CREATE TABLE / INSERT statements
│ │ ├── db.js # opens the SQLite connection + runs schema
│ │ └── setup.js # one-time seed: creates the default admin user
│ ├── middleware/
│ │ ├── auth.js # authenticate() and authorize() middleware
│ │ ├── errorHandler.js # global Express error handler
│ │ └── validate.js # wraps express-validator result checking
│ ├── routes/ # thin router files — only wiring
│ │ ├── auth.js
│ │ ├── backup.js
│ │ ├── customers.js
│ │ ├── dashboard.js
│ │ ├── employees.js
│ │ ├── expenses.js
│ │ ├── products.js
│ │ ├── reports.js
│ │ ├── sales.js
│ │ └── settings.js
│ └── controllers/ # business logic
│ ├── authController.js
│ ├── backupController.js
│ ├── customerController.js
│ ├── dashboardController.js
│ ├── employeeController.js
│ ├── expenseController.js
│ ├── productController.js
│ ├── reportController.js
│ ├── saleController.js
│ └── settingsController.js
│
└── frontend/
├── index.html
├── vite.config.js # Vite + /api proxy config
├── tailwind.config.js
├── postcss.config.js
├── package.json
└── src/
├── main.jsx # React + Router entry point
├── App.jsx # route definitions + auth guard
├── index.css # Tailwind directives + custom CSS classes
├── api/
│ └── axios.js # axios instance with token-refresh interceptor
├── store/
│ ├── authStore.js # Zustand store — user / login / logout
│ └── appStore.js # Zustand store — dark mode / notifications / confirm modal
├── hooks/
│ └── useApi.js # thin hook wrapping api calls with loading/error state
├── utils/
│ └── format.js # formatCurrency / formatDate / formatDateTime
├── components/
│ ├── Layout.jsx
│ ├── Sidebar.jsx
│ ├── Header.jsx
│ ├── DataTable.jsx
│ ├── Modal.jsx
│ ├── ConfirmModal.jsx
│ ├── NotificationContainer.jsx
│ └── StatsCard.jsx
└── pages/
├── Login.jsx
├── Dashboard.jsx
├── Customers.jsx
├── Products.jsx
├── Sales.jsx
├── Expenses.jsx
├── Employees.jsx
├── Reports.jsx
└── Settings.jsx
| Tool | Minimum version | Notes |
|---|---|---|
| Node.js | 18.11 | Required for node --watch |
| npm | 9 | Bundled with Node.js LTS |
| C++ build tools | — | Required by better-sqlite3 |
Install C++ build tools:
- Windows:
npm install -g windows-build-tools(run as Administrator) or install Visual C++ Build Tools - Ubuntu/Debian:
sudo apt install build-essential python3 - Fedora/RHEL:
sudo dnf install gcc-c++ make python3 - macOS:
xcode-select --install
# 1. Clone the repository
git clone <repo-url>
cd bizmanager
# 2. Set up the backend
cd backend
npm install # installs all dependencies and compiles better-sqlite3
npm run init # copies .env.example → .env (cross-platform Node.js script)
# IMPORTANT: open .env and set your own JWT secrets!
npm run setup # creates the SQLite database and default admin account
# 3. Set up the frontend (new terminal)
cd ../frontend
npm install
# 4. Start both servers
# Terminal A (backend):
cd backend && npm run dev # nodemon-like: restarts on file changes (node --watch)
# Terminal B (frontend):
cd frontend && npm run dev # Vite HMR dev serverOpen http://localhost:5173 — Vite proxies all /api requests to the backend at port 5000.
# Linux / macOS
bash start-linux.sh
# Windows (CMD or double-click)
start-windows.batAll variables live in backend/.env (copy from .env.example).
| Variable | Description | Default |
|---|---|---|
PORT |
Port the Express server listens on | 5000 |
NODE_ENV |
development or production |
development |
JWT_SECRET |
Must change — signs access tokens | placeholder |
JWT_REFRESH_SECRET |
Must change — signs refresh tokens | placeholder |
JWT_EXPIRES_IN |
Access token lifetime | 15m |
JWT_REFRESH_EXPIRES_IN |
Refresh token lifetime | 7d |
DB_PATH |
Path to the SQLite file (relative to backend/) |
./database/bizmanager.db |
FRONTEND_URL |
CORS origin whitelist | http://localhost:5173 |
Security: Use long random strings for JWT secrets in production.
Generate them with:node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
| Table | Purpose |
|---|---|
users |
Login accounts with hashed passwords and roles |
refresh_tokens |
Server-side storage of refresh tokens (invalidated on logout) |
settings |
Key-value store for business configuration |
categories |
Product categories |
products |
Inventory items with price, cost, and stock |
customers |
Customer contact records |
sales |
Invoice headers (totals, status, customer link) |
sale_items |
Individual line items within a sale |
expenses |
Business expense entries |
employees |
Staff records (optionally linked to a users row) |
notifications |
Per-user notification records (reserved for future use) |
backend/database/schema.sql — every CREATE TABLE uses IF NOT EXISTS so the file is safe to re-run on an existing database.
# Delete the database file and re-run setup
rm backend/database/bizmanager.db
cd backend && npm run setup# Linux / macOS
sqlite3 backend/database/bizmanager.db
# Windows (if sqlite3 is installed)
sqlite3 backend\database\bizmanager.dbThere is no migration framework included. For schema changes:
- Add the DDL to
schema.sqlusingALTER TABLEor a newCREATE TABLE IF NOT EXISTS. - If adding columns to existing tables, run the
ALTER TABLEstatement manually on deployed databases before deploying new code.
All API routes are prefixed with /api.
Authenticated routes require the Authorization: Bearer <accessToken> header.
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request (validation error or business rule) |
| 401 | Unauthenticated (missing or expired token) |
| 403 | Forbidden (insufficient role) |
| 404 | Resource not found |
| 409 | Conflict (duplicate unique field) |
| 500 | Internal Server Error |
Base path: /api/auth
Authenticate a user and obtain JWT tokens.
Request body:
{
"email": "admin@bizmanager.local",
"password": "admin123"
}Response 200:
{
"accessToken": "<jwt>",
"refreshToken": "<jwt>",
"user": {
"id": "...",
"name": "Admin",
"email": "admin@bizmanager.local",
"role": "admin",
"avatar_url": null
}
}Create a new user account. No authentication required.
Request body:
{
"name": "Jane Smith",
"email": "jane@example.com",
"password": "securepassword",
"role": "manager"
}role is optional; defaults to "employee". Accepted values: admin, manager, employee.
Response 201: Same shape as /login.
Exchange a valid refresh token for a new access/refresh token pair.
The old refresh token is deleted (rotation).
Request body:
{ "refreshToken": "<jwt>" }Response 200:
{
"accessToken": "<new-jwt>",
"refreshToken": "<new-jwt>"
}Invalidate a refresh token.
Request body:
{ "refreshToken": "<jwt>" }Response 200: { "message": "Logged out" }
Return the currently authenticated user's profile.
Response 200:
{
"id": "...",
"name": "Admin",
"email": "admin@bizmanager.local",
"role": "admin",
"is_active": 1,
"avatar_url": null
}Returns statistics and chart data for the home screen.
Response 200:
{
"stats": {
"today_revenue": 1234.50,
"month_revenue": 45000.00,
"month_expenses": 12000.00,
"net_profit": 33000.00,
"total_customers": 120,
"total_products": 45,
"low_stock_count": 3
},
"low_stock_items": [ { "id": "...", "name": "...", "sku": "...", "stock": 2, "low_stock_at": 10 } ],
"recent_sales": [ { "id": "...", "invoice_number": "INV-00001", "customer_name": "...", "total": 99.00, "status": "paid", "created_at": "..." } ],
"charts": {
"daily_sales": [ { "date": "2025-06-01", "revenue": 500.00 } ],
"monthly_sales": [ { "month": "06", "revenue": 45000.00 } ]
}
}Base path: /api/customers — all routes require authentication.
List customers with optional search and pagination.
Query params:
| Param | Type | Default | Description |
|---|---|---|---|
search |
string | "" |
Filter by name, email, or phone |
page |
integer | 1 |
Page number |
limit |
integer | 20 |
Records per page |
Response 200:
{
"customers": [ { "id": "...", "name": "...", "email": "...", "phone": "...", "address": "...", "total_spent": 500.00, "created_at": "..." } ],
"total": 42
}Get a single customer with their 10 most recent sales.
Response 200:
{
"id": "...",
"name": "Alice Jones",
"email": "alice@example.com",
"phone": "555-1234",
"address": "123 Main St",
"notes": null,
"total_spent": 2300.00,
"recent_sales": [ { "id": "...", "invoice_number": "INV-00010", ... } ]
}Create a new customer.
Request body:
{
"name": "Bob Martin",
"email": "bob@example.com",
"phone": "555-0000",
"address": "456 Oak Ave",
"notes": "VIP customer"
}Only name is required. Response 201: the created customer object.
Update a customer. Same body as POST. Response 200: updated customer object.
Delete a customer. Response 200: { "message": "Customer deleted" }
Base path: /api/products — all routes require authentication.
List products.
Query params:
| Param | Type | Default | Description |
|---|---|---|---|
search |
string | "" |
Filter by name or SKU |
page |
integer | 1 |
Page number |
limit |
integer | 20 |
Records per page |
category_id |
string | — | Filter by category UUID |
low_stock |
"true" |
— | Show only low-stock items |
Response 200:
{
"products": [ { "id": "...", "name": "...", "sku": "...", "price": 9.99, "cost": 4.00, "stock": 50, "low_stock_at": 10, "category_name": "General", "is_active": 1 } ],
"total": 45,
"categories": [ { "id": "...", "name": "General" } ]
}Returns all active products whose stock <= low_stock_at. Response 200: { "products": [...] }
Get a single product. Response 200: product object with category_name.
Create a product.
{
"name": "Widget A",
"sku": "WGT-001",
"description": "A useful widget",
"price": 19.99,
"cost": 8.00,
"stock": 100,
"low_stock_at": 15,
"category_id": "<uuid>",
"is_active": 1
}name and price are required. Response 201: created product.
Update a product. Same body as POST. Response 200: updated product.
Delete a product. Response 200: { "message": "Product deleted" }
Create a category.
{ "name": "Electronics" }Response 201: { "id": "...", "name": "Electronics" }
Base path: /api/sales — all routes require authentication.
List sales.
Query params:
| Param | Type | Default | Description |
|---|---|---|---|
search |
string | "" |
Filter by invoice number or customer name |
page |
integer | 1 |
Page number |
limit |
integer | 20 |
Records per page |
status |
string | — | paid / pending / cancelled |
from |
date | — | YYYY-MM-DD — start date |
to |
date | — | YYYY-MM-DD — end date |
Response 200:
{
"sales": [ { "id": "...", "invoice_number": "INV-00001", "customer_name": "Alice", "total": 150.00, "status": "paid", "created_at": "..." } ],
"total": 200
}Get a single sale with its line items, customer info, and current settings (for invoice rendering).
Response 200:
{
"id": "...",
"invoice_number": "INV-00001",
"customer_name": "Alice",
"customer_email": "alice@example.com",
"customer_phone": "555-1234",
"subtotal": 100.00,
"tax_amount": 10.00,
"discount": 5.00,
"total": 105.00,
"status": "paid",
"notes": null,
"created_at": "...",
"items": [
{ "id": "...", "product_id": "...", "name": "Widget A", "price": 50.00, "cost": 20.00, "quantity": 2, "subtotal": 100.00 }
],
"settings": { "business_name": "My Business", "currency_symbol": "$", "tax_rate": "10", ... }
}Create a sale. Stock is decremented and customer total_spent is updated within a transaction.
Request body:
{
"customer_id": "<uuid or null>",
"items": [
{ "product_id": "<uuid>", "quantity": 2 }
],
"discount": 5.00,
"notes": "Rush order"
}items is required and must not be empty. Price is taken from the product record at the time of sale.
Response 201: the created sale (header only; use GET /:id for items).
Error 400: returned if any product has insufficient stock.
Update sale status.
{ "status": "cancelled" }Response 200: updated sale object.
Delete a sale. Stock is restored for each line item. Customer total_spent is decremented if the sale was paid.
Response 200: { "message": "Sale deleted" }
Base path: /api/expenses — requires admin or manager role.
List expenses.
Query params: search, page, limit, category, from, to
Response 200:
{
"expenses": [ { "id": "...", "title": "Electricity", "amount": 120.00, "category": "Utilities", "date": "2025-06-01", "notes": null } ],
"total": 30,
"totalAmount": 4500.00,
"categories": ["General", "Salaries", "Utilities"]
}Create an expense.
{
"title": "Office supplies",
"amount": 45.00,
"category": "General",
"date": "2025-06-05",
"notes": "Pens, paper"
}title and amount are required. Response 201: created expense.
Update an expense. Same body as POST. Response 200: updated expense.
Delete an expense. Response 200: { "message": "Expense deleted" }
Base path: /api/employees — requires admin or manager role.
List employees with search and pagination. Response 200: { "employees": [...], "total": N }
Create an employee record and optionally a linked user account.
{
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "555-9999",
"role": "employee",
"department": "Sales",
"salary": 3500.00,
"hire_date": "2025-01-15",
"notes": "",
"create_user": true,
"password": "initialPassword1"
}If create_user is true and the email doesn't already have a login account, a new users row is created with the given password (hashed with bcrypt, cost 12).
Response 201: created employee object.
Update an employee. Does not update the linked user account password.
{
"name": "Jane Doe",
"role": "manager",
"department": "Sales",
"salary": 4000.00,
"is_active": 1
}Response 200: updated employee object.
Delete an employee record. The linked user account is not deleted. Response 200: { "message": "Employee deleted" }
Base path: /api/reports — requires admin or manager role.
Financial summary for a date range.
Query params: from (date), to (date). Defaults to current month.
Response 200:
{
"revenue": 45000.00,
"expenses": 12000.00,
"cogs": 18000.00,
"gross_profit": 27000.00,
"net_profit": 33000.00,
"today_revenue": 1200.00,
"total_customers": 120,
"total_products": 45,
"low_stock_count": 3
}Query params: days (integer, default 30)
Response 200: { "data": [ { "date": "2025-06-01", "revenue": 800.00, "count": 5 } ] }
Query params: year (e.g. "2025", defaults to current year)
Response 200: { "data": [ { "month": "06", "revenue": 45000.00, "count": 120 } ] }
Query params: from, to, limit (default 10)
Response 200: { "data": [ { "product_id": "...", "name": "Widget A", "quantity": 50, "revenue": 999.50, "cost": 400.00 } ] }
Response 200: { "data": [ { "id": "...", "name": "Alice", "email": "...", "total_spent": 5000.00 } ] }
Query params: from, to. Defaults to current month.
Response 200: { "data": [ { "category": "Utilities", "amount": 600.00, "count": 3 } ] }
Base path: /api/settings
Returns all settings as a flat JSON object:
{
"business_name": "My Business",
"address": "",
"phone": "",
"email": "",
"currency": "USD",
"currency_symbol": "$",
"tax_rate": "10",
"tax_name": "Tax",
"invoice_prefix": "INV-",
"low_stock_threshold": "10"
}Update one or more settings. Sends any subset of the keys above.
{
"business_name": "Acme Corp",
"tax_rate": "15",
"currency_symbol": "£"
}Uses INSERT OR REPLACE (upsert) so new keys can also be added.
Response 200: full updated settings object.
Base path: /api/backup — requires admin role.
Streams a .db file backup using better-sqlite3's built-in backup() API (online backup; no server restart needed).
Response 200: application/octet-stream binary file download.
Lists backup files currently present in the database directory.
Response 200:
{
"backups": [
{ "name": "backup-2025-06-01T12-00-00-000Z.db", "size": 204800, "created_at": "2025-06-01T12:00:00.000Z" }
]
}No authentication required. Returns server status.
Response 200: { "status": "ok", "timestamp": "2025-06-01T12:00:00.000Z" }
Client Server
│ │
│── POST /api/auth/login ─────────────────►│
│◄─ { accessToken, refreshToken, user } ───│
│ │
│── GET /api/... + Bearer <accessToken> ──►│ (every request)
│◄─ 200 data ──────────────────────────────│
│ │
│ (access token expires after 15 minutes) │
│ │
│── GET /api/... + Bearer <expired> ──────►│
│◄─ 401 { code: "TOKEN_EXPIRED" } ─────────│
│ │
│── POST /api/auth/refresh ────────────────►│
│ { refreshToken: <7-day token> } │
│◄─ { accessToken, refreshToken } ──────────│ (old refresh token deleted)
│ │
│── Retry original request ───────────────►│
│◄─ 200 data ──────────────────────────────│
│ │
│── POST /api/auth/logout ────────────────►│
│ { refreshToken } │
│◄─ 200 { message: "Logged out" } ──────────│ (refresh token deleted)
The axios.js interceptor in the frontend handles the refresh flow automatically. Concurrent requests that fail with TOKEN_EXPIRED are queued and replayed after the new token is obtained.
The authorize(...roles) middleware factory is applied in route files:
// Only admins can delete a product
router.delete('/:id', authenticate, authorize('admin'), ctrl.remove);
// Admins and managers can create/edit
router.post('/', authenticate, authorize('admin', 'manager'), ctrl.create);
// All authenticated users can read
router.get('/', authenticate, ctrl.getAll);authenticate populates req.user from the JWT. authorize then checks req.user.role.
To add a new role:
- Update the
CHECKconstraint inschema.sql:CHECK(role IN ('admin','manager','employee','supervisor')). - Run the
ALTER TABLEstatement on deployed databases. - Add the role string to the relevant
authorize()calls.
Two stores:
| Store | State |
|---|---|
authStore |
user, isAuthenticated, login(), logout(), checkAuth(), hasRole() |
appStore |
darkMode, sidebarOpen, notifications, settings, confirm modal |
authStore is persisted to localStorage via zustand/middleware/persist.
Tokens are stored in localStorage:
access_token— short-lived JWTrefresh_token— long-lived JWT
src/api/axios.js creates an axios instance with:
baseURL: '/api'(resolved tohttp://localhost:5000/apivia Vite proxy)timeout: 30000- Request interceptor — attaches
Authorization: Bearer <token>header - Response interceptor — handles
TOKEN_EXPIREDby calling/api/auth/refreshand retrying
React Router v6. All authenticated routes are wrapped in <Layout> which renders <Sidebar> + <Header> + <Outlet>.
Tailwind's darkMode: 'class' strategy. The toggleDarkMode() action in appStore adds/removes the dark class on document.documentElement.
appStore.addNotification({ type, message }) adds a toast that disappears after 5 seconds.
<NotificationContainer> renders the stack of toasts.
appStore.showConfirm({ title, message }) returns a Promise that resolves true/false.
<ConfirmModal> renders the dialog.
Here is a step-by-step walkthrough to add a hypothetical Suppliers module.
Add to backend/database/schema.sql:
CREATE TABLE IF NOT EXISTS suppliers (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
name TEXT NOT NULL,
email TEXT,
phone TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);Run npm run setup (or apply the ALTER TABLE manually on an existing database).
Create backend/controllers/supplierController.js:
const db = require('../database/db');
const getAll = (req, res, next) => {
try {
const { search = '', page = 1, limit = 20 } = req.query;
const offset = (parseInt(page) - 1) * parseInt(limit);
const like = `%${search}%`;
const suppliers = db.prepare(
`SELECT * FROM suppliers WHERE name LIKE ? OR email LIKE ? ORDER BY name ASC LIMIT ? OFFSET ?`
).all(like, like, parseInt(limit), offset);
const { total } = db.prepare(
`SELECT COUNT(*) as total FROM suppliers WHERE name LIKE ? OR email LIKE ?`
).get(like, like);
res.json({ suppliers, total });
} catch (err) {
next(err);
}
};
// ... create, update, remove (follow same pattern as customerController.js)
module.exports = { getAll };Create backend/routes/suppliers.js:
const router = require('express').Router();
const { authenticate, authorize } = require('../middleware/auth');
const ctrl = require('../controllers/supplierController');
router.use(authenticate);
router.get('/', ctrl.getAll);
router.post('/', authorize('admin', 'manager'), ctrl.create);
router.put('/:id', authorize('admin', 'manager'), ctrl.update);
router.delete('/:id', authorize('admin'), ctrl.remove);
module.exports = router;app.use('/api/suppliers', apiLimiter, require('./routes/suppliers'));Create frontend/src/pages/Suppliers.jsx following the pattern of Customers.jsx.
In frontend/src/components/Sidebar.jsx, add a new nav item.
In frontend/src/App.jsx, add:
import Suppliers from './pages/Suppliers';
// ...
<Route path="/suppliers" element={<Suppliers />} />cd backend
npm install --omit=dev # install production dependencies only
npm start # node server.jsUse a process manager to keep the server alive:
# pm2 (recommended)
npm install -g pm2
pm2 start server.js --name bizmanager-api
pm2 save
pm2 startup # auto-start on rebootcd frontend
npm install
npm run build # outputs static files to frontend/dist/Serve frontend/dist/ with a static file server (nginx, Caddy, IIS, etc.).
nginx example (serves frontend + proxies API):
server {
listen 80;
server_name yourdomain.com;
root /var/www/bizmanager/frontend/dist;
index index.html;
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API requests to the backend
location /api {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Update backend/.env:
NODE_ENV=production
FRONTEND_URL=https://yourdomain.com
- Set strong unique values for
JWT_SECRETandJWT_REFRESH_SECRET - Set
NODE_ENV=production - Set
FRONTEND_URLto your actual domain - Serve over HTTPS (use Let's Encrypt / Certbot)
- Restrict SQLite file permissions:
chmod 600 backend/database/bizmanager.db - Keep
backend/.envout of version control (already in.gitignore) - Set up regular automated backups (see next section)
# Edit crontab
crontab -e
# Run backup every day at 02:00
0 2 * * * curl -s -H "Authorization: Bearer <token>" \
http://localhost:5000/api/backup/download \
-o /backups/bizmanager-$(date +\%Y-\%m-\%d).dbReplace <token> with a long-lived admin token (or generate one programmatically).
- Open Task Scheduler → Create Basic Task.
- Set trigger to Daily.
- Action: Start a program →
powershell.exe - Arguments:
-Command "Invoke-WebRequest -Headers @{Authorization='Bearer <token>'} -OutFile 'C:\Backups\bizmanager-$(Get-Date -Format yyyy-MM-dd).db' http://localhost:5000/api/backup/download"
If the application is not running (e.g. during a maintenance window), simply copy the SQLite file:
# Linux
cp backend/database/bizmanager.db /backups/bizmanager-$(date +%Y-%m-%d).db
# Windows CMD
copy backend\database\bizmanager.db C:\Backups\bizmanager-%DATE%.db- Backend: CommonJS modules (
require/module.exports), no TypeScript, Express conventions. - Frontend: ES modules, functional React components, hooks only (no class components), Tailwind utility classes.
- Follow the existing file and naming conventions in each layer.
main ← production-ready code
feature/<name>
fix/<name>
- Backend: does the new route follow the authenticate → authorize → controller pattern?
- Backend: are user inputs validated (express-validator or manual checks)?
- Frontend: does the page handle loading and error states?
- No secrets or database files committed.
- README / docs updated if user-facing behaviour changed.
# Start backend
cd backend && npm run dev
# In another terminal — hit the health endpoint
curl http://localhost:5000/api/health
# Expected: {"status":"ok","timestamp":"..."}
# Login
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@bizmanager.local","password":"admin123"}'For end-user documentation on how to use the application day-to-day, see the User Guide.