dde18a2cd2c2a09e47b61183619e28a9ddd8881a
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
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
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
cd /opt/salon_pos
python3.11 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
4. Environment configuration
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
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):
[client]
user=salon_pos_backup
password=backup-password-here
host=localhost
6. Database migrations
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)
flask --app wsgi_admin:app shell
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
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
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
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
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
# 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)
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
cd /opt/salon_pos
source venv/bin/activate
pip install pytest
FLASK_ENV=testing pytest tests/ -v
Monthly Restore Drill (Mandatory)
# 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
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
Languages
HTML
51.2%
Python
48.6%