<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dominik</title>
    <description>The latest articles on DEV Community by Dominik (@gronskideveloper).</description>
    <link>https://dev.to/gronskideveloper</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4063459%2F22e7ac79-5161-44b2-b933-ff19ebcb5783.png</url>
      <title>DEV Community: Dominik</title>
      <link>https://dev.to/gronskideveloper</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gronskideveloper"/>
    <language>en</language>
    <item>
      <title>How I built @grodev/claude-chat-react — architecture notes</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Tue, 18 Aug 2026 12:59:38 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/how-i-built-grodevclaude-chat-react-architecture-notes-5e61</link>
      <guid>https://dev.to/gronskideveloper/how-i-built-grodevclaude-chat-react-architecture-notes-5e61</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;A drop-in React chat widget on the Claude API — &lt;a href="https://www.npmjs.com/package/@grodev/claude-chat-react" rel="noopener noreferrer"&gt;@grodev/claude-chat-react&lt;/a&gt; — with a headless hook (&lt;code&gt;useClaudeStream&lt;/code&gt;) if you want to bring your own UI, or a &lt;code&gt;&amp;lt;ClaudeChat/&amp;gt;&lt;/code&gt; component if you don't. 8 KB gzipped, zero runtime deps beyond React. Companion project to my &lt;a href="https://github.com/GronskiDeveloper/claude-chat-widget" rel="noopener noreferrer"&gt;vanilla-JS widget&lt;/a&gt; — same wire contract, same PHP proxy, different frontend stack.&lt;/p&gt;

&lt;p&gt;This is a notes-from-the-build post: three decisions that shaped the API, and one thing I audited line-by-line because I don't trust the model with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: Two ways in, not one
&lt;/h2&gt;

&lt;p&gt;Every React library eventually gets asked &lt;em&gt;"can I use just the state, not the UI?"&lt;/em&gt; — so I built both from day one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Drop-in — 5 lines&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ClaudeChat&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grodev/claude-chat-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ClaudeChat&lt;/span&gt; &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/api/chat"&lt;/span&gt; &lt;span class="na"&gt;accentColor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#1D9E75"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Headless — bring your own UI&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useClaudeStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grodev/claude-chat-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useClaudeStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook does the streaming, state, and abort control; the component adds layout, dark mode, and a11y on top. Consumers who need a different look pay zero cost — they import only the hook and ~2 KB drops out of the bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: The API key never touches the browser (and I mean &lt;em&gt;never&lt;/em&gt;)
&lt;/h2&gt;

&lt;p&gt;Central design choice — the widget calls &lt;strong&gt;your&lt;/strong&gt; server-side proxy, never &lt;code&gt;api.anthropic.com&lt;/code&gt; directly. If you look at the source, there is no place where you can accidentally pass an API key as a prop. It's not in the type definitions. It's not a hidden config. &lt;strong&gt;The escape hatch does not exist.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the boring kind of security: not a validator that checks for keys, but an API shape where the wrong pattern isn't representable. Companion repo &lt;a href="https://github.com/GronskiDeveloper/claude-chat-widget" rel="noopener noreferrer"&gt;claude-chat-widget&lt;/a&gt; ships a working PHP proxy with the exact wire contract this library expects — copy it, deploy anywhere PHP runs, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: Streaming with a real abort
&lt;/h2&gt;

&lt;p&gt;The streaming loop looks like a hundred other SSE readers on GitHub. Three things I got specifically right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;AbortController&lt;/code&gt; lives in &lt;code&gt;useRef&lt;/code&gt;, not &lt;code&gt;useState&lt;/code&gt;. State would re-render every &lt;code&gt;send&lt;/code&gt;, and downstream components would blink.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setMessages((prev) =&amp;gt; ...)&lt;/code&gt; — never closure-based. Tokens arriving during a re-render need current state, not stale state, or you'll see torn text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if (payload.error) throw&lt;/code&gt; — routes upstream errors through the same catch, so &lt;code&gt;onError&lt;/code&gt; fires once with a real &lt;code&gt;Error&lt;/code&gt;, not on each malformed frame.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The a11y I refuse to leave to Claude
&lt;/h2&gt;

&lt;p&gt;When I let the model draft the JSX, it produced a working panel. It also produced a working panel without any of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;role="dialog"&lt;/code&gt; on the panel&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-live="polite"&lt;/code&gt; on the message log&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-expanded&lt;/code&gt; on the launcher&lt;/li&gt;
&lt;li&gt;Focus management (input focus on panel open, escape close)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Enter&lt;/code&gt; sends, &lt;code&gt;Shift+Enter&lt;/code&gt; inserts a newline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is in the final code. Every one was added by me after the draft, not before. LLMs don't skip a11y out of malice — they skip it because most tutorial code they trained on skipped it. If you don't audit specifically for the boring accessibility bits, you don't get them.&lt;/p&gt;

&lt;p&gt;That pattern generalizes: audit the AI's work for what the training data underrepresents, not for what the model gets "wrong."&lt;/p&gt;

&lt;h2&gt;
  
  
  What's not in the package
&lt;/h2&gt;

&lt;p&gt;Deliberately out of scope, documented in the README:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limiting → add on your proxy&lt;/li&gt;
&lt;li&gt;Retrieval-augmented context → your job; feed the system prompt from your data&lt;/li&gt;
&lt;li&gt;Session persistence → the hook exposes &lt;code&gt;messages&lt;/code&gt;, you decide where to store&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those turns a demo into a product, and each one is where your particular use case matters more than a shared abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full write-up
&lt;/h2&gt;

&lt;p&gt;Project decisions, human-vs-AI split, and the security checklist I run before every proxy change are in &lt;a href="https://github.com/GronskiDeveloper/claude-chat-react/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/a&gt; in the repo. Or the &lt;a href="https://dev.to/gronskideveloper/how-i-document-my-ai-first-workflow-in-every-public-repo-4l0h"&gt;broader post&lt;/a&gt; about how I document AI-first workflow across all six of my repos.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm &lt;a href="https://grodev.pl" rel="noopener noreferrer"&gt;Dominik Groński / GroDev&lt;/a&gt; — a new studio in Poznań, Poland (JDG since May 2026), available for first paid deployments. This library is MIT.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>claude</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I document my AI-first workflow in every public repo</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:10:46 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/how-i-document-my-ai-first-workflow-in-every-public-repo-4l0h</link>
      <guid>https://dev.to/gronskideveloper/how-i-document-my-ai-first-workflow-in-every-public-repo-4l0h</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In every one of my public repos I keep three artifacts that make my AI-first workflow visible from the file tree, not just claimed in a README:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/strong&gt; — a literal table of "who did what" (human vs AI) per layer of the project, plus a list of things I &lt;strong&gt;rejected&lt;/strong&gt; from the AI's draft, plus "known gotchas for the next AI pass."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.claude/commands/&lt;/code&gt;&lt;/strong&gt; — one slash command per repo, tuned to the most expensive failure mode (security review, math invariants, WebGL disposal, hook chain audit, React contract).&lt;/li&gt;
&lt;li&gt;For one representative repo — a full &lt;strong&gt;&lt;code&gt;CASE_STUDY.md&lt;/code&gt;&lt;/strong&gt;: 90-minute build retrospective, step by step, threat model → SDK lookup → AI draft → line-by-line audit → hardening → in-browser verification → docs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Decision authority (architecture, security posture, API contract, cost engineering, what to &lt;em&gt;reject&lt;/em&gt;) stays with the human. AI gets boilerplate and first drafts. This post shows the how, with links to five working repos as reference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why publish this at all
&lt;/h2&gt;

&lt;p&gt;Because AI-augmented work isn't worth hiding. Developers who cover it up today will be the ones saying "I don't do computers" in two years. Companies hiring devs already write "AI-first (Claude, Copilot, Cursor)" straight into job posts — and they want to see &lt;strong&gt;evidence&lt;/strong&gt;, not statements.&lt;/p&gt;

&lt;p&gt;But "evidence of AI-first work" isn't "I have ChatGPT open in a tab." It's &lt;strong&gt;artifacts in the file tree&lt;/strong&gt;. This post shows three concrete ones I've deployed across every public repo I own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact 1: &lt;code&gt;CLAUDE.md&lt;/code&gt; with a human/AI split
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is the file Claude Code reads as project context (analogous to &lt;code&gt;.cursorrules&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt;). But I use it for &lt;strong&gt;two audiences at once&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;For the next AI pass&lt;/strong&gt; — so the model knows what's sacred and what's flexible in this repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For humans&lt;/strong&gt; — so anyone browsing the file tree (a client, a recruiter, a teammate) sees, in black and white: &lt;strong&gt;who did what&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The table shape I use in every repo:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Who did it&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Threat model &amp;amp; architecture (proxy, not client-side call)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Human&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The whole reason this repo exists. Delegating "should the API key touch the browser?" to an AI is how you ship a leaked-key incident. Not negotiable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side proxy skeleton&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AI-drafted, human-reviewed line by line&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude wrote the first pass; I audited every line for four things: (1) key never in response headers or logs, (2) &lt;code&gt;role&lt;/code&gt; validated, not trusted, (3) input length capped, (4) SSE framing correct.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widget UI (vanilla JS, ~6 KB)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AI-drafted, human-styled&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dependency-free was a hard constraint from me. &lt;code&gt;textContent&lt;/code&gt; (not &lt;code&gt;innerHTML&lt;/code&gt;) on user text is mine — XSS prevention isn't a call to leave to the model's discretion.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt caching decision&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Human&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cost engineering — the difference between $10/mo and $1/mo of API spend.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things make this useful and not fluff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "Why" column&lt;/strong&gt; is the part most people skip. It's also the only place you can tell the author understood &lt;em&gt;why&lt;/em&gt; they delegated (or didn't).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A "What I rejected from the AI's draft" section&lt;/strong&gt; — I write out, in plain text, what Claude proposed, what I refused to merge, and why. This is what separates "I use AI" from "I use AI well." An example from one of my repos:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Rejected two things Claude proposed initially:&lt;br&gt;
(1) logging the request body — would log user PII to a plain file. Removed.&lt;br&gt;
(2) falling back to &lt;code&gt;$_ENV['ANTHROPIC_API_KEY']&lt;/code&gt; if &lt;code&gt;getenv()&lt;/code&gt; returned false — unnecessary, and on some hosting stacks it could pick up stale values. Removed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A "Known gotchas for the next AI pass" section&lt;/strong&gt; — spots where the next iteration of AI would do something dumb. This protects &lt;em&gt;both&lt;/em&gt; me and the model — because Claude reads this file as context, so the next round it won't re-propose an idea that's already been rejected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Working examples:&lt;br&gt;
&lt;a href="https://github.com/GronskiDeveloper/claude-chat-widget/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; in claude-chat-widget&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://github.com/GronskiDeveloper/claude-chat-react/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;claude-chat-react&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://github.com/GronskiDeveloper/booking-slots-php/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;booking-slots-php&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://github.com/GronskiDeveloper/woocommerce-custom-product-data/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;woocommerce-custom-product-data&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://github.com/GronskiDeveloper/threejs-product-configurator-starter/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;threejs-product-configurator-starter&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact 2: A case study — one build, step by step
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; tells you &lt;strong&gt;what&lt;/strong&gt; someone did. A case study tells you &lt;strong&gt;how&lt;/strong&gt; and &lt;strong&gt;in what order&lt;/strong&gt;. Two different documents, both needed.&lt;/p&gt;

&lt;p&gt;The case study I wrote for &lt;code&gt;claude-chat-widget&lt;/code&gt; has eight sections, each one concrete step of a 90-minute build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Threat model and architecture&lt;/strong&gt; (human, before writing a single line of code)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Looking up the exact SDK shape&lt;/strong&gt; (human, from the &lt;code&gt;claude-api&lt;/code&gt; skill) — because the model's training data may be stale for SDK APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI drafts the proxy skeleton&lt;/strong&gt; (Claude, ~10 min)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line-by-line audit + hardening&lt;/strong&gt; (human, ~20 min) — the most important section&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI drafts the widget&lt;/strong&gt; (Claude, ~10 min)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widget audit + hardening&lt;/strong&gt; (human, ~15 min) — XSS, dark mode, mobile, UX&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification in a real browser&lt;/strong&gt; (human, ~10 min)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; (human, ~15 min) — because LLMs write generic READMEs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This format has one unexpected side effect: &lt;strong&gt;it forces honesty&lt;/strong&gt;. When you write "AI wrote 60% of the lines, human made every load-bearing decision" — you have to say &lt;em&gt;which&lt;/em&gt; decisions those were. Suddenly it's visible where you actually did the work, versus where you just signed off on a generated file.&lt;/p&gt;

&lt;p&gt;Full case study: &lt;a href="https://github.com/GronskiDeveloper/claude-chat-widget/blob/main/CASE_STUDY.md" rel="noopener noreferrer"&gt;&lt;code&gt;CASE_STUDY.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact 3: &lt;code&gt;.claude/commands/&lt;/code&gt; — automating the review
&lt;/h2&gt;

&lt;p&gt;Claude Code (and most agents) support &lt;strong&gt;slash commands&lt;/strong&gt; — Markdown files under &lt;code&gt;.claude/commands/&lt;/code&gt; invoked in a session as &lt;code&gt;/name&lt;/code&gt;. They load as part of the prompt, so I can save a checklist and pull it up with one word.&lt;/p&gt;

&lt;p&gt;Every repo I own has &lt;strong&gt;one&lt;/strong&gt; such file, tuned to the class of failure I most want to catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/GronskiDeveloper/claude-chat-widget/blob/main/.claude/commands/security-review.md" rel="noopener noreferrer"&gt;&lt;code&gt;security-review.md&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;claude-chat-widget&lt;/code&gt;) — 6-invariant audit of the proxy's security posture (key only in &lt;code&gt;Anthropic\Client(apiKey:)&lt;/code&gt;, &lt;code&gt;role&lt;/code&gt; validated, length capped, &lt;code&gt;X-Accel-Buffering: no&lt;/code&gt;, etc.). Runs before every merge to &lt;code&gt;server/chat.php&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/GronskiDeveloper/claude-chat-react/blob/main/.claude/commands/react-security-review.md" rel="noopener noreferrer"&gt;&lt;code&gt;react-security-review.md&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;claude-chat-react&lt;/code&gt;) — 8-invariant audit for the React version (no &lt;code&gt;@anthropic-ai/sdk&lt;/code&gt; import in &lt;code&gt;src/&lt;/code&gt;, no &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;, &lt;code&gt;setMessages(prev =&amp;gt; ...)&lt;/code&gt; never closure-based, &lt;code&gt;AbortController&lt;/code&gt; in a ref not state, &lt;code&gt;react&lt;/code&gt; in peerDependencies, a11y attributes preserved).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/GronskiDeveloper/booking-slots-php/blob/main/.claude/commands/verify-slot-math.md" rel="noopener noreferrer"&gt;&lt;code&gt;verify-slot-math.md&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;booking-slots-php&lt;/code&gt;) — audit of the overlap math (half-open intervals &lt;code&gt;[start, end)&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt; not &lt;code&gt;&amp;lt;=&lt;/code&gt;, symmetric buffer). Because booking bugs come from off-by-one errors right there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/GronskiDeveloper/woocommerce-custom-product-data/blob/main/.claude/commands/hook-audit.md" rel="noopener noreferrer"&gt;&lt;code&gt;hook-audit.md&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;woocommerce-custom-product-data&lt;/code&gt;) — audit of the three-hook WooCommerce chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/GronskiDeveloper/threejs-product-configurator-starter/blob/main/.claude/commands/webgl-review.md" rel="noopener noreferrer"&gt;&lt;code&gt;webgl-review.md&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;threejs-product-configurator-starter&lt;/code&gt;) — geometry disposal (so WebGL doesn't leak) and "no build step" invariant (so nobody sneaks Vite in).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The effect: &lt;strong&gt;repeatable code review that doesn't depend on my discipline on a given day&lt;/strong&gt;. Instead of remembering all six points every time I touch the proxy, I type &lt;code&gt;/security-review&lt;/code&gt; and get the checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these three artifacts
&lt;/h2&gt;

&lt;p&gt;Because they cover the three levels of question anyone evaluating your AI-first work actually asks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;The artifact that answers it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"What specifically did you do vs the AI?"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt; — one-screen table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What does your process look like — order of decisions, what you reject from AI drafts?"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CASE_STUDY.md&lt;/code&gt; — one build retrospective&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Can you configure agents, or do you just type in a chat?"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.claude/commands/&lt;/code&gt; — config tuned to a specific failure class&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A recruiter reviewing 40 GitHub profiles a week isn't going to read your code. They &lt;em&gt;are&lt;/em&gt; going to scan your file tree. If they see &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;CASE_STUDY.md&lt;/code&gt;, and &lt;code&gt;.claude/commands/&lt;/code&gt; there — they immediately know they're looking at someone who &lt;strong&gt;actually practices&lt;/strong&gt; the workflow, not someone who put it in their CV.&lt;/p&gt;

&lt;h2&gt;
  
  
  The philosophy I'm trying to make routine
&lt;/h2&gt;

&lt;p&gt;AI is a &lt;strong&gt;fast typist and a reasonable reviewer&lt;/strong&gt;. Decision authority — architecture, threat model, validation strategy, dependency selection, what to &lt;strong&gt;reject&lt;/strong&gt; — stays with the human. When I hand decision authority to a model, I get a repo that lints clean and blows up in production.&lt;/p&gt;

&lt;p&gt;This philosophy works well enough that I shipped 5 working public repos (a WordPress plugin, a PHP booking-slots engine, a Three.js configurator starter, a vanilla-JS chat widget, and a React chat widget on Claude API) — each with a full &lt;code&gt;CLAUDE.md&lt;/code&gt;, docs, badges, and a pinned &lt;code&gt;v1.0.0&lt;/code&gt; release. Every load-bearing decision documented, every AI draft audited before merge.&lt;/p&gt;

&lt;p&gt;Full profile: &lt;a href="https://github.com/GronskiDeveloper" rel="noopener noreferrer"&gt;github.com/GronskiDeveloper&lt;/a&gt;. Fork any repo and check what &lt;code&gt;CLAUDE.md&lt;/code&gt; looks like in practice.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build websites, e-commerce, custom 3D configurators and AI assistants — see &lt;a href="https://grodev.pl" rel="noopener noreferrer"&gt;grodev.pl&lt;/a&gt; or &lt;a href="https://grodev.pl/ai" rel="noopener noreferrer"&gt;grodev.pl/ai&lt;/a&gt;. Poznań, Poland, remote across PL and EU.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aifirst</category>
      <category>claude</category>
      <category>workflow</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a 3D Product Configurator in Three.js — Lessons From 9 Client Deployments</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Wed, 05 Aug 2026 06:47:15 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/building-a-3d-product-configurator-in-threejs-lessons-from-9-client-deployments-3c90</link>
      <guid>https://dev.to/gronskideveloper/building-a-3d-product-configurator-in-threejs-lessons-from-9-client-deployments-3c90</guid>
      <description>&lt;p&gt;Over the last year I shipped &lt;strong&gt;9 production 3D configurators&lt;/strong&gt; for polish manufacturers — pools, garage doors, saunas, pergolas, greenhouses, packaging, decorative lamps, terrace roofs, and light-boxes. Each one runs live on its own subdomain of my studio at &lt;a href="https://grodev.pl" rel="noopener noreferrer"&gt;grodev.pl&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Some of the lessons were obvious in hindsight. Some cost me a weekend of debugging. Sharing the non-obvious ones here.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Draco compression is not optional for CAD-heavy models
&lt;/h2&gt;

&lt;p&gt;Manufacturers send you &lt;strong&gt;STEP or SolidWorks files exported to glTF&lt;/strong&gt;. Raw output is 40–120 MB per variant. On 4G mobile that's a 20-second load with an empty white canvas.&lt;/p&gt;

&lt;p&gt;Draco compression brings that to 2–5 MB with &lt;strong&gt;no visible quality loss&lt;/strong&gt; on product shots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GLTFLoader&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;three/examples/jsm/loaders/GLTFLoader.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DRACOLoader&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;three/examples/jsm/loaders/DRACOLoader.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dracoLoader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DRACOLoader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;dracoLoader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDecoderPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/draco/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// self-hosted, don't use CDN&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GLTFLoader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDRACOLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dracoLoader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/models/pool-3.5m.glb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gltf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gltf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Self-host the decoder — Google's CDN version added ~600 ms to first paint in my measurements. Copy &lt;code&gt;node_modules/three/examples/jsm/libs/draco/&lt;/code&gt; to your &lt;code&gt;public/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tooling:&lt;/strong&gt; &lt;code&gt;gltf-pipeline -i model.glb -o model.draco.glb --draco.compressionLevel 10&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Instancing beats individual meshes past ~200 objects
&lt;/h2&gt;

&lt;p&gt;A pergola with 40 louvres × 3 tilt positions × user color picker = 120 meshes updating on every frame. Naive approach tanks FPS to 12 on mid-range phones.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;InstancedMesh&lt;/code&gt; batches identical geometry into one draw call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;geo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BoxGeometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MeshStandardMaterial&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;louvres&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InstancedMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dummy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object3D&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userTilt&lt;/span&gt;  &lt;span class="c1"&gt;// update per frame is fine&lt;/span&gt;
  &lt;span class="nx"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMatrix&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;louvres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMatrixAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;louvres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instanceMatrix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needsUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;louvres&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same for basen tiles, brama slats, sauna wall boards. &lt;strong&gt;One material change updates all instances.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Ship the price calculator to the server, not the client
&lt;/h2&gt;

&lt;p&gt;Every configurator eventually needs a "Show price" button. Tempting to compute client-side — you already have all the state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't.&lt;/strong&gt; Manufacturers change prices monthly. Every hardcoded PHP-in-JS multiplication is a redeploy. Also: users open DevTools.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Only what changed goes over the wire&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pool-3.5m&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granite-grey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lighting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deliveryWeeks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/quote&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Server (Laravel in my case) hits a &lt;code&gt;pricing_rules&lt;/code&gt; table with monthly-updated coefficients. Client only knows what a valid config looks like, never the pricing logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus:&lt;/strong&gt; the same endpoint powers the "email me a PDF quote" flow. Zero duplication.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Mobile is your target device, not a "nice to have"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;70% of my configurator traffic is mobile.&lt;/strong&gt; Sauna buyers browse on the couch, pool buyers browse at the site. Any deployment that assumes desktop-first will feel broken.&lt;/p&gt;

&lt;p&gt;Concrete mobile survival kit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))&lt;/code&gt; — clamping to 2 saves ~40% GPU on retina phones&lt;/li&gt;
&lt;li&gt;Detect &lt;code&gt;WEBGL_lose_context&lt;/code&gt; and reload cleanly when the browser tab is backgrounded on iOS&lt;/li&gt;
&lt;li&gt;Preload one hero material, lazy-load the rest — first paint at 400 ms feels instant even if full swatch library is still coming&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. The integration is 60% of the project
&lt;/h2&gt;

&lt;p&gt;Building the WebGL viewer takes 2–3 weeks. Making it actually feed the manufacturer's WooCommerce / CRM / ERP takes &lt;strong&gt;another 4–6 weeks&lt;/strong&gt; — and the client only sees the shiny part.&lt;/p&gt;

&lt;p&gt;Budget accordingly. I now quote &lt;strong&gt;12–55k PLN&lt;/strong&gt; depending on catalog depth and integration surface, not per screen.&lt;/p&gt;




&lt;p&gt;Live examples if you want to see any of these techniques in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://basen3d.grodev.pl" rel="noopener noreferrer"&gt;basen3d.grodev.pl&lt;/a&gt;&lt;/strong&gt; — pool configurator (Draco + PWA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pergola3d.grodev.pl" rel="noopener noreferrer"&gt;pergola3d.grodev.pl&lt;/a&gt;&lt;/strong&gt; — bioclimatic pergola (InstancedMesh louvres)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://lampy3d.grodev.pl" rel="noopener noreferrer"&gt;lampy3d.grodev.pl&lt;/a&gt;&lt;/strong&gt; — decorative lamp (white-label)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full studio at &lt;strong&gt;&lt;a href="https://grodev.pl" rel="noopener noreferrer"&gt;grodev.pl&lt;/a&gt;&lt;/strong&gt; — happy to chat if you're integrating similar for a manufacturer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What tripped you up on your first Three.js production project? Reply — always curious about other people's disaster stories.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webgl</category>
      <category>javascript</category>
      <category>ecommerce</category>
    </item>
  </channel>
</rss>
