Skip to content

Data model

The app deliberately avoids schema migrations where it can — the people store rides inside an existing table so “add People & Access” needed no SQL to run.

One row per submitted build.

Column Type Notes
name, mobile, email text contact
dealership, vehicle, colour, rear text build summary fields
build jsonb full build: { config, plate, rear, bull, acc[], extras[], … }
consent bool nullable; some older tables predate this column (the insert falls back gracefully)
total, weekly numeric price + weekly finance figure
created_at timestamptz

A two-row table doing double duty:

Row id Holds
1 the product catalogue
2 the people store{ users: [...] }

Funnel analytics sink (POST /api/event writes, GET /api/events reads, staff-only).

User record (people store, catalogue row 2)

Section titled “User record (people store, catalogue row 2)”
{
"id": "uuid",
"name": "Jane",
"role": "staff | dealer | mfr",
"dealer_slug": "norris",
"code_hash": "hmac-sha256(VIZ_SECRET, CODE)",
"active": true,
"revoked_at": null,
"last_login_at": "ISO8601"
}

The personal code is never stored — only its HMAC hash. A database leak alone therefore does not hand anyone a working login.

Authentication is a signed cookie (viz_auth, HMAC-SHA256 with VIZ_SECRET) that is re-validated against the database on every request — so revoking a user takes effect immediately, not when their cookie eventually expires. It fails closed: no VIZ_SECRET, or an unconfirmable session, means access is refused.

Role Granted by Lifetime Sees
demo shared demo code / tenant gate code 5 min the builder
view GET /api/build (rego + mobile re-open) 2 h, read-only their own saved build
staff per-user account or VIZ_STAFF_CODES 12 h everything, incl. leads & people admin
dealer:<slug> per-user account or VIZ_DEALER_CODES 12 h their dealer portal + margins
mfr:<slug> per-user account or VIZ_MFR_CODES 12 h product demand across all dealers

The token payload is exp.role.uid.iat (older shared-code tokens are just exp.role). Role is read from the store, not the cookie, so changing someone’s role or dealership takes effect on their next request.

api/_odoo.js maps each configurator selection to an Odoo product. Matched by default_code (SKU); the 12V extras have no SKU, so they match on product name (ilike). Price always comes from Odoo.

Selection Match SKU / name
setup (fitting kit) SKU ACTFITKIT
rear.tray SKU T1T1819RC
rear.canopy SKU X1F2D1817R
bull.post2 SKU ACASRR1400-1
bull.4x4 SKU ACAFRR2400
acc.drawers SKU ACADCB900B
acc.rack SKU ACAAFL
acc.tank SKU ACAFLJ20L-1
acc.toolbox SKU X2F1286R
acc.led SKU ATBFDS0605-3D
extras.antenna name Smart 40A
extras.suspension name 200W
extras.dualbattery name 100Ah
extras.drivinglights name 200Ah
extras.recovery name Core 2K