6.5 KiB
02 — Architecture
Local-First Principle
SQLite is the single source of truth for the user. MariaDB is a sync server only — it never blocks local operations.
User
│
▼
SQLite (qfs.db) ← all CRUD operations go here
│
│ background sync (every 5 min)
▼
MariaDB (RFQ_LOG) ← pull/push only
Rules:
- All CRUD operations go through SQLite only
- If MariaDB is unavailable → local work continues without restrictions
- Changes are queued in
pending_changesand pushed on next sync
Synchronization
Data Flow Diagram
[ SERVER / MariaDB ]
┌───────────────────────────┐
│ qt_projects │
│ qt_configurations │
│ qt_pricelists │
│ qt_pricelist_items │
│ qt_pricelist_sync_status │
└─────────────┬─────────────┘
│
pull (projects/configs/pricelists)
│
┌────────────────────┴────────────────────┐
│ │
[ CLIENT A / SQLite ] [ CLIENT B / SQLite ]
local_projects local_projects
local_configurations local_configurations
local_pricelists local_pricelists
local_pricelist_items local_pricelist_items
pending_changes pending_changes
│ │
└────── push (projects/configs only) ─────┘
│
[ SERVER / MariaDB ]
Sync Direction by Entity
| Entity | Direction |
|---|---|
| Configurations | Client ↔ Server ↔ Other Clients |
| Projects | Client ↔ Server ↔ Other Clients |
| Pricelists | Server → Clients only (no push) |
| Components | Server → Clients only |
Local pricelists not present on the server and not referenced by active configurations are deleted automatically on sync.
Soft Deletes (Archive Pattern)
Configurations and projects are never hard-deleted. Deletion is archive via is_active = false.
DELETE /api/configs/:uuid→ setsis_active = false(archived); can be restored viareactivateDELETE /api/projects/:uuid→ archives a project variant only (variantfield must be non-empty); main projects cannot be deleted via this endpoint
Sync Readiness Guard
Before every push/pull, a preflight check runs:
- Is the server (MariaDB) reachable?
- Can centralized local DB migrations be applied?
- Does the application version satisfy
min_app_versionof pending migrations?
If the check fails:
- Local CRUD continues without restriction
- Sync API returns
423 Lockedwithreason_codeandreason_text - UI shows a red indicator with the block reason
Pricing
Principle
Prices come only from local_pricelist_items.
Components (local_components) are metadata-only — they contain no pricing information.
Lookup Pattern
// Look up a price for a line item
price, found := s.lookupPriceByPricelistID(pricelistID, lotName)
if found && price > 0 {
// use price
}
// Inside lookupPriceByPricelistID:
localPL, err := s.localDB.GetLocalPricelistByServerID(pricelistID)
price, err := s.localDB.GetLocalPriceForLot(localPL.ID, lotName)
Multi-Level Pricelists
A configuration can reference up to three pricelists simultaneously:
| Field | Purpose |
|---|---|
pricelist_id |
Primary (estimate) |
warehouse_pricelist_id |
Warehouse pricing |
competitor_pricelist_id |
Competitor pricing |
Pricelist sources: estimate | warehouse | competitor
"Auto" Pricelist Selection
Configurator supports explicit and automatic selection per source (estimate, warehouse, competitor):
- Explicit mode: concrete
pricelist_idis set by user in settings. - Auto mode: client sends no explicit ID for that source; backend resolves the current latest active pricelist.
auto must stay auto after price-level refresh and after manual "refresh prices":
- resolved IDs are runtime-only and must not overwrite user's mode;
- switching to explicit selection must clear runtime auto resolution for that source.
Latest Pricelist Resolution Rules
For both server (qt_pricelists) and local cache (local_pricelists), "latest by source" is resolved with:
- only pricelists that have at least one item (
EXISTS ...pricelist_items); - deterministic sort:
created_at DESC, id DESC.
This prevents selecting empty/incomplete snapshots and removes nondeterministic ties.
Configuration Versioning
Principle
Append-only: every save creates an immutable snapshot in local_configuration_versions.
local_configurations
└── current_version_id ──► local_configuration_versions (v3) ← active
local_configuration_versions (v2)
local_configuration_versions (v1)
version_no = max + 1on every save- Old versions are never modified or deleted in normal flow
- Rollback does not rewind history — it creates a new version from the snapshot
Rollback
POST /api/configs/:uuid/rollback
{
"target_version": 3,
"note": "optional comment"
}
Result:
- A new version
vNis created withdatafrom the target version change_note = "rollback to v{target_version}"(+ note if provided)current_version_idis switched to the new version- Configuration moves to
sync_status = pending
Sync Status Flow
local → pending → synced
Sync Payload for Versioning
Events in pending_changes for configurations contain:
| Field | Description |
|---|---|
configuration_uuid |
Identifier |
operation |
create / update / rollback |
current_version_id |
Active version ID |
current_version_no |
Version number |
snapshot |
Current configuration state |
idempotency_key |
For idempotent push |
conflict_policy |
last_write_wins |
Background Processes
| Process | Interval | What it does |
|---|---|---|
| Sync worker | 5 min | push pending + pull all |
| Backup scheduler | configurable (backup.time) |
creates ZIP archives |