Design Studio — System Overview¶
Platform-wide architecture summary for the Rauschmayer / DIY ROCKS jewelry configurator. Compiled from the per-component documentation in this folder. For component-level detail, follow the links in the Documentation Index.
Last compiled: 2026-06-11. Source docs verified against
develop-2.0branches.
1. Platform Summary¶
This is a multi-tenant jewelry configurator platform that lets end customers design custom
jewelry — name chains, signet rings, bridal/engagement rings, earrings, necklaces, and wedding
bands — through interactive 3D configurators embedded in e-commerce storefronts (WooCommerce,
Shopify, Shopware, or custom sites). The customer-facing layer is a Vue 3 web component called
Design Studio (the "container app") that orchestrates three independent designer applications,
each loaded in its own iframe and communicating over window.postMessage. The three designers are
the Name-chain Designer (jewelry_configurator), the Bridal Designer
(engagement_configurator), and the Wedding Designer (metrix_configurator, an outsourced
Metrix integration). 3D rendering is handled by an external service called Crystal Cave, hosted
separately and loaded into the designer iframes at runtime. The production backend is PersnApi, a
Laravel API that stores configurations, calculates prices (via the external R2 service), and
converts orders into factory jobs through EDI (internal Crystal Cave factory) or the R2 external order
API. A second Laravel application, PersnBackend, provides an admin panel plus e-commerce
middleware that bridges Shopify and Shopware storefronts to PersnApi. For WooCommerce stores, a
dedicated WordPress plugin (design-studio-woo) performs the same add-to-cart and order-sync role.
The platform is multi-tenant throughout: each storefront brand is a named tenant scoped by config
files in the designers and by bearer-token-scoped, customer-isolated data in the backends.
2. System Architecture Diagram¶
CUSTOMER BROWSER
┌──────────────────────────────────────────────────────────────────────────────┐
│ Storefront host page (WooCommerce / Shopify / Shopware / custom site) │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Integration layer (one of): │ │
│ │ • design-studio-woo (WordPress plugin, [design_studio] shortcode) │ │
│ │ • diy-event-listener.js (injected by PersnBackend on Shopify) │ │
│ │ • custom embed + custom triggerHostAction handler │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ ▲ │ postMessage: triggerHostAction │
│ jca.ready│ │ (addToCart / wishlist / print …) │
│ │ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Design Studio = <jewelry-container-widget> (Vue 3 Custom Element) │ │
│ │ container-app · ConfiguratorService.js orchestrates all postMessage │ │
│ │ │ │
│ │ #mount-configurator (≤ 3 live iframes, LRU eviction) │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────┐ │ │
│ │ │ Name-chain │ │ Bridal │ │ Wedding (Metrix) │ │ │
│ │ │ jewelry │ │ engagement │ │ outsourced iframe │ │ │
│ │ │ PersnVueApp │ │ configurator │ │ (bridge.* events) │ │ │
│ │ └──────┬────────┘ └──────┬────────┘ └──────────┬────────────┘ │ │
│ └──────────┼─────────────────┼──────────────────────┼─────────────────┘ │
└─────────────┼─────────────────┼──────────────────────┼──────────────────────┘
│ widget-loader │ widget-loader │ integration.js
▼ ▼ ▼
┌────────────────────────────────────┐ ┌─────────────────────┐
│ WebGI 3D widgets (CrystalCave) │ │ Metrix API │
│ signetring → crystal-cave- │ │ (snapshots, │
│ signet │ │ manufacturing) │
│ bridalrings → crystal-cave- │ │ EXTERNAL / 3rd pty │
│ ring (diy-bridal)│ └─────────────────────┘
│ bridalearrings → crystal-cave- │
│ earring │
│ bridalpendants → crystal-cave- │
│ necklace │
│ iframe bundles · contentWindow │
│ .CrystalCave.world.* (no postMsg) │
└────────────────────────────────────┘
───── Configuration capture + order (server side) ───────────────────────
Integration layer ── capture/price/order HTTP ──► ┌────────────────────────┐
│ PersnApi (Laravel) │
PersnBackend (Shopify/Shopware) ──────────────────►│ configs, pricing, │
middleware cron every 15 min │ orders, EDI, PDFs │
└───────┬────────────────┘
│
┌───────────────────────────────────────────┼───────────────────┐
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌────────────────┐ ┌──────────────┐
│ R2 pricing + │ │ Crystal Cave │ │ EDI filesystem │ │ AWS S3 │
│ external │ │ FACTORY │ │ (factory in/ │ │ screenshots, │
│ order API │ │ (name-chain │ │ out messages) │ │ PDFs, certs, │
│ (bridal + │ │ production via │ │ │ │ price JSON │
│ metrix) │ │ TriggerProduction│ │ │ │ │
└──────────────┘ └──────────────────┘ └────────────────┘ └──────────────┘
PersnBackend admin panel (AdminLTE): orders, customers, procurements, vendor invoices,
manual orders → PersnApi. Polls Shopify/Shopware for paid orders → PersnApi order/submit.
Data-flow summary: configuration (designer + Crystal Cave) → capture (postMessage
triggerHostAction → integration layer → PersnApi /capture) → order (PersnApi /order/submit
or /order/external-bulk) → factory (Crystal Cave for name-chain via EDI; R2 external order API
for bridal + Metrix).
3. Component Breakdown¶
| Component | Repo | Tech stack | Primary responsibility |
|---|---|---|---|
| Design Studio (container app) | jewelry-container-app |
Vue 3 Custom Element, Pinia, Vue Router (hash), Vite, Axios, vue-i18n | Embeds as <jewelry-container-widget>; hosts and orchestrates the 3 designer iframes; normalizes all postMessage traffic; emits triggerHostAction to the host page |
| Name-chain Designer | jewelry (PersnVueApp-2.0) |
Vue 3, Vuex, Vue Router, Vite, Axios | Configures personalized jewelry (name chains, signet rings; ring/bracelet/necklace/earring); builds model data for Crystal Cave; dispatches add-to-cart by tenant cart type |
| Bridal Designer | engagement (engagementconfigurator-2.0) |
Vue 3, Vuex, Vue Router (hash), Vite, Axios | Configures bridal/engagement rings, earrings, necklaces; diamond search; per-jewelry-type build targets sharing src/shared/ |
| PersnApi | PersnApi |
PHP 8.1, Laravel 9, MySQL, Redis, S3, Sanctum, TCPDF/FPDI | Stores configs + model data; routes pricing to R2; converts configs into EDI/factory orders; generates PDFs/certificates; multi-tenant by {customer} slug |
| PersnBackend | PersnBackend |
PHP 8.0, Laravel 9, AdminLTE 3.8, Fortify+Sanctum, Spatie Media Library | Admin panel (orders, customers, procurements, invoices) + Shopify/Shopware middleware that creates products and syncs paid orders to PersnApi |
Supporting components (not standalone "main repos" but active):
| Component | Repo | Tech stack | Role |
|---|---|---|---|
| design-studio-woo | design-studio-woo |
WordPress plugin, PHP 8.0, WooCommerce | Embeds Design Studio via shortcode; add-to-cart REST endpoint; two-layer order sync to PersnApi /order/submit |
| Wedding Designer (Metrix) | Metrix (outsourced) | 3rd-party | Wedding band configurator; snapshot + manufacturing API; embedded via integration.js |
| Crystal Cave | crystalcave |
TypeScript/Express, Blender 2.79b + Blend4Web, Webpack | 3D asset pipeline (DXF→STL/web models) and in-browser 3D widget; also the internal name-chain production factory |
4. Key Data Flows¶
Flow 1 — Customer configures and adds to cart (WooCommerce)¶
- Storefront page renders the
[design_studio]shortcode →<jewelry-container-widget>+<div id="mount-configurator">. - Design Studio boots:
POST /api/v2/jewelry-container-app/config/loads offers/settings; emitsjca.ready. - User picks an offer; container app creates the designer iframe and sends
load-configuration. - The designer (Name-chain or Bridal) builds model data,
POST .../modelData→model_hash; Crystal Cave renders the 3D view. - User clicks Add to Cart inside the designer iframe →
addToCartpostMessage to the container app. - Container app calls
personalizeActiveApp()(updates localStorage/iframe id) then poststriggerHostAction{ intent:'addToCart', details:{ tenant, app, r2CustomerId, configurationId, configurationUrl, imageUrl, currency } }to the host page. - The plugin's
event-listener.jscatches it →POST /wp-json/design-studio/v1/add-to-cart(retries up to 5× with exponential backoff). - The plugin REST endpoint fetches product details + price: Name-chain/Bridal →
GET {diyApi}/capture/{configurationId}?configurator_type={app}(price included in capture response). - A hidden WooCommerce product (SKU
DS-{configurationId}, virtual, sold-individually,catalog_visibility: hidden) is created or reused and added to the cart; mini-cart fragments returned.
For Wedding (Metrix): the container app first creates a snapshot (
POST /gateway/api/integration/containers/{containerId}/snapshots) to obtain asnapshotId, then emitstriggerHostAction; the plugin uses/api/snapshots/{id}/manufacturing+/metrix/{id}/price.
Flow 2 — Order placed → factory¶
- WooCommerce order moves to Processing or Completed.
- Plugin instant hook (fast path) calls
POST {diyApiUrl}/order/submitonce with all Design Studio line items in a single bulk request. A WP-Cron safety net (every 15 min, last 24h, up to 10 retries/item) covers failures. HTTP423 Lockedis treated as success (idempotent re-submission). - PersnApi
order/submitplaces each item in one DB transaction and routes byapp: jewelry_configurator→NameChainOrder→ dispatchesCrystalCave\TriggerProduction(POSTs model JSON to the Crystal Cave factory; EDI prefixAPICALL_).engagement_configurator→BridalOrderandmetrix_configurator→DesignerOrder→ dispatched to R2 viaPlaceExternalOrder(POST/api/public/externalOrder/order).- Each configuration can be ordered once; a repeat returns
423 Locked(but an idempotent retry of the samecustomer_order_numberreturns the existing order).
Flow 3 — Admin management / Shopify-Shopware sync (PersnBackend)¶
- On a Shopify store,
diy-event-listener.js(injected by PersnBackend when the site is added) handles thetriggerHostActionand callsPOST /api/products/create/{configurationId}on PersnBackend. - PersnBackend looks up the
ShopifySitebycustom_domain, fetches capture/manufacturing data + price, creates a Shopify product, and stores aShopifyProductrecord (shopify_order_id = null). - Customer pays. Scheduled
app:process-orders(every 15 min) runsprocess-shopify-ordersandprocess-shopware-orders: fetches paid orders from the last 12h, and per line item calls PersnApiPOST order(name-chain/bridal) orPOST order/metrix(Metrix). - After all line items in an order succeed, one
POST order/external-bulkcovers all Bridal + Metrix items atomically;shopify_order_id/shopware_order_idis then set. A failed batch leaves it null and retries next run. - Admin panel (AdminLTE) manages orders, customers, products, procurements, vendor invoices, manual orders (
/api/v1/{tenant}/capturethen/order), tokens, and audits. New-order emails go out every 10 minutes.
Flow 4 — Direct embed (non-WooCommerce / custom site)¶
- Custom site includes the Design Studio script and the
<jewelry-container-widget>element withoptionsid,apibaseurl,metrixapibaseurl,embedhosturl,apppublicurl. - Design Studio boots and emits
jca.ready. - User configures; the designer posts
addToCart→ container app emitstriggerHostActionto the host page. - The host page implements its own
window.addEventListener('message', …)to catchtriggerHostAction, then calls PersnApi/capture/{id}, fetches the price, and adds the item to its own cart. The persn.com Shopify Liquid embed (seepersn-com-embed-fix.md) is an example of this pattern, including ajca.changeLocalebridge gated onjca.ready.
5. Multi-Tenancy¶
Tenancy runs through every layer:
Designer tenant configs. Each designer app ships a tenant config keyed by name:
- Name-chain:
src/data/tenants.js(16 tenants), wrapped bysrc/classes/Tenant.js. The active tenant comes fromembedConfig.tenant. - Bridal:
src/tenantConfig.js(12*-bridaltenants), merged into runtime config byappConfig.js; matched againstconfiguratorConfig.tenant.
A tenant config controls branding (colors, logo, PDF footer), which options are shown
(inactivePlatingGroups, inactiveStoneGroups, inactiveThemeTypes, etc.), pricing/customer id,
router mode, and — critically — the cart dispatch method.
Cart / dispatch types (Name-chain Tenant.addToCart):
| Type | Meaning / mechanism |
|---|---|
woocommerce |
Dispatches CustomEvent('diyConfigurator:addToCartButtonClicked', …) (or buyNow…). Used by persn-com, mytrauringstore, veynou, unique-jewelry, diy-rocks, vanhess-jeweller, stargold, emperor-diamond, passion-jewelry, rauschmayer |
fourGradAddToCart |
Calls window.fourGradAddToCart(configurationId) directly. Used by christ, christ-ch, valmano, gravurschmuck, meicel-jewelry |
systaro |
Dispatches the same CustomEvent as woocommerce; only juwelier-plus uses it |
contactJeweler |
Dispatches CustomEvent('diyConfigurator:contactJeweler', …) instead of cart |
| (container-app) | When inside the iframe container, the designer posts {type:'addToCart'} to window.parent — the container app then normalizes to triggerHostAction |
The container app emits a single normalized contract upward regardless of the designer's internal dispatch.
PersnApi. Multi-tenant by {customer} slug in every route. Laravel Sanctum bearer tokens carry
named abilities mapping to route names; an admin ability bypasses customer scoping. ApiController
enforces checkAccess (ability) and checkCustomer (ownership) on every request. The Customer
model's configurator_type (0 = jewelry/Persn, 1/2 = bridal, 3 = Metrix) drives pricing-path
selection. Tokens may also carry an allowed_factory for factory-scoped production reads.
PersnBackend. Customer-scoped data isolation by role. Users have a customer_id (active context)
and a customers JSON array of permitted customers; customer_id = -1 means all. CUSTOMER /
CUSTOMER_ACCOUNTING roles are always scoped to their assigned customer; super-admin/super-user can
switch context. Encrypted per-customer bearer tokens authenticate calls into PersnApi.
6. PostMessage Protocol¶
The container app (ConfiguratorService.js) is the central hub. A single
window.addEventListener('message', …) handles all inbound messages. The seam has three boundaries:
host page ↔ Design Studio ↔ designer iframes.
Design Studio → Host page (window.postMessage(msg, '*')):
jca.ready— boot complete; the host may now send locale/currency messages.triggerHostAction— normalized user action.intent ∈ { addToCart, createWishlist, print, contactJeweler, convertOldSnapshotId };detailsincludestenant, app, r2CustomerId, configurationId, configurationUrl, imageUrl, currency?. This is the primary integration contract for any storefront.
Host page → Design Studio (handled via window.addEventListener):
jca.changeLocale{ details:{ locale } },jca.changeCurrency{ details:{ currency } }— propagated to all mounted iframes.loadSnapshot{ details:{ snapshotId } }— response toconvertOldSnapshotId.
Designer iframes → Design Studio:
- All:
iframe-height { height }(debounced 150 ms). - Name-chain / Bridal:
initializedConfigurator;saveConfiguration { details:{ configurationId, imageUrl } };addToCart;createWishlist;contactJeweler. - Wedding (Metrix, via
MetrixConfigurator.html):bridge.containerId;bridge.addToCart;bridge.wishlist;bridge.print;bridge.consultation;metrix.saveConfiguration. For all Metrix host actions, the container app first creates a snapshot via the Metrix API to obtain aconfigurationId(snapshot id). mobileCartState— received but currently inactive (setMobileCartStatenot defined in the Pinia store; the Mobile Sticky Cart feature is disabled).
Design Studio → Designer iframes (iframe.contentWindow.postMessage):
load-configuration { config, clarityId, clarityUserId }(once after iframe load),saveConfiguration,changeCurrency,changeLocale,injectCustomCss(Name-chain/Bridal inject into<head>; Metrix injects into its shadow DOM).
Legacy DOM-event bridge. The WooCommerce plugin's event-listener.js and the designers'
Tenant.addToCart also support custom DOM events (diyConfigurator:addToCartButtonClicked,
engagementConfigurator:addToCartButtonClicked) for non-container embeds.
7. Deployment & Environments¶
Designers (jewelry, engagement). GitLab CI pipelines, triggered manually. Set $TENANT (and
$APP = ring/earring/necklace for engagement) before running. Branch convention: develop* →
staging, release* → production. Local: ./setupTenant.sh <tenant> (jewelry) or
./setupAppAndTenant.sh <app> <tenant> (engagement), then npm run dev. Crystal Cave is deployed
separately per jewelry type (crystal-cave-ring/, etc.) and copied to the target server.
Container app. Vite build; embedded on the host page via a <script> tag loading the built JS
that defines the custom element. Local dev: npm run dev (no setup script). Build-time env vars:
VITE_API_URL, VITE_API_TOKEN, VITE_SCREENSHOT_URL, VITE_METRIX_API_URL, VITE_MODE.
PersnApi. Laravel app. Requires a queue worker (php artisan queue:work) and a per-minute
scheduler. Deploy via bash deploy_tenant.sh (clears + re-caches config/routes, reloads PHP-FPM).
APP_ENV=staging enables an extra hourly edi:force. Cache/queue on Redis in production.
PersnBackend. Laravel + Vite (HTTPS required locally to inject the Shopify script tag). Per-minute
scheduler runs app:process-orders (15 min), app:delete-unused-products (daily), email:new-orders
(10 min). Queue worker handles media conversions.
design-studio-woo. WordPress plugin; WP-Cron jobs (ds_woo_every_15_min, daily cleanup) — a real
server cron hitting wp-cron.php is recommended on low-traffic sites.
Known stage URLs. 3D Designer API https://test-api.3d-designer.shop; Metrix
https://rauschmayer.metrix-demo.com; R2 pricing
https://api.r2-stage.demo-version.net/api/public/b2c/calculatePrices. Production 3D Designer API:
https://api.3d-designer.shop/.
8. 3D Rendering — WebGI Widgets¶
The interactive 3D rendering layer is a set of four WebGI-based TypeScript widgets, each built to
a UMD bundle (diamond_shader.umd.js) that exposes a single browser global, window.CrystalCave. All
public surface lives on window.CrystalCave.world (an instance of a per-widget World class). Each
widget is embedded as an iframe by a designer app. The signet ring widget is embedded by the
Name-chain Designer (PersnVueApp / jewelry); the other three are embedded by the Bridal
Designer (engagement).
No postMessage. The parent designer drives each widget by reaching into the iframe and calling
methods directly: iframe.contentWindow.CrystalCave.world.setData(...). (The bridalrings integration
doc also describes an optional postMessage routing layer for cross-origin use, but the same-origin
direct-call pattern is the one in use.) The widgets emit no events and register no callbacks; the only
callback mechanism is the per-call result of setData / getScreenshot / getProductImageAsBase64.
These WebGI widgets are distinct from the legacy crystalcave repo (the Blender/Blend4Web asset
pipeline and the internal name-chain production factory described in §4 Flow 2). They share the
"Crystal Cave" name and global namespace but are separate codebases with their own GLB asset pipelines.
8.1 Widget map¶
| Widget | Repo / source | Embedded by | Configures |
|---|---|---|---|
| Signet ring | signetring |
Name-chain Designer (PersnVueApp) | Signet ring: base style + plate shape, engraving plate (letter/font/zodiac/fingerprint/text), metal plating, surface finish (top/side), stone-texture rings, diamonds |
| Bridal rings | bridalrings |
Bridal Designer | Engagement ring: diamond shape, head (setting), shank (band), per-head/per-shank plating, engraving, AR try-on, lift-up |
| Bridal earrings | bridalearrings |
Bridal Designer | Mirrored left/right earring pair from a single model ID; metal + diamond optics; standalone stone preview |
| Bridal pendants | bridalpendants |
Bridal Designer | Pendant or necklace head; optional chain (1 of 2 styles); bail connector; metal + diamond color |
8.2 Common API¶
All four widgets share a core surface:
| Method | Behavior |
|---|---|
world.initialize(json?) |
Boots the WebGI viewer (loads scene, materials, volume/stone data). Must run once before anything else. Takes an optional { assetRoot }; defaults to ./assets/. Camera zoom/pan/rotation are locked at init. Requires a canvas element in the host page (#webgi-canvas for signetring, #webgl_drawer for the three bridal widgets). |
world.setData(data) |
The main call. Accepts the configuration object/JSON, loads the right GLBs, applies materials, and returns weight/volume + stone data. Async (returns a Promise). |
world.setBackgroundColor(val) |
Sets the scene background to a CSS color/hex (e.g. "#F8F8F8") so the iframe blends into the page. |
world.setCameraDistance(d) |
Scales the camera distance from the model. |
world.resetCamera() |
Recenters the camera on the model / default position. |
What setData returns — the shape differs per widget but always carries the data the configurator
forwards to PersnApi pricing (metal weight = volume × material specific weight):
| Widget | setData return (pricing-relevant fields) |
|---|---|
| signetring | { status, weight, stones } — weight in grams (from ring_data.json volume × plating specific weight, base scaled by inner diameter); stones: [{ quantity, code }] |
| bridalrings | { status, response_number, volume: { head, shank }, stones: [{ head, shank }] } — volumes in mm³ (shank scaled to ring diameter) |
| bridalearrings | { status, response_number, volume: { left, right }, stones: [{ left, right }] } |
| bridalpendants | { status, response_number, volume: { head }, stones: [{ head }] } |
The three bridal widgets include a response_number (monotonic counter) so the configurator can
discard stale out-of-order responses. Screenshot capture is getScreenshot() (bridal widgets, returns
a PNG data URL) or getProductImageAsBase64(json, cb) (signetring, callback with base64); all reset
the camera before capturing.
8.3 Per-widget differences¶
- signetring — Geometry is a base + engraving-plate structure. Driven by
ring_style(basic / shoulder / side-circle / top-frame / combinations) ×ring_shape(cushion / oval / rectangle / round / mini / small / hexagon / hexagon-stone). Settingplate_stone(onyx / lapis / malachite / tigerseye) auto-switches the ring to the Lucke "stone" style. Rich plate system: single/double/quarter letters, multiple fonts (Roboto Flex, Playfair, Courier, Libre Baskerville), zodiac symbols, fingerprint plates, and free engraving text. Plating SKU → metal ID lookup; surface finish (polished/brushed) set independently for top and side faces. Includes a standaloneCrystalCave.FingerprintProcessorutility for preprocessing fingerprint images. - bridalrings — Head + shank + diamond as separate GLBs. Head file resolved as
{head_id}_ct{karat}.glb(karat bucketed to 6 tiers); setting a head forces the loose diamond toempty. Shank needs a matching cutout so head prongs fit —setDataswaps the shank folder (shank/,shank_cutout_h1041/,_h4018/,_hd038/,_h5021/) by head-ID + karat rules. Supports two-tone plating (head and shank independent), text engraving, AR try-on (ijewel3d web-vto, 20 localized languages), and lift-up mode (per-shank Y offsets). - bridalearrings — Renders a mirrored pair from one model ID: left at
(+0.5,0,0)rot-1.5z, right at(-0.5,0,0)rot+1.5z. No shank/head separation — each earring is a single self-contained GLB ({id}_ct{carat}.glb). Left and right cannot be configured independently. Carat token is passed asstone.diamond.code(e.g."ct1000";eb036/eb046also havect1500).setEmpty(shape)shows a bare stone. - bridalpendants — Single piece with chain attachment.
chain.pendantType.typeselects the loader:"pendant"→setPendant()(prefixp, single chain GLB +bail_1.glbbail connector) vs"necklace"→setNecklace()(prefixn, split_left/_rightchain halves). Chains attach at named anchor objects (Default_chain_ap,Default_left_ap,Default_right_ap). All models rotatedπ/2on X at load.
8.4 Asset pipeline¶
GLB files are not hand-authored. They are produced by Python pipelines from CAD source
(Rhino .3dm / 3DM):
- signetring assets come from the separate SignetRingAssets project — a 4-step pipeline
(
step0_clear_output→step1_save_as_glb(Rhino export) →step2_fix_material(apply WebGI.pmat/.dmatextensions, mesh names, diamond/brushed extensions) →step3_copy_glb_to_public). Volume/diamond data per variant lives insrc/ring_data.json(drives weight calc). - bridalrings / bridalearrings / bridalpendants assets derive from the DemoWebGI_Assets source
project. Files follow strict naming conventions (heads
h{id}_ct{karat}.glb, shankss{id}_c56_w{width}_ct{karat}.glb, earringse{id}_ct{carat}.glb, pendants/necklacesp|n{id}_ct{carat}.glb). An asset manifest (variations.config.json) plus volume/stone-count JSON files feed the viewer. Per-repo Python utility scripts audit completeness and regenerate dropdowns (list_files.py,list_missing_carats.py,fix_bad_shanks.py, etc.).
WebGI versions differ across widgets: signetring and bridalrings use 0.17.0; bridalearrings and bridalpendants use 0.9.13.
8.5 Deploy paths¶
Each widget builds (npm run b) to dist/ and deploys via a per-repo rsync_*.py script
(paramiko SSH/SFTP, SHA-256 hash diffing, SSH key at %USERPROFILE%\.ssh\id_rsa). Both servers run on
port 1022: staging 49.12.38.120 (test-api3d-designershop), production 23.88.47.146
(api3d-designershop). Each widget deploys to a different CDN path:
| Widget | Remote path / served URL |
|---|---|
| signetring | …/crystal-cave-signet |
| bridalrings | …/crystal-cave-ring (served at /diy-bridal/ring/) |
| bridalearrings | …/crystal-cave-earring |
| bridalpendants | …/crystal-cave-necklace |
Production hosts under https://api.3d-designer.shop/, staging under
https://test-api.3d-designer.shop/.
8.6 Known quirks¶
- signetring
setColordoes not exist. The README showssetColor(1, 1, 1, …), but no such method exists insrc/index.ts. It is a stale/removed reference — do not use it. (The three bridal widgets do have a realsetColor(r,g,b,absorption,refractiveIndex,reflectivity,dispersion)for diamond optics.) - bridalpendants
setDataalways resets metal to gold.setPendant/setNecklaceend by callingsetMetal(0). The configurator must re-apply the desired metal withsetMetal(metalId)after everysetData(). - bridalpendants 400ms chain-positioning workaround. Chain anchors are positioned via an
await timeout(400)before chain placement, to ensure anchor objects are settled after model load. Chain offsets are hardcoded (-0.07Y for pendants;+0.02X /+0.03Y for necklaces) rather than read from the anchors. - bridalpendants inherited
e-prefix keys. Volume lookups useearrings_volume_data.jsonkeyed bye<headId>and stone counts useheads_stone_count.jsonkeyed byh<headId>, both inherited from the earrings/rings projects (the code rewrites thee→hprefix where needed). - bridalrings shank width matching. Width is not transmitted in the API payload; the shank GLB is
resolved by regex against
variations.config.json, with a width-agnostic fallback if the cutout variant was built at a different width.
9. Out of Scope / Prototype¶
original-configurator is a prototype and is not yet integrated into the production platform.
Its only documentation is a minimal README describing a git-submodule-based client app run with
yarn install && yarn start — it appears to be an early standalone configurator client, kept for
reference but not part of the active Design Studio → PersnApi → factory flow.
10. Documentation Index¶
| Area | File | Description |
|---|---|---|
| Top level | design-studio/ONBOARDING.md |
Onboarding: repos, how the apps talk, add-to-cart summary, stage URLs, gotchas |
design-studio/add-to-cart-integration-docs.md |
External IT integration guide: triggerHostAction, capture, prices, /order/submit |
|
design-studio/persn-com-embed-fix.md |
persn.com Shopify Liquid embed + locale-bridge fix | |
design-studio/persn-skus/products-changes-report.md |
SKU/plating-suffix changes; stone-plate splits into shape variants | |
design-studio/persn-skus/skus-price-update-21052026.md |
Filtered SKU price table (2026-05-21 update) | |
| Container app | design-studio/container-app/README.md |
Container app index + quick facts |
design-studio/container-app/architecture.md |
Stack, boot sequence, ConfiguratorService, Pinia store, component tree | |
design-studio/container-app/api-endpoints.md |
3D Designer + Metrix API endpoints used by the container app | |
design-studio/container-app/designers.md |
The three designer iframes: config shapes, ids, settings merge | |
design-studio/container-app/embedding.md |
Embed attributes, inline options, custom CSS/theming, locales | |
design-studio/container-app/features.md |
Feature inventory (multi-config, sharing, QR, deep links, Clarity) | |
design-studio/container-app/postmessage-protocol.md |
Full postMessage reference, all directions | |
design-studio/container-app/routing-and-deep-links.md |
Hash routing, route table, deep-link params | |
| Jewelry (Name-chain) | design-studio/jewelry/project-overview.md |
Stack, categories/themes, router, embed config, deploy |
design-studio/jewelry/tenant-config.md |
16 tenants, cart types, config fields, per-tenant notes | |
design-studio/jewelry/store-state.md |
Vuex state shape, mutations, session persistence | |
design-studio/jewelry/model-data-structure.md |
storedInputData payload sent to createModelData |
|
design-studio/jewelry/shared-reference-data.md |
Platings, stones, enamelings, ring sizes | |
design-studio/jewelry/signet-ring-data-structure.md |
Signet 3-product structure, theme types, stone plates, surfaces | |
design-studio/jewelry/demo-pages.md |
Demo/widget-loader page locations | |
| Engagement (Bridal) | design-studio/engagement/project-overview.md |
Build targets (ring/earring/necklace), steps, routes, deploy |
design-studio/engagement/tenant-config.md |
12 *-bridal tenants, config fields, embed overrides |
|
design-studio/engagement/store-state.md |
Vuex modules (configuration, diamond, canvasMenu) | |
design-studio/engagement/model-data-structure.md |
Ring/earring/necklace model data; stone modes | |
design-studio/engagement/reference-data.md |
Alloys, colors, diamonds, heads, shanks, chains | |
design-studio/engagement/api-and-services.md |
API config, BaseDesignerApiService, messaging plugin | |
| WooCommerce plugin | design-studio-woo/README.md |
Install, config, add-to-cart flow, order sync, cleanup, REST endpoint |
design-studio-woo/technical-reference.md |
Hooks, product/order meta, cron, payloads, capture mapping | |
| PersnApi | persn-api/README.md |
What it does, configurator types, scheduled tasks, Artisan commands |
persn-api/api-endpoints.md |
Full V1/V2 endpoint reference + error codes | |
persn-api/architecture.md |
Controllers, models, services, jobs, middleware, enums, schema | |
persn-api/configuration.md |
All .env variables (DB, Redis, S3, R2, Crystal Cave, Metrix, EDI) |
|
| PersnBackend | persn-backend/README.md |
Admin panel sections, API endpoint, scheduled tasks, docs feature |
persn-backend/ARCHITECTURE.md |
System context, layers, data model, order lifecycle, security | |
persn-backend/CONFIGURATION.md |
All .env variables + custom config files |
|
persn-backend/LOGGING.md |
Log channels, Sentry, what gets logged, per-env setup | |
persn-backend/ROLES-AND-ACCESS.md |
Roles, access matrix, customer scoping, 2FA, tokens | |
persn-backend/SCHEDULED-TASKS.md |
Cron schedule, process-orders detail, one-off commands | |
| 3D widget: signet ring | 3d-widgets/signetring/README.md |
WebGI signet-ring widget: what it renders, tech stack, structure, build/deploy |
3d-widgets/signetring/api-reference.md |
window.CrystalCave.world API: setData shape, ring/plate/plating tables, capture |
|
3d-widgets/signetring/integration.md |
Iframe embedding (direct contentWindow), modelData, plating/finish, weight/stones | |
3d-widgets/signetring/asset-pipeline.md |
SignetRingAssets 4-step Rhino→GLB pipeline, file naming, ring_data.json, rsync |
|
| 3D widget: bridal rings | 3d-widgets/bridalrings/README.md |
WebGI engagement-ring widget: diamond/head/shank/plating, AR, build/deploy |
3d-widgets/bridalrings/api-reference.md |
API: setData/getVolume, selection JSON, karat buckets, plating codes, AR, engraving | |
3d-widgets/bridalrings/integration.md |
Iframe embed + postMessage routing, modelData, return shape, screenshot, AR | |
3d-widgets/bridalrings/asset-pipeline.md |
Head/shank/diamond naming, cutout rules, lift-up offsets, variations.config, rsync | |
| 3D widget: bridal earrings | 3d-widgets/bridalearrings/README.md |
WebGI earring-pair widget: mirrored pair, materials, structure, build/deploy |
3d-widgets/bridalearrings/api-reference.md |
API: setData (left/right), plating codes, setMetal/setColor/setEmpty, screenshot | |
3d-widgets/bridalearrings/integration.md |
Embed, modelData, return shape, diffs from bridalrings (pair vs single, no shank) | |
| 3D widget: bridal pendants | 3d-widgets/bridalpendants/README.md |
WebGI pendant/necklace widget: chain/bail, carat buckets, structure, build/deploy |
3d-widgets/bridalpendants/api-reference.md |
API: setData, setPendant/setNecklace, plating codes, setColor, modelData shape | |
3d-widgets/bridalpendants/integration.md |
Embed, pendant vs necklace, metal-reset quirk, diffs from rings/earrings | |
| External / context | crystalcave/README.md |
Legacy Crystal Cave pipeline (Blender/Blend4Web), exporters, name-chain factory |
original-configurator/README.md |
Prototype client (not integrated) | |
| This file | SYSTEM-OVERVIEW.md |
Platform-wide architecture summary (you are here) |