Back to Portfolio

Ski Vesotel

Multi-tenant SaaS platform for comprehensive ski school management. It allows teachers to register their work shifts (private lessons, courses) and administration to manage payroll, dynamic rates, and users with granular role control.

System Architecture

The system follows a containerized microservices architecture with 4 main services orchestrated via Docker Compose:

Frontend

  • Framework: Next.js 14 (App Router)
  • Port: 3000 → 3010
  • Build: GHCR Image

Backend API

  • Framework: FastAPI + SQLAlchemy
  • Database: PostgreSQL 16
  • Auth: JWT RS256 + 2FA

Redis Cache

In-memory cache and session management. Configured with LRU (Least Recently Used) eviction policy and 256MB limit for rate limiting and temporary states.

Architecture Diagram and Data Flow
Architecture Diagram and Data Flow

Advanced SaaS Features

The system implements advanced multi-tenant SaaS patterns that allow multiple companies to operate independently on the same instance.

Data Structure (PostgreSQL 16)

Relational database managed via SQLAlchemy ORM and Alembic for schema version control. Makes extensive use of JSONB columns for dynamic configurations, allowing each company to customize rates, shift types, and features without modifying the schema.

Enumerated Types

userrole: userrole — Global user role in the system: admin (super administrator) or user (standard user).
companyrole: companyrole — User role within a specific company: admin (administrator), manager (supervisor/boss) or worker (employee).

Schema Tables

users

Central registered users table. Stores credentials, global role, 2FA status and default company. Each user has a UUID as primary key.

Schema Columns
ColumnConstraints / Type
idPK, uuid
emailunique
hashed_password
first_name
last_name
roleuserrole enum
is_active
default_company_idFK → companies
must_change_password
is_2fa_enabled
otp_secret
created_at
updated_at

CI/CD Pipeline

Deployment is fully automated via GitHub Actions. Each push to the main branch triggers a 2-stage pipeline:

1. Build & Push (GitHub-hosted runner)

Docker images for frontend and backend are built and pushed to GitHub Container Registry (GHCR) as private images.

2. Deploy (Self-hosted runner)

A self-hosted runner on my home server (192.168.x.x) pulls the new images from GHCR, runs database migrations with Alembic, and brings up the updated containers.

3. Automatic Migrations

Before starting services, 'alembic upgrade head' is run in a temporary container to safely apply database schema changes.

4. Cleanup

After deployment, 'docker image prune' is executed to remove obsolete images and free disk space on the production server.

Images are hosted on GitHub Container Registry (GHCR), which allows storing private Docker images linked to the repository, eliminating the need for a self-hosted registry.

Security and Deployment

Public access follows a Defense in Depth scheme with 3 layers, designed to expose the service securely.

1

Layer 1: Reverse Proxy

Nginx Proxy Manager with Let's Encrypt SSL certificates. Terminates the secure connection and redirects to the internal service.

2

Layer 2: Isolated Network

Services operate in a dedicated MikroTik subnet, isolated from the home network. Traffic between VMs is managed through firewall rules.

3

Layer 3: Docker Networks

Isolated Docker bridge networks (app and db). Containers communicate with each other without exposing unnecessary ports. Sensitive variables injected via GitHub Secrets.

Note: This is a real production project used by a ski school. Complete documentation includes sensitive configurations that have been omitted for security.

🇬🇧EN🇪🇸ES