# 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 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 ```