The setup that looks fine
You build a 3D product configurator in Three.js. User picks material, size, options - and next to the model, a live price updates as they configure. The frontend calculates it because that is fast, feels responsive, and does not need a network trip.
The code looks like this:
function calculatePrice(config) {
let price = BASE_PRICE;
price += MATERIAL_PRICING[config.material];
price += config.width * config.height * PER_SQ_METER;
return price;
}
// Live update on any config change
someSlider.addEventListener('input', () => {
state.config = readConfig();
state.price = calculatePrice(state.config);
updatePriceDisplay(state.price);
});
// On order submission, send the whole state
function submitOrder() {
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(state)
});
}
This is what most tutorials show. It is also what I shipped in my first configurator.
The obvious problem I missed
A user opens DevTools. window.state.price = 1. Clicks 'Buy'. Order goes to my backend with price = 1. Backend has no independent pricing logic - it trusts what the frontend sends.
This is not paranoia. This is a 5-second exploit that anyone with basic browser knowledge can do. I caught it because a friend I asked to test the configurator did exactly this within 20 seconds of opening the page.
The fix that is actually secure
Separate two responsibilities:
- Frontend: preview the price. This is UX, not truth.
- Backend: calculate the final price from raw config, independently. This is truth.
// Frontend - preview only
function previewPrice(config) {
// Same math as before, but purely for display
// Explicitly labeled 'estimate' in UI
}
function submitOrder() {
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({ config: state.config }) // No price sent
});
}
// Backend - authoritative
function calculateFinalPrice(array $config): float {
$price = self::BASE_PRICE;
$price += self::MATERIAL_PRICING[$config['material']] ?? throw new \InvalidArgumentException();
$price += $config['width'] * $config['height'] * self::PER_SQ_METER;
return $price;
}
Frontend never sends price. Backend never trusts frontend price. If someone hacks the DevTools price display, they only change what they see - the order still calculates correctly server-side.
The 'but performance' objection
One network trip per configuration change would kill UX. So do not do that. Frontend preview stays instant (client-side math), but is labeled 'preview' or 'estimate'. Backend calculation happens ONCE, at order submission. User never waits during configuration.
Add a 'Confirm order' step where the real price is fetched from backend and displayed for confirmation - so there is no surprise between what they saw and what they pay. This is the pattern Bagisto, Shopify custom apps and Zakeke all use.
The gotcha with variant validation
Same principle applies to configuration validity. Frontend might allow user to set impossible combinations (glass thickness 20mm with hinge type 'lightweight'). Backend must validate independently and reject.
I keep validation logic in a shared JSON schema (JSON Schema draft-07) that both frontend and backend read. Frontend uses it for real-time feedback. Backend uses it as the final gate.
{
"if": { "properties": { "thickness": { "minimum": 15 } } },
"then": { "properties": { "hinge": { "enum": ["heavy-duty"] } } }
}
One source of truth. Both sides read it. Backend is authoritative on submission.
Reference implementation
My starter threejs-product-configurator-starter has the frontend preview pattern wired in. Backend calculator is not in the starter (it is deliberately frontend-only for education), but the client-side code is structured so that adding server-side pricing later is a one-file change.
The pricing question is one of five I hit building configurators. The full list is in my 4programmers.net Polish-language thread - WebGL context leaks, material vs new material for color changes, pixel ratio caps for 4K, OrbitControls vs TrackballControls for touch. Different post, same theme: things Three.js tutorials skip.
I am Dominik Groński / GroDev - a new studio in Poznań, Poland (JDG since May 2026), building custom 3D configurators and Laravel panels for manufacturers. Available for first paid deployments.
Top comments (0)