PostgreSQL 16+. All primary keys are UUID. All timestamps are stored in UTC.
Users & Auth
users
| Column | Type | Description |
|---|
id | UUID (PK) | |
email | string | Unique |
phone | string | Unique |
role | enum | client · professional · admin |
encrypted_password | string | bcrypt |
confirmed_at | timestamp | Email confirmation |
discarded_at | timestamp | Soft delete |
Salon owner is determined via salons.owner_id, not through role.
profiles
| Column | Type |
|---|
user_id | UUID (FK → users) |
first_name | string |
last_name | string |
avatar_url | string |
bio | text |
locale | string |
oauth_identities
| Column | Type | Description |
|---|
id | UUID (PK) | |
user_id | UUID (FK) | → users.id |
provider | string | "google" and others in the future |
uid | string | Unique ID at the provider (sub) |
created_at | timestamp | |
updated_at | timestamp | |
Unique index: provider + uid.
Salons & Services
salons
| Column | Type | Description |
|---|
id | UUID (PK) | |
owner_id | UUID (FK) | → users.id |
name | string | |
slug | string | URL-friendly, unique |
address | string | |
lat, lng | decimal | Coordinates |
phone | string | |
status | enum | pending · active · suspended |
photo_url | string | |
reviews_count | integer | Counter cache |
rating | decimal | Average rating |
currency | string | ISO 4217, default MDL |
timezone | string | IANA, default Europe/Chisinau |
cancellation_hours_before | integer | Default 24 |
cancellation_fee_percent | decimal | Default 0 |
auto_cancel_after_min | integer | nullable |
salon_services (AR model: Service)
| Column | Type |
|---|
id | UUID (PK) |
salon_id | UUID (FK) |
name | string |
description | text |
duration_min | integer |
category_id | integer (FK → categories) |
Unique index: salon_id + name.
service_master_prices (AR model: ServiceMasterPrice)
| Column | Type |
|---|
id | UUID (PK) |
salon_id | UUID (FK) |
service_id | UUID (FK → salon_services) |
master_id | UUID (FK → users) |
price | decimal |
Unique index: service_id + master_id.
categories
| Column | Type |
|---|
id | integer |
name | string |
slug | string |
parent_id | integer (self-join, nullable) |
position | integer |
Memberships & Master Profiles
salon_memberships
| Column | Type | Description |
|---|
id | UUID (PK) | |
salon_id | UUID (FK) | |
user_id | UUID (FK) | |
role | enum | master · receptionist |
status | enum | pending · active · deactivated |
invite_token | string | For accepting invitations |
invited_at | timestamp | |
accepted_at | timestamp | nullable |
Unique index: salon_id + user_id.
master_profiles
| Column | Type |
|---|
id | UUID (PK) |
user_id | UUID (FK) |
specialization | string |
experience_years | integer |
instagram_url | string |
portfolio_url | string |
reviews_count | integer |
rating | decimal |
Scheduling & Bookings
working_hours
| Column | Type | Description |
|---|
salon_id | UUID (FK) | |
membership_id | UUID (FK → salon_memberships, nullable) | null = entire salon |
day_of_week | integer | 0=Sun, 1=Mon ... 6=Sat |
start_time | time | |
end_time | time | |
appointments
| Column | Type | Description |
|---|
id | UUID (PK) | |
client_id | UUID (FK) | → users.id |
salon_id | UUID (FK) | |
master_id | UUID (FK) | → users.id |
service_id | UUID (FK) | → salon_services.id |
starts_at | timestamp | |
ends_at | timestamp | |
status | string | AASM: confirmed/in_progress/completed/reviewed/cancelled/no_show |
price_snapshot | decimal | Price at the time of booking (frozen) |
duration_min_snapshot | integer | Duration at the time of booking |
total_amount | decimal | |
Reviews & Favorites
reviews
| Column | Type |
|---|
id | UUID (PK) |
appointment_id | UUID (FK) |
client_id | UUID (FK) |
salon_id | UUID (FK) |
master_id | UUID (FK) |
rating | integer |
body | text |
favorites
| Column | Type |
|---|
id | UUID (PK) |
user_id | UUID (FK) |
salon_id | UUID (FK) |
Unique index: user_id + salon_id.