Database schema¶
The PerfShop MySQL 8 database is managed by Flyway. Migrations are located in backend/src/main/resources/db/migration-fr/ and are versioned from V1 to V10. The choice of the migration-fr folder instead of migration is deliberate: this is a schema consolidated in its final state, with no ALTER TABLE history. This approach makes the schema easier to read pedagogically (an instructor can understand it by reading a single file) and speeds up clean deployments.
Sources
backend/src/main/resources/db/migration-fr/V1__schema.sql to V10__data_pedagogique.sql
Overview¶
| Migration | Size | Role |
|---|---|---|
V1__schema.sql |
13 KB | Creation of every table in its final state |
V2__data_users.sql |
3 KB | Initial user accounts (demo agents, test accounts) |
V3__data_informatique.sql |
44 KB | Catalog seed — Computing category (~200 products) |
V4__data_avions.sql |
46 KB | Catalog seed — Aircraft category |
V5__data_voitures.sql |
46 KB | Catalog seed — Cars category |
V6__data_entreprises.sql |
65 KB | Catalog seed — Companies category (valuations) |
V7__data_helicopteres.sql |
29 KB | Catalog seed — Helicopters category |
V8__data_bateaux.sql |
8 KB | Catalog seed — Boats category |
V9__data_motos.sql |
7 KB | Catalog seed — Motorcycles category |
V10__data_pedagogique.sql |
7 KB | Pedagogical infrastructure: 4 products + 5 BAC1-BAC5 agents |
The full catalog totals roughly 1,000 products spread across seven heterogeneous categories. This variety is intentional: it allows pedagogical enigmas to exploit distinctive search criteria and load tests to simulate realistic volumes.
Entity-relationship diagram¶
erDiagram
users ||--o{ cart_items : "owns"
users ||--o{ orders : "places"
products ||--o{ cart_items : "referenced by"
products ||--o{ order_items: "referenced by"
orders ||--o{ order_items : "contains"
users {
BIGINT id PK
VARCHAR email UK
VARCHAR password "BCrypt $2b$10$"
VARCHAR first_name
VARCHAR last_name
VARCHAR civility "M | Mme | Mx"
DATE birth_date
VARCHAR phone
TEXT street
VARCHAR postal_code "VARCHAR(5) — Pedagogical Chaos N2+ target"
VARCHAR city
VARCHAR region
VARCHAR country "ISO2, default FR"
DATETIME created_at
DATETIME last_login
BOOLEAN is_pedagogique "agents hidden from UI"
}
products {
BIGINT id PK
VARCHAR name
VARCHAR description
DECIMAL price "DECIMAL(18,2)"
INT stock
VARCHAR category
VARCHAR image_url
DATETIME created_at
DATETIME updated_at
BOOLEAN is_pedagogique "products hidden from UI"
BOOLEAN pedagogique_protected "stock not decremented"
}
cart_items {
BIGINT id PK
BIGINT user_id FK
VARCHAR session_id "anonymous cart"
BIGINT product_id FK
INT quantity
DECIMAL price
DATETIME added_at
}
orders {
BIGINT id PK
VARCHAR order_number UK
BIGINT user_id FK
DECIMAL total_amount
ENUM status "PENDING..CANCELLED"
VARCHAR shipping_address
VARCHAR shipping_method
VARCHAR payment_method
VARCHAR card_last4 "never the CVV"
TINYINT cvv_verified
BOOLEAN is_test_data "agent orders excluded"
DATETIME created_at
DATETIME updated_at
}
order_items {
BIGINT id PK
BIGINT order_id FK
BIGINT product_id FK
INT quantity
DECIMAL unit_price
DECIMAL subtotal
}
admin_users {
BIGINT id PK
VARCHAR email UK
VARCHAR password_hash "BCrypt"
BOOLEAN can_access_chaos
BOOLEAN can_access_monitoring
BOOLEAN can_access_admin
BOOLEAN can_access_jmeter
BOOLEAN can_access_scripts
BOOLEAN is_superadmin
DATETIME created_at
}
perfshop_license {
VARCHAR id PK "always current"
TEXT license_key
VARCHAR plan "functional | performance | enterprise"
VARCHAR holder
DATE issued_at
DATE expires_at "NULL = unlimited"
DATETIME activated_at
TINYINT revoked
}
pedagogique_sessions {
VARCHAR token PK "UUID"
VARCHAR alias
VARCHAR level "bac1..bac5"
BIGINT joined_at "epoch ms"
BIGINT completed_at
INT current_step
VARCHAR agent_code
VARCHAR extraction_answer_hash
VARCHAR logique_question_indices
VARCHAR logique_expected_hash
TEXT attempts_json
TIMESTAMP created_at
}
The admin_users, perfshop_license, and pedagogique_sessions tables are intentionally detached from the main graph — they handle cross-cutting aspects (instructor authentication, license, pedagogical journey) that do not fit into the e-commerce model.
V1 — Complete schema¶
V1__schema.sql creates all tables in their final state. No ALTER TABLE is run afterwards. This consolidation is a deliberate rewrite of the historical schema (which had 38 incremental migrations) to offer students a clearer read.
Table users¶
Identifies the shop's customers. Contains classic fields (email, BCrypt password, last name, first name) enriched by Business Chaos extensions:
civility:VARCHAR(5)and notENUM— Hibernate validates the schema againstVARCHAR. The business constraint (M,Mme,Mx) is enforced on the application side byValidationService.birth_date: date of birth, used by Business Chaos for rules like the loyalty discount.- Decomposed address:
street,postal_code,city,region,country(instead of a singleaddressfield). This breakdown is required by several business chaos cases. postal_codelimited to 5 characters: intentional limit. Pedagogical Chaos Level 2+ intentionally tries to insert a longer value, which triggers a MySQLDataTruncationobservable in logs and metrics — a pedagogical demonstration of a "silent bug under chaos".country: ISO2, defaultFR.is_pedagogique: flag that identifies the "agent" accounts created by V10. These accounts are hidden in the admin UI and their orders are excluded from statistics by default.
Indexes: idx_email (unique), idx_country, idx_is_pedagogique.
Table products¶
E-commerce catalog. The pedagogical extensions:
price:DECIMAL(18,2)— extended precision for company valuations (billions). Standard catalog products remain at normal precision.is_pedagogique: products hidden in the admin UI — the 4 products created by V10 that students use to solve enigmas.pedagogique_protected: flag that preventsOrderServicefrom decrementing the stock on order creation. Used on products whose enigmas depend on the exact stock (e.g.Basic USB-C Cable 1mhasstock=60, enigma BAC1-2 computes8 × 60 = 480).
Indexes: idx_category, idx_name, idx_prod_pedagogique.
Table cart_items¶
Cart. Supports two cases:
- Authenticated cart —
user_idpopulated,session_idnull - Anonymous cart —
session_idpopulated,user_idnull
The FK on user_id allows NULL to support anonymous carts. On login, CartService migrates items from the anonymous cart to the user account.
Table orders¶
Order. The status is a real MySQL ENUM with six values: PENDING, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED.
Fields dedicated to payment traceability without ever storing sensitive data:
card_last4: last 4 digits of the card (user display)cvv_verified: boolean — has the CVV been verified? The real CVV is never stored
The is_test_data field marks orders generated by pedagogical agents. By default, they are excluded from stats displayed in the admin UI, to avoid polluting reports with training orders.
Table order_items¶
Order line. quantity, unit_price and subtotal capture the amount at the time of ordering (later catalog prices do not affect it).
Table admin_users¶
Instructor accounts. Five rights flags (can_access_chaos, can_access_monitoring, can_access_admin, can_access_jmeter, can_access_scripts) and an is_superadmin flag. The superadmin enjoys hard-coded protections in AdminUserService — see Admin authentication.
No INSERT in V1: superadmin bootstrap is handled by AdminUserService @EventListener(ApplicationReadyEvent) from the PERFSHOP_ADMIN_EMAIL and PERFSHOP_ADMIN_PASSWORD environment variables. This separation avoids having a BCrypt hash hard-coded in the migration SQL.
Table perfshop_license¶
Stores the active license. Constraint id = 'current': a single row at a time. Upserts from LicenseService.activateLicense() do an UPDATE first then an INSERT if the row does not exist. A CHECK constraint validates the plan. See License system.
Table pedagogique_sessions¶
Individual sessions of the pedagogical journey. Identified by a token UUID — no FK to users because sessions do not require a user account. Write-through architecture: the database is always the source of truth, and an optional memory cache can be enabled on the fly by the instructor (see Pedagogical journey concept and architecture).
Two logic columns (logique_question_indices, logique_expected_hash) store the logic questions drawn once at /join — this eliminates any front/back desynchronization.
The joined_at and completed_at timestamps are in epoch milliseconds (BIGINT), consistent with Instant.toEpochMilli() on the Java side.
attempts_json is a TEXT (without DEFAULT — MySQL 8 strict mode forbids it on TEXT) that stores a flat JSON { "bac1-0": 2, "bac1-1": 1, … } counting attempts per step.
V2 — User data¶
Creates demonstration accounts: a few test users with varied profiles (civility, date of birth, addresses in several countries) to feed functional test scenarios and business chaos cases.
V3 through V9 — Catalog seeds¶
Seven migrations, one per major catalog category:
| Migration | Category | Approximate volume |
|---|---|---|
| V3 | Computing | ~200 products (computers, peripherals, components, storage, network…) |
| V4 | Aircraft | ~150 products |
| V5 | Cars | ~150 products |
| V6 | Companies | ~230 products (with DECIMAL(18,2) valuations) |
| V7 | Helicopters | ~170 products |
| V8 | Boats | ~50 products |
| V9 | Motorcycles | ~50 products |
The total catalog sits around 1,000 products. This volume is calibrated to:
- Fit comfortably in JVM memory with a default heap
- Make a Performance Chaos visible (slow pagination, missing index)
- Offer enough diversity for the search criteria of pedagogical enigmas
Product images are served locally from the backend container (/images/...) to avoid any external dependency.
V10 — Pedagogical infrastructure¶
This migration is critical and contains untouchable elements tied to the enigmas. It combines four dimensions:
1. Four pedagogical products¶
These products have is_pedagogique = TRUE and pedagogique_protected = TRUE. They are invisible in the admin UI and their stocks are never decremented. Each product is carefully built to serve a specific enigma:
| Product | Price | Stock | Enigma |
|---|---|---|---|
| Basic USB-C Cable 1m | €22.00 | 60 | BAC1-1 (single price filter), BAC1-2 (stock × 8 bits) |
| CODE Pro USB-C Adapter | €64.00 | 9999 | BAC2-3 (CODE search), BAC2-4 (2⁶ = 64) |
| Lorem NVMe SSD 1TB PCIe 4.0 | €255.00 | 32 | BAC4-2 (hex ASCII header), BAC4-3 (0xFF = ff) |
| Rainbow RGB Braided Cable 2m | €34.99 | 9999 | BAC5-2 (ROT13 "Follow the rainbow") |
Prices and stocks are referenced exactly by the enigmas. Any modification breaks the journeys — the V10 migration contains an explicit warning on this point.
2. Five BAC1 to BAC5 agent accounts¶
Emails are agent.bac1@perfshop.io to agent.bac5@perfshop.io. The BCrypt hashes are untouchable — they correspond to passwords that each journey reveals to the student:
| Agent | Password | Hint |
|---|---|---|
agent.bac1 |
Cable22Go |
Given in clear at step 7 of BAC1 |
agent.bac2 |
Code64Exp6 |
Built by deduction at steps 4 and 5 of BAC2 |
agent.bac3 |
Docker8080Get |
Base64-encoded in the statement of step 15 of BAC3 |
agent.bac4 |
Lorem255FF42 |
Base64-encoded in the statement of step 19 of BAC4 |
agent.bac5 |
Rsa3Xor51Pi14 |
Built by computation (XOR + Pi) in BAC5 |
Students reuse these credentials to authenticate on the shop in the final step of their journey and trigger the success page.
3. Complete agent profiles¶
Each agent receives a full spy-themed profile (borrowed from V37 of the historical schema): codename inspired by espionage or cryptography characters, French address, phone number built from symbolic numbers, themed postal code.
| Agent | Name | City | Postal code |
|---|---|---|---|
| BAC1 | Jason Bourne | Paris | 75007 |
| BAC2 | Alan Enigma | Lyon | 69001 |
| BAC3 | Lisbeth Docker | Bordeaux | 33000 |
| BAC4 | Ethan Lorem | Toulouse | 31000 |
| BAC5 | Ada Cipher | Nantes | 44000 |
4. pedagogique_protected flags on standard products¶
Three products from the standard catalog (Basic USB Keyboard, Basic Optical Mouse, DDR4 16GB Kit (2x8GB)) receive the pedagogique_protected = TRUE flag. Their stock is no longer decremented by OrderService. These products are used in BAC3 enigmas (keyboards and mice) and BAC5 (DDR4 kit).
For the DDR4 kit, the stock is forced to 9999 because BAC5-22/23 enigmas don't compute on its stock. For the other two, the real stock is preserved because the enigmas depend on it.
Indexes and performance¶
Indexes placed in V1__schema.sql reflect the queries expected by the Spring Boot services:
| Table | Index | Target query |
|---|---|---|
users |
idx_email (unique) |
Login, email lookup |
users |
idx_country |
Business chaos country filters |
users |
idx_is_pedagogique |
Agent exclusion in the admin UI |
products |
idx_category |
Category filter in the catalog |
products |
idx_name |
Approximate text search |
products |
idx_prod_pedagogique |
Pedagogical product exclusion |
cart_items |
idx_user_id |
User cart retrieval |
cart_items |
idx_session_id |
Anonymous cart retrieval |
orders |
idx_order_number (unique) |
Lookup by order number |
orders |
idx_user_id |
Order history |
orders |
idx_created_at |
Chronological sort and time-based stats |
orders |
idx_is_test_data |
Fast exclusion of test orders |
admin_users |
idx_admin_email (unique) |
Admin login |
pedagogique_sessions |
idx_ped_sessions_level |
Per-level stats |
pedagogique_sessions |
idx_ped_sessions_joined_at |
Sort by age, purge |
pedagogique_sessions |
idx_ped_sessions_completed |
Completed sessions filter |
Encoding and engine¶
All tables use:
- Engine:
InnoDB(transactions, FK constraints) - Charset:
utf8mb4 - Collation:
utf8mb4_unicode_ci
Choosing utf8mb4 (and not utf8) is mandatory to support emojis in product descriptions and user names. MySQL treats utf8 as a legacy 3-byte encoding.
Bootstrapping an empty instance¶
On first startup, Flyway runs all V1 to V10 migrations in order. After V10, the database contains:
- A
userstable with the V2 test accounts plus the 5 pedagogical agents - A
productstable with ~1,000 products from the V3-V9 catalogs plus the 4 V10 pedagogical products - Empty tables:
cart_items,orders,order_items,pedagogique_sessions,perfshop_license - An empty
admin_userstable — the superadmin is created right after by Spring (ApplicationReadyEvent)
No manual intervention is needed: the platform is immediately usable after docker compose up.