293 lines
8.3 KiB
Markdown
293 lines
8.3 KiB
Markdown
# Salon POS — Multi-Tenant SaaS
|
|
|
|
## Overview
|
|
|
|
A multi-tenant SaaS POS web application for nail salons, served across two dedicated domains:
|
|
|
|
| Domain | Audience | Purpose |
|
|
|---|---|---|
|
|
| `admin.mydomain.com` | System Admins | Platform management |
|
|
| `mydomain.com` | Tenant users | Salon management |
|
|
|
|
---
|
|
|
|
## Tech Stack
|
|
|
|
- **Backend:** Python 3.11+ / Flask
|
|
- **Database:** MySQL 8.0+ with SQLAlchemy ORM
|
|
- **Auth:** Flask-Login (UI) + Flask-JWT-Extended (API)
|
|
- **Frontend:** Jinja2 + Bootstrap 5 + Vanilla JS
|
|
- **WSGI:** Gunicorn (2 independent processes)
|
|
- **Reverse Proxy:** Nginx
|
|
- **Scheduler:** APScheduler
|
|
- **Process Manager:** systemd
|
|
|
|
---
|
|
|
|
## Fresh Install (Production)
|
|
|
|
### 1. System dependencies
|
|
|
|
```bash
|
|
sudo apt update && sudo apt install -y python3.11 python3.11-venv python3-pip \
|
|
mysql-server nginx libmysqlclient-dev build-essential libssl-dev
|
|
```
|
|
|
|
### 2. Application user
|
|
|
|
```bash
|
|
sudo useradd -m -s /bin/bash salonpos
|
|
sudo mkdir -p /opt/salon_pos
|
|
sudo chown salonpos:salonpos /opt/salon_pos
|
|
```
|
|
|
|
### 3. Clone and set up virtual environment
|
|
|
|
```bash
|
|
cd /opt/salon_pos
|
|
python3.11 -m venv venv
|
|
source venv/bin/activate
|
|
pip install -r requirements.txt
|
|
```
|
|
|
|
### 4. Environment configuration
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
chmod 600 .env
|
|
# Edit .env with your values — SECRET_KEY, ADMIN_SECRET_KEY, DATABASE_URL, MAIL_*, etc.
|
|
nano .env
|
|
```
|
|
|
|
### 5. MySQL setup
|
|
|
|
```sql
|
|
CREATE DATABASE salon_pos CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
CREATE USER 'salon_pos_app'@'localhost' IDENTIFIED BY 'strong-password-here';
|
|
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP
|
|
ON salon_pos.* TO 'salon_pos_app'@'localhost';
|
|
|
|
-- Backup user (read-only)
|
|
CREATE USER 'salon_pos_backup'@'localhost' IDENTIFIED BY 'backup-password-here';
|
|
GRANT SELECT, LOCK TABLES ON salon_pos.* TO 'salon_pos_backup'@'localhost';
|
|
FLUSH PRIVILEGES;
|
|
```
|
|
|
|
Store backup credentials in `/etc/mysql/backup.cnf` (mode 600):
|
|
```ini
|
|
[client]
|
|
user=salon_pos_backup
|
|
password=backup-password-here
|
|
host=localhost
|
|
```
|
|
|
|
### 6. Database migrations
|
|
|
|
```bash
|
|
cd /opt/salon_pos
|
|
source venv/bin/activate
|
|
flask --app wsgi_tenant:app db init # First time only
|
|
flask --app wsgi_tenant:app db migrate -m "Phase 1 — initial schema"
|
|
flask --app wsgi_tenant:app db upgrade
|
|
```
|
|
|
|
### 7. Seed initial data (plans + superadmin)
|
|
|
|
```bash
|
|
flask --app wsgi_admin:app shell
|
|
```
|
|
|
|
```python
|
|
from app.extensions import db, bcrypt
|
|
from app.models.platform import Plan, SystemUser
|
|
|
|
# Create subscription plans
|
|
plans = [
|
|
Plan(name="Starter", price_monthly=29.00, max_staff=3, max_locations=1,
|
|
features_json={"pos": True, "appointments": True, "customers": True,
|
|
"services": True, "promotions": True,
|
|
"appointment_reminders": True, "customer_reviews": True,
|
|
"reconciliation": True, "basic_reports": True,
|
|
"inventory": False, "commission": False,
|
|
"full_reports": False, "multi_location": False,
|
|
"online_booking": False, "waitlist": False,
|
|
"marketing": False}),
|
|
Plan(name="Growth", price_monthly=59.00, max_staff=10, max_locations=3,
|
|
features_json={"pos": True, "appointments": True, "customers": True,
|
|
"services": True, "promotions": True,
|
|
"appointment_reminders": True, "customer_reviews": True,
|
|
"reconciliation": True, "basic_reports": True,
|
|
"inventory": True, "commission": True,
|
|
"full_reports": True, "multi_location": True,
|
|
"online_booking": True, "waitlist": True,
|
|
"marketing": False}),
|
|
Plan(name="Pro", price_monthly=99.00, max_staff=None, max_locations=None,
|
|
features_json={"pos": True, "appointments": True, "customers": True,
|
|
"services": True, "promotions": True,
|
|
"appointment_reminders": True, "customer_reviews": True,
|
|
"reconciliation": True, "basic_reports": True,
|
|
"inventory": True, "commission": True,
|
|
"full_reports": True, "multi_location": True,
|
|
"online_booking": True, "waitlist": True,
|
|
"marketing": True}),
|
|
]
|
|
db.session.add_all(plans)
|
|
|
|
# Create first superadmin
|
|
admin = SystemUser(
|
|
email="admin@yourdomain.com",
|
|
password_hash=bcrypt.generate_password_hash("ChangeMe1!").decode("utf-8"),
|
|
name="System Admin",
|
|
role="superadmin",
|
|
is_active=True,
|
|
)
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
print("Seeded successfully.")
|
|
exit()
|
|
```
|
|
|
|
### 8. Log directories
|
|
|
|
```bash
|
|
sudo mkdir -p /var/log/salon_pos_admin /var/log/salon_pos_tenant
|
|
sudo chown salonpos:salonpos /var/log/salon_pos_admin /var/log/salon_pos_tenant
|
|
```
|
|
|
|
### 9. systemd services
|
|
|
|
```bash
|
|
sudo cp deploy/salon_pos_admin.service /etc/systemd/system/
|
|
sudo cp deploy/salon_pos_tenant.service /etc/systemd/system/
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable salon_pos_admin salon_pos_tenant
|
|
sudo systemctl start salon_pos_admin salon_pos_tenant
|
|
sudo systemctl status salon_pos_admin salon_pos_tenant
|
|
```
|
|
|
|
### 10. Nginx
|
|
|
|
```bash
|
|
sudo cp deploy/nginx.conf /etc/nginx/sites-available/salon_pos
|
|
sudo ln -s /etc/nginx/sites-available/salon_pos /etc/nginx/sites-enabled/
|
|
# Update IP allowlist in nginx.conf before enabling
|
|
sudo nginx -t
|
|
sudo systemctl reload nginx
|
|
```
|
|
|
|
### 11. Backup cron
|
|
|
|
```bash
|
|
sudo cp deploy/backup/db_backup.sh /opt/salon_pos/deploy/backup/
|
|
sudo chmod +x /opt/salon_pos/deploy/backup/db_backup.sh
|
|
sudo mkdir -p /var/backups/salon_pos
|
|
sudo chown salonpos:salonpos /var/backups/salon_pos
|
|
# Install cron job for salonpos user
|
|
sudo crontab -u salonpos deploy/backup/backup.cron
|
|
```
|
|
|
|
---
|
|
|
|
## Service Management
|
|
|
|
```bash
|
|
# Status
|
|
sudo systemctl status salon_pos_admin
|
|
sudo systemctl status salon_pos_tenant
|
|
|
|
# Restart
|
|
sudo systemctl restart salon_pos_admin
|
|
sudo systemctl restart salon_pos_tenant
|
|
|
|
# View logs
|
|
sudo journalctl -u salon_pos_admin -f
|
|
sudo journalctl -u salon_pos_tenant -f
|
|
tail -f /var/log/salon_pos_admin/error.log
|
|
tail -f /var/log/salon_pos_tenant/error.log
|
|
```
|
|
|
|
---
|
|
|
|
## Database Migrations (Ongoing)
|
|
|
|
```bash
|
|
cd /opt/salon_pos
|
|
source venv/bin/activate
|
|
flask --app wsgi_tenant:app db migrate -m "Description of change"
|
|
flask --app wsgi_tenant:app db upgrade
|
|
|
|
# Rollback one step
|
|
flask --app wsgi_tenant:app db downgrade
|
|
```
|
|
|
|
---
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
cd /opt/salon_pos
|
|
source venv/bin/activate
|
|
pip install pytest
|
|
FLASK_ENV=testing pytest tests/ -v
|
|
```
|
|
|
|
---
|
|
|
|
## Monthly Restore Drill (Mandatory)
|
|
|
|
```bash
|
|
# On staging server:
|
|
LATEST=$(ls -t /var/backups/salon_pos/*.sql.gz | head -1)
|
|
gunzip -c "$LATEST" | mysql -u root salon_pos_staging
|
|
echo "Restore drill completed: $LATEST"
|
|
# Document the result and date in this runbook.
|
|
```
|
|
|
|
**Last restore drill:** _______________
|
|
**Performed by:** _______________
|
|
**Result:** _______________
|
|
|
|
---
|
|
|
|
## Development Setup
|
|
|
|
```bash
|
|
git clone <repo>
|
|
cd salon_pos
|
|
python3.11 -m venv venv
|
|
source venv/bin/activate
|
|
pip install -r requirements.txt
|
|
cp .env.example .env
|
|
# Set FLASK_ENV=development in .env
|
|
flask --app wsgi_tenant:app db upgrade
|
|
flask --app wsgi_tenant:app run --port 5001
|
|
flask --app wsgi_admin:app run --port 5002
|
|
```
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
salon_pos/
|
|
├── app/
|
|
│ ├── __init__.py
|
|
│ ├── extensions.py # Shared Flask extensions
|
|
│ ├── context.py # load_tenant_context, load_location_context
|
|
│ ├── decorators.py # @require_role, @tenant_feature_required, @demo_readonly
|
|
│ ├── security.py # Security headers + input sanitisers
|
|
│ ├── forms.py # Password validation helper
|
|
│ ├── models/
|
|
│ │ ├── platform.py # SystemUser, Tenant, Plan, AuditLog, ...
|
|
│ │ └── salon.py # All tenant-scoped models
|
|
│ ├── admin/ # admin.mydomain.com blueprints
|
|
│ └── tenant/ # mydomain.com blueprints
|
|
├── config.py # Dev / Prod / Test config classes
|
|
├── wsgi_admin.py # Gunicorn entrypoint — admin
|
|
├── wsgi_tenant.py # Gunicorn entrypoint — tenant
|
|
├── requirements.txt
|
|
├── .env.example
|
|
├── deploy/ # systemd units, Nginx config, backup scripts
|
|
└── tests/ # pytest test suites
|
|
```
|