<?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: Constanza Diaz</title>
    <description>The latest articles on DEV Community by Constanza Diaz (@constanza_diaz_dev).</description>
    <link>https://dev.to/constanza_diaz_dev</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3538347%2Ff355ba65-0762-4616-aa4e-1800706b4f12.png</url>
      <title>DEV Community: Constanza Diaz</title>
      <link>https://dev.to/constanza_diaz_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/constanza_diaz_dev"/>
    <language>en</language>
    <item>
      <title>How I Built My Design System from a Color Palette (with Claude Code and tweakcn)</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Mon, 08 Jun 2026 14:47:23 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/how-i-built-my-design-system-from-a-color-palette-with-claude-code-and-tweakcn-4gmh</link>
      <guid>https://dev.to/constanza_diaz_dev/how-i-built-my-design-system-from-a-color-palette-with-claude-code-and-tweakcn-4gmh</guid>
      <description>&lt;p&gt;Building a coherent and attractive design system is one of those problems that seems simple until you actually sit down to do it. Colors that look great in isolation but clash together, inconsistently named tokens, CSS variables nobody understands three weeks later... This post documents how I solved it by combining Claude Code with tweakcn, a tool I'd never heard of that completely changed my workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point: I had colors, not a system
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4p5r9amax8uoktz8n1i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4p5r9amax8uoktz8n1i.png" alt="Image of the social media posts" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It all started with a palette. I had my brand colors well defined — primaries, secondaries, neutrals — but the way I'd organized them in code wasn't working for me. I'd set them up directly in Claude Code and, while it functioned, the visual result wasn't what I was after.&lt;br&gt;
The problem wasn't the code itself. It was that I was making design decisions inside a text environment, with no immediate visual feedback. Deciding whether primary-600 should be the hover color or the active color is nearly impossible without seeing it in context.&lt;/p&gt;

&lt;p&gt;I got this types of previews from Claude, but I didn't feel it looked good enough, and iterating was not great because I wasn't sure about what I wanted exactly, and it would take up a lot of time and AI tokens...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flo97o879ew7nrug6kzrh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flo97o879ew7nrug6kzrh.png" alt=" " width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering tweakcn
&lt;/h2&gt;

&lt;p&gt;While looking for tools to visualize design tokens, I found tweakcn.com/editor/theme. It's a visual theme editor for Tailwind/shadcn that lets you:&lt;/p&gt;

&lt;p&gt;Import your existing configuration (CSS variables, Tailwind config)&lt;br&gt;
Edit visually with real-time preview on actual components&lt;br&gt;
Export the result as production-ready code&lt;/p&gt;

&lt;p&gt;What makes it particularly useful for developers is that the output isn't just a pretty palette — it's semantic tokens applied to real components. You can immediately see how your destructive color looks on a button, how readable your muted-foreground is as secondary text, or whether the contrast between your card and background is sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export from Claude Code
The first step was getting my existing configuration into a format tweakcn could read. I exported my CSS custom properties as they were:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkcoomlf1d0j3oxhlko3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkcoomlf1d0j3oxhlko3.png" alt=" " width="800" height="726"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Import into tweakcn
In the tweakcn editor I used the import option to paste my configuration. The editor automatically maps your variables to its semantic token system (background, foreground, primary, secondary, muted, accent, destructive, border, ring...).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fetttp0eeeor4k0zogqfn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fetttp0eeeor4k0zogqfn.png" alt=" " width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Adjust visually
This is where the workflow shifts dramatically compared to editing CSS by hand. You can:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Modify the hue, saturation and lightness of each token with sliders&lt;br&gt;
See the change applied in real time across a set of reference components (buttons, cards, inputs, badges, alerts...)&lt;br&gt;
Check foreground/background pairs for readability&lt;br&gt;
Toggle between light and dark mode to make sure both themes are coherent&lt;/p&gt;

&lt;p&gt;The adjustments I made were mostly around the lightness values in dark mode — my original setup was too saturated — and fine-tuning the accent color so it had enough contrast with primary without competing with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8dcl78itdw9se3zlblci.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8dcl78itdw9se3zlblci.png" alt=" " width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Export the result
Once satisfied, tweakcn generates a CSS variables block ready to copy. The output includes both the light and dark themes, with values in HSL format (which makes it trivial to adjust lightness programmatically later if you need to).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy17vau5djnu2uadny5me.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy17vau5djnu2uadny5me.png" alt=" " width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to Claude Code
&lt;/h2&gt;

&lt;p&gt;With the exported CSS in hand, I went back to Claude Code to integrate it into the project. The process was straightforward: import the variables and let Claude reorganize the token architecture to be consistent with the rest of the codebase.&lt;br&gt;
The result was a complete semantic palette:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8x2erh6x2msnaxn4nm0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8x2erh6x2msnaxn4nm0.png" alt=" " width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I value most about the final result is that the tokens have semantic meaning, not just color values. The difference between having --color-blue-500 and having --primary is enormous when it comes to maintaining the system: if you rebrand, you change the value of --primary in one place, not forty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway: why this workflow works
&lt;/h2&gt;

&lt;p&gt;The Claude Code + tweakcn combination covers two distinct needs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbk3v72m1xmtufiq1k7hy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbk3v72m1xmtufiq1k7hy.png" alt=" " width="800" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code is excellent at generating architecture, naming tokens consistently, and writing CSS boilerplate. But it can't give you visual feedback. tweakcn does exactly that — it puts your colors in context on real components — but it doesn't manage your codebase.&lt;br&gt;
Using them together eliminates the core problem of defining design systems in text: making visual decisions blind.&lt;/p&gt;

&lt;p&gt;Resources&lt;/p&gt;

&lt;p&gt;tweakcn editor — the theme editor I used&lt;br&gt;
shadcn/ui theming docs — documentation on the token system tweakcn is based on&lt;br&gt;
Claude Code — the terminal agent I used for the codebase&lt;/p&gt;

&lt;p&gt;Do you have a different approach to managing design tokens? I'd love to know what tools you use — drop it in the comments.&lt;/p&gt;

</description>
      <category>designsystem</category>
      <category>ui</category>
    </item>
    <item>
      <title>Multilanguage good practices.</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Mon, 08 Jun 2026 14:11:13 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/multilanguage-good-practices-3io1</link>
      <guid>https://dev.to/constanza_diaz_dev/multilanguage-good-practices-3io1</guid>
      <description>&lt;h2&gt;
  
  
  My i18n Setup Was Right. Until It Wasn't.
&lt;/h2&gt;

&lt;p&gt;Best practices have an expiry date. They expire silently.&lt;/p&gt;

&lt;p&gt;I'm building a 4-language landing page for a client — Catalan, Spanish,&lt;br&gt;
  English, French. Before I wrote a single page, I asked Claude what the&lt;br&gt;
  cleanest way to do i18n in Astro was. The answer was solid. I wrote it into&lt;br&gt;
   the specs. The scaffold worked.&lt;/p&gt;

&lt;p&gt;Until I added a second page. This is the story of how I almost shipped a&lt;br&gt;
  maintenance nightmare without ever breaking a single rule from my own&lt;br&gt;
  specs.&lt;/p&gt;

&lt;p&gt;The setup&lt;/p&gt;

&lt;p&gt;The original recommendation was straightforward: one file per language.&lt;br&gt;
  Astro had recently stabilized native i18n support — the i18n: {&lt;br&gt;
  defaultLocale, locales, routing } block in astro.config.mjs — and the&lt;br&gt;
  simplest pattern paired that config with a directory tree like:&lt;/p&gt;

&lt;p&gt;src/pages/&lt;br&gt;
  ├── index.astro       # Catalan (default)&lt;br&gt;
  ├── es/index.astro    # Spanish&lt;br&gt;
  ├── en/index.astro    # English&lt;br&gt;
  └── fr/index.astro    # French&lt;/p&gt;

&lt;p&gt;Four files. One per language. Each file pulled its strings from a shared&lt;br&gt;
  src/i18n/ui.ts catalogue, so the content was DRY even if the structure&lt;br&gt;
  wasn't. This worked. It went into the specs as the canonical pattern.&lt;/p&gt;

&lt;p&gt;The growth&lt;/p&gt;

&lt;p&gt;Months in, the project needed two more pages: /gallery and /press. I did&lt;br&gt;
  what any disciplined builder does — I followed the existing pattern. Two&lt;br&gt;
  new pages. Eight new files.&lt;/p&gt;

&lt;p&gt;Something started to feel off, but I couldn't name it. The specs said "one&lt;br&gt;
  file per language." I was following the specs.&lt;/p&gt;

&lt;p&gt;The catch&lt;/p&gt;

&lt;p&gt;It clicked during a review with Claude. I asked, almost as a sanity check:&lt;br&gt;
  "we made four versions with the text hardcoded in each language?" — and as&lt;br&gt;
  I typed the question, I heard how absurd it sounded.&lt;/p&gt;

&lt;p&gt;We had four copies of every page. The structure was duplicated. The text&lt;br&gt;
  was duplicated. Every bug would be quadrupled. And worse: when a file lives&lt;br&gt;
   in an es/ folder, the brain treats it as "the Spanish version" — so you&lt;br&gt;
  start writing Spanish strings directly into the file, bypassing the very&lt;br&gt;
  i18n catalogue you built. The pattern itself was inviting the regression.&lt;/p&gt;

&lt;p&gt;That was the symptom. The cause was deeper: the per-language file pattern&lt;br&gt;
  scales the wrong axis. Add a new page and your surface multiplies by N&lt;br&gt;
  (where N is the number of languages). The structure and the language are no&lt;br&gt;
   longer orthogonal.&lt;/p&gt;

&lt;p&gt;The refactor&lt;/p&gt;

&lt;p&gt;Astro has a feature the original setup hadn't been using: dynamic routes&lt;br&gt;
  via [lang]. One file generates a route for each parameter, statically, at&lt;br&gt;
  build time:&lt;/p&gt;

&lt;p&gt;src/pages/&lt;br&gt;
  ├── index.astro          # / (Catalan, default)&lt;br&gt;
  ├── gallery.astro        # /gallery (Catalan)&lt;br&gt;
  ├── press.astro          # /press (Catalan)&lt;br&gt;
  └── [lang]/&lt;br&gt;
      ├── index.astro      # /es, /en, /fr&lt;br&gt;
      ├── gallery.astro    # /es/gallery, /en/gallery, /fr/gallery&lt;br&gt;
      └── press.astro      # /es/press, /en/press, /fr/press&lt;/p&gt;

&lt;p&gt;Six files instead of thirteen. A 54% reduction in surface area. The&lt;br&gt;
  structure lives in one place per page. The language varies via the route&lt;br&gt;
  param. The catalogue handles the strings. Each axis is finally orthogonal.&lt;/p&gt;

&lt;p&gt;The refactor itself took about an hour: getStaticPaths per file, swap&lt;br&gt;
  useTranslations('es') for useTranslations(Astro.params.lang), rewrite the&lt;br&gt;
  language switcher to do a URL-prefix swap.&lt;/p&gt;

&lt;p&gt;Why this matters&lt;/p&gt;

&lt;p&gt;I wasn't violating my specs. The specs were the trap. They encoded the&lt;br&gt;
  assumption that the project would stay at one or two pages — an assumption&lt;br&gt;
  that was true when I wrote them, and stopped being true the moment I added&lt;br&gt;
  the third.&lt;/p&gt;

&lt;p&gt;Specs are snapshots. They capture what you knew when you wrote them. A spec&lt;br&gt;
   that says "follow pattern X" is silently dependent on the conditions that&lt;br&gt;
  made X a good idea in the first place. When those conditions change, the&lt;br&gt;
  rewrite the language switcher to do a URL-prefix swap.&lt;/p&gt;

&lt;p&gt;Why this matters&lt;/p&gt;

&lt;p&gt;I wasn't violating my specs. The specs were the trap. They encoded the assumption that the project would stay at one or two pages — an&lt;br&gt;
  assumption that was true when I wrote them, and stopped being true the moment I added the third.&lt;/p&gt;

&lt;p&gt;Specs are snapshots. They capture what you knew when you wrote them. A spec that says "follow pattern X" is silently dependent on the&lt;br&gt;
  conditions that made X a good idea in the first place. When those conditions change, the spec stops protecting you and starts pushing&lt;br&gt;
  you toward the wrong answer.&lt;/p&gt;

&lt;p&gt;The takeaway isn't "review your specs every time you add a feature." That's exhausting and nobody does it. The takeaway is to listen to&lt;br&gt;
   friction. When you find yourself hardcoding strings, or copy-pasting structure, or saying "this is the third time I'm doing this" —&lt;br&gt;
  that's the signal that a pattern is past its expiry date.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;Best practices have an expiry date. They expire silently.&lt;/p&gt;

&lt;p&gt;The agent writes the code, the spec sets the pattern, but you're still the engineer. The friction in your hands is the only thing that&lt;br&gt;
  knows when something used to be right and isn't anymore.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>webdev</category>
      <category>goodpractices</category>
    </item>
    <item>
      <title>AI Pair Programming Isn't Autopilot: Scaffolding HandyFEM and Catching What the AI Threw Away</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Mon, 01 Jun 2026 20:26:20 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/ai-pair-programming-isnt-autopilot-scaffolding-handyfem-and-catching-what-the-ai-threw-away-5p6</link>
      <guid>https://dev.to/constanza_diaz_dev/ai-pair-programming-isnt-autopilot-scaffolding-handyfem-and-catching-what-the-ai-threw-away-5p6</guid>
      <description>&lt;h2&gt;
  
  
  The agent writes the code. You're still the engineer.
&lt;/h2&gt;

&lt;p&gt;I'm building HandyFEM with Claude Code as my pair. It's fast — sometimes startlingly so. But the way I work with it is deliberate: I treat everything it produces the way I'd treat a pull request from a capable junior developer. I read it. I question it. I decide what stays.&lt;/p&gt;

&lt;p&gt;This post is a concrete example of why that habit matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  The task: scaffolding the project
&lt;/h2&gt;

&lt;p&gt;Before writing features, you scaffold a project — generate its skeleton: folder structure, config files, a base page that runs. I had the agent set up the foundation for HandyFEM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js with TypeScript and Tailwind&lt;/li&gt;
&lt;li&gt;shadcn/ui — component code lives in your repo, so you own it&lt;/li&gt;
&lt;li&gt;Design tokens wired into the theme — exact color palette from my specs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because my project folder already had docs, Git, and a &lt;code&gt;.env.local&lt;/code&gt; with a real secret, the agent did the smart thing: generated the app in a temporary folder and integrated carefully, without clobbering my existing files.&lt;/p&gt;




&lt;h2&gt;
  
  
  The catch
&lt;/h2&gt;

&lt;p&gt;During the integration, the agent mentioned — almost in passing — that the Next.js generator had created its own &lt;code&gt;CLAUDE.md&lt;/code&gt; file, and that it had &lt;strong&gt;discarded it&lt;/strong&gt; so as not to overwrite mine.&lt;/p&gt;

&lt;p&gt;On the surface: correct behavior. But it raised a question I didn't want to skip. &lt;strong&gt;Did that discarded file contain anything useful?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I asked. We went back and looked. The generated &lt;code&gt;CLAUDE.md&lt;/code&gt; pointed to a second file — &lt;code&gt;AGENTS.md&lt;/code&gt; — and that one held something genuinely valuable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# This is NOT the Next.js you know&lt;/span&gt;

This version has breaking changes — APIs, conventions, and file structure may
all differ from your training data. Read the relevant guide in
node_modules/next/dist/docs/ before writing any code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real. The framework version I'm using is newer than most AI models were trained on. Losing this note meant the agent might later write code using outdated patterns — confidently, and wrongly.&lt;/p&gt;

&lt;p&gt;We rescued it into my project instructions. One small note, but it changes the quality of every future line of framework code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;The agent didn't do anything wrong. Discarding a file to protect mine was sensible. But the side effect — dropping context that mattered — was easy to miss, buried in a one-line aside during a much bigger operation.&lt;/p&gt;

&lt;p&gt;That's the pattern to internalize. AI agents make a high volume of fast, plausible decisions. Most are good. But "plausible" isn't "reviewed." The skill isn't prompting — it's &lt;strong&gt;reading the output like a reviewer&lt;/strong&gt;: what did it change, what did it remove, is each of those what I actually want?&lt;/p&gt;

&lt;p&gt;I don't review because the tool is bad. I review &lt;strong&gt;because it's good enough that I'd otherwise stop paying attention.&lt;/strong&gt; That's the trap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest reflections on the workflow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's working:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security before features.&lt;/strong&gt; Pre-commit hook, &lt;code&gt;.gitignore&lt;/code&gt;, environment variables — all set up before scaffolding. For a product whose whole premise is trust, that ordering is non-negotiable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detailed specs as source of truth.&lt;/strong&gt; The agent had real screens, components, and a color palette to build against — not an invitation to invent one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; with project conventions.&lt;/strong&gt; Mobile-first, accessibility minimums, never hardcode secrets. The agent defaults to my standards instead of generic ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I'd refine:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decide manual vs. automated before committing to a path.&lt;/strong&gt; Earlier I had the agent script a one-time setup task. It became a long debugging session. Doing it by hand would have been faster. Automation pays off when you repeat something — for a true one-off, manual is often smarter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take inventory before opening a new thread.&lt;/strong&gt; Some questions I treated as open were already answered in my own specs. Five minutes of review saves re-litigating settled decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The meta-lesson: with an AI agent, the bottleneck shifts. It's no longer writing the code — it's &lt;strong&gt;deciding well and reviewing carefully&lt;/strong&gt;. The typing got cheap. The judgment got more valuable, not less.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;An AI agent is a genuine force multiplier — but only for someone who stays in the driver's seat. It will move faster than you, make mostly-good calls, and occasionally drop something that matters in a sentence you could easily skim past.&lt;/p&gt;

&lt;p&gt;So don't skim. Read the diff. Ask "what did you remove, and why?"&lt;/p&gt;

&lt;p&gt;The agent writes the code. You're still the engineer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;#HandyFEMApp #BuildingInPublic #AI #ClaudeCode #WebDev&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>specsdriven</category>
      <category>ai</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Security by Design: Keeping API Tokens Out of Git with a 3-Layer Setup</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Mon, 01 Jun 2026 20:17:26 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/security-by-design-keeping-api-tokens-out-of-git-with-a-3-layer-setup-4cob</link>
      <guid>https://dev.to/constanza_diaz_dev/security-by-design-keeping-api-tokens-out-of-git-with-a-3-layer-setup-4cob</guid>
      <description>&lt;p&gt;When you build a product whose entire reason to exist is safety, security can't be something you bolt on later. It has to be a default — baked into the workflow from day one.&lt;/p&gt;

&lt;p&gt;So before any application code, I set up how my app handles secrets. This post walks through that setup: a deliberate, &lt;strong&gt;three-layer approach&lt;/strong&gt; that makes it structurally impossible for a token to end up in version control.&lt;/p&gt;

&lt;p&gt;The star of the show is a Git &lt;strong&gt;pre-commit hook&lt;/strong&gt;. I'll explain it from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Defense in depth
&lt;/h2&gt;

&lt;p&gt;No single control should be the only thing standing between you and a leak. Three layers, each catching what the previous one might miss:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/strong&gt; — prevention: keep secret-bearing files out of Git entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;pre-commit&lt;/code&gt; hook&lt;/strong&gt; — detection: scan every commit for secrets and block it if one slips through&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; — design: keep secrets out of the codebase in the first place&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Layer 1 — &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Environment variables
.env
.env.*
!.env.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;!.env.example&lt;/code&gt; exception keeps a template in the repo — a documented list of which variables are needed, with empty values. Anyone picking up the project knows exactly what to fill in without ever seeing a secret.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 2 — the &lt;code&gt;pre-commit&lt;/code&gt; hook
&lt;/h3&gt;

&lt;p&gt;A Git hook is a script Git runs automatically at a specific moment. A &lt;code&gt;pre-commit&lt;/code&gt; hook runs right before a commit is created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git commit
    │
    ▼
pre-commit hook runs   ← automatic
    │
    ├─ secret found?  → ❌ commit blocked
    └─ all clean?     → ✅ commit proceeds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mine scans staged files for patterns that match real credentials — Atlassian tokens, AWS keys, private keys. If it finds one, the commit is blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ACM&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="nv"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ATATT[A-Za-z0-9_=-]{16,}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}|-----BEGIN [A-Z ]*PRIVATE KEY-----'&lt;/span&gt;

&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git show &lt;span class="s2"&gt;":&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nEq&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$patterns&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ Possible secret in: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🛑 Commit blocked: secrets detected in staged files."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exit code is everything.&lt;/strong&gt; If a pre-commit hook exits non-zero, Git aborts the commit. That single &lt;code&gt;exit 1&lt;/code&gt; is what makes this real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I keep the hook in the repo.&lt;/strong&gt; Git's default hooks live in &lt;code&gt;.git/hooks/&lt;/code&gt;, which isn't versioned. I store mine in a tracked &lt;code&gt;.githooks/&lt;/code&gt; folder and point Git at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config core.hooksPath .githooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the hook travels with the project and gets reviewed like any other code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 3 — environment variables
&lt;/h3&gt;

&lt;p&gt;The first two layers stop secrets from being committed. The third makes sure they're not in the code to begin with:&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;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;apiToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JIRA_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing JIRA_API_TOKEN. Run with: node --env-file=.env.local script.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Node 20.6+, no &lt;code&gt;dotenv&lt;/code&gt; dependency needed — the built-in &lt;code&gt;--env-file&lt;/code&gt; flag loads your &lt;code&gt;.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.local script.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing it
&lt;/h2&gt;

&lt;p&gt;The best way to trust a safety net is to test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'const token = "ATATT3xFAKEFAKEFAKEFAKEFAKE123456"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; leak-test.js
git add leak-test.js
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt;
&lt;span class="c"&gt;# ❌ Possible secret in: leak-test.js&lt;/span&gt;
&lt;span class="c"&gt;# 🛑 Commit blocked: secrets detected in staged files.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commit never happens. That's the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Security works best when the tooling enforces the rules — not your memory. Three small pieces of configuration and a whole category of mistakes simply can't happen.&lt;/p&gt;

&lt;p&gt;For HandyFEM, where trust is the product, this wasn't over-engineering. It was the starting line.&lt;/p&gt;




&lt;p&gt;📚 &lt;strong&gt;HandyFEM App Series&lt;/strong&gt;&lt;br&gt;
🔗 Previous: &lt;a href="https://dev.to/constanza_diaz_dev/building-handyfems-design-system-with-claudeai-specs-components-and-visual-previews-2mk8"&gt;From Specs to Tickets: Automating Jira Setup with Node.js&lt;/a&gt;&lt;br&gt;
🔗 Next: Coming soon — Building the Design System in Code with Claude Code&lt;/p&gt;

&lt;h2&gt;
  
  
  🏷️ All posts in this series: #HandyFEMApp
&lt;/h2&gt;

&lt;p&gt;*Follow the build: #HandyFEMApp *&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>git</category>
      <category>devops</category>
    </item>
    <item>
      <title>Security by Design: Keeping API Tokens Out of Git with a 3-Layer Setup</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Mon, 01 Jun 2026 14:47:33 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/security-by-design-keeping-api-tokens-out-of-git-with-a-3-layer-setup-5ake</link>
      <guid>https://dev.to/constanza_diaz_dev/security-by-design-keeping-api-tokens-out-of-git-with-a-3-layer-setup-5ake</guid>
      <description>&lt;p&gt;When you build a product whose entire reason to exist is safety, security can't be something you bolt on later. It has to be a default — baked into the workflow from day one.&lt;/p&gt;

&lt;p&gt;So before any application code, I set up how my app handles secrets. This post walks through that setup: a deliberate, &lt;strong&gt;three-layer approach&lt;/strong&gt; that makes it structurally impossible for a token to end up in version control.&lt;/p&gt;

&lt;p&gt;The star of the show is a Git &lt;strong&gt;pre-commit hook&lt;/strong&gt;. I'll explain it from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Defense in depth
&lt;/h2&gt;

&lt;p&gt;No single control should be the only thing standing between you and a leak. Three layers, each catching what the previous one might miss:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/strong&gt; — prevention: keep secret-bearing files out of Git entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;pre-commit&lt;/code&gt; hook&lt;/strong&gt; — detection: scan every commit for secrets and block it if one slips through&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; — design: keep secrets out of the codebase in the first place&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Layer 1 — &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Environment variables
.env
.env.*
!.env.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;!.env.example&lt;/code&gt; exception keeps a template in the repo — a documented list of which variables are needed, with empty values. Anyone picking up the project knows exactly what to fill in without ever seeing a secret.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 2 — the &lt;code&gt;pre-commit&lt;/code&gt; hook
&lt;/h3&gt;

&lt;p&gt;A Git hook is a script Git runs automatically at a specific moment. A &lt;code&gt;pre-commit&lt;/code&gt; hook runs right before a commit is created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git commit
    │
    ▼
pre-commit hook runs   ← automatic
    │
    ├─ secret found?  → ❌ commit blocked
    └─ all clean?     → ✅ commit proceeds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mine scans staged files for patterns that match real credentials — Atlassian tokens, AWS keys, private keys. If it finds one, the commit is blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ACM&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="nv"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ATATT[A-Za-z0-9_=-]{16,}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}|-----BEGIN [A-Z ]*PRIVATE KEY-----'&lt;/span&gt;

&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git show &lt;span class="s2"&gt;":&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nEq&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$patterns&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ Possible secret in: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🛑 Commit blocked: secrets detected in staged files."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exit code is everything.&lt;/strong&gt; If a pre-commit hook exits non-zero, Git aborts the commit. That single &lt;code&gt;exit 1&lt;/code&gt; is what makes this real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I keep the hook in the repo.&lt;/strong&gt; Git's default hooks live in &lt;code&gt;.git/hooks/&lt;/code&gt;, which isn't versioned. I store mine in a tracked &lt;code&gt;.githooks/&lt;/code&gt; folder and point Git at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config core.hooksPath .githooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the hook travels with the project and gets reviewed like any other code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 3 — environment variables
&lt;/h3&gt;

&lt;p&gt;The first two layers stop secrets from being committed. The third makes sure they're not in the code to begin with:&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;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;apiToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JIRA_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing JIRA_API_TOKEN. Run with: node --env-file=.env.local script.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Node 20.6+, no &lt;code&gt;dotenv&lt;/code&gt; dependency needed — the built-in &lt;code&gt;--env-file&lt;/code&gt; flag loads your &lt;code&gt;.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.local script.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing it
&lt;/h2&gt;

&lt;p&gt;The best way to trust a safety net is to test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'const token = "ATATT3xFAKEFAKEFAKEFAKEFAKE123456"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; leak-test.js
git add leak-test.js
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt;
&lt;span class="c"&gt;# ❌ Possible secret in: leak-test.js&lt;/span&gt;
&lt;span class="c"&gt;# 🛑 Commit blocked: secrets detected in staged files.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commit never happens. That's the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Security works best when the tooling enforces the rules — not your memory. Three small pieces of configuration and a whole category of mistakes simply can't happen.&lt;/p&gt;

&lt;p&gt;For HandyFEM, where trust is the product, this wasn't over-engineering. It was the starting line.&lt;/p&gt;




&lt;p&gt;📚 &lt;strong&gt;HandyFEM App Series&lt;/strong&gt;&lt;br&gt;
🔗 Previous: &lt;a href="https://dev.to/constanza_diaz_dev/building-handyfems-design-system-with-claudeai-specs-components-and-visual-previews-2mk8"&gt;From Specs to Tickets: Automating Jira Setup with Node.js&lt;/a&gt;&lt;br&gt;
🔗 Next: Coming soon — Building the Design System in Code with Claude Code&lt;/p&gt;

&lt;h2&gt;
  
  
  🏷️ All posts in this series: #HandyFEMApp
&lt;/h2&gt;

&lt;p&gt;*Follow the build: #HandyFEMApp *&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>git</category>
      <category>devops</category>
    </item>
    <item>
      <title>Security by Design: Keeping API Tokens Out of Git with a 3-Layer Setup</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Mon, 01 Jun 2026 14:47:33 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/security-by-design-keeping-api-tokens-out-of-git-with-a-3-layer-setup-33a9</link>
      <guid>https://dev.to/constanza_diaz_dev/security-by-design-keeping-api-tokens-out-of-git-with-a-3-layer-setup-33a9</guid>
      <description>&lt;p&gt;When you build a product whose entire reason to exist is safety, security can't be something you bolt on later. It has to be a default — baked into the workflow from day one.&lt;/p&gt;

&lt;p&gt;So before any application code, I set up how my app handles secrets. This post walks through that setup: a deliberate, &lt;strong&gt;three-layer approach&lt;/strong&gt; that makes it structurally impossible for a token to end up in version control.&lt;/p&gt;

&lt;p&gt;The star of the show is a Git &lt;strong&gt;pre-commit hook&lt;/strong&gt;. I'll explain it from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Defense in depth
&lt;/h2&gt;

&lt;p&gt;No single control should be the only thing standing between you and a leak. Three layers, each catching what the previous one might miss:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/strong&gt; — prevention: keep secret-bearing files out of Git entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;pre-commit&lt;/code&gt; hook&lt;/strong&gt; — detection: scan every commit for secrets and block it if one slips through&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; — design: keep secrets out of the codebase in the first place&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Layer 1 — &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Environment variables
.env
.env.*
!.env.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;!.env.example&lt;/code&gt; exception keeps a template in the repo — a documented list of which variables are needed, with empty values. Anyone picking up the project knows exactly what to fill in without ever seeing a secret.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 2 — the &lt;code&gt;pre-commit&lt;/code&gt; hook
&lt;/h3&gt;

&lt;p&gt;A Git hook is a script Git runs automatically at a specific moment. A &lt;code&gt;pre-commit&lt;/code&gt; hook runs right before a commit is created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git commit
    │
    ▼
pre-commit hook runs   ← automatic
    │
    ├─ secret found?  → ❌ commit blocked
    └─ all clean?     → ✅ commit proceeds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mine scans staged files for patterns that match real credentials — Atlassian tokens, AWS keys, private keys. If it finds one, the commit is blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ACM&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="nv"&gt;patterns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ATATT[A-Za-z0-9_=-]{16,}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}|-----BEGIN [A-Z ]*PRIVATE KEY-----'&lt;/span&gt;

&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git show &lt;span class="s2"&gt;":&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nEq&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$patterns&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ Possible secret in: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🛑 Commit blocked: secrets detected in staged files."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exit code is everything.&lt;/strong&gt; If a pre-commit hook exits non-zero, Git aborts the commit. That single &lt;code&gt;exit 1&lt;/code&gt; is what makes this real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I keep the hook in the repo.&lt;/strong&gt; Git's default hooks live in &lt;code&gt;.git/hooks/&lt;/code&gt;, which isn't versioned. I store mine in a tracked &lt;code&gt;.githooks/&lt;/code&gt; folder and point Git at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config core.hooksPath .githooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the hook travels with the project and gets reviewed like any other code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 3 — environment variables
&lt;/h3&gt;

&lt;p&gt;The first two layers stop secrets from being committed. The third makes sure they're not in the code to begin with:&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;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;apiToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JIRA_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing JIRA_API_TOKEN. Run with: node --env-file=.env.local script.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Node 20.6+, no &lt;code&gt;dotenv&lt;/code&gt; dependency needed — the built-in &lt;code&gt;--env-file&lt;/code&gt; flag loads your &lt;code&gt;.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.local script.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing it
&lt;/h2&gt;

&lt;p&gt;The best way to trust a safety net is to test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'const token = "ATATT3xFAKEFAKEFAKEFAKEFAKE123456"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; leak-test.js
git add leak-test.js
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt;
&lt;span class="c"&gt;# ❌ Possible secret in: leak-test.js&lt;/span&gt;
&lt;span class="c"&gt;# 🛑 Commit blocked: secrets detected in staged files.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commit never happens. That's the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Security works best when the tooling enforces the rules — not your memory. Three small pieces of configuration and a whole category of mistakes simply can't happen.&lt;/p&gt;

&lt;p&gt;For HandyFEM, where trust is the product, this wasn't over-engineering. It was the starting line.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 HandyFEM App Series
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Previous:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://dev.to/constanza_diaz_dev/from-specs-to-tickets-automating-jira-setup-with-nodejs-and-the-jira-api-3j9f/"&gt;From Specs to Tickets: Automating Jira Setup with Node.js and the Jira API&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
🔗 &lt;strong&gt;Next:&lt;/strong&gt; &lt;em&gt;none (latest post)&lt;/em&gt;  &lt;/p&gt;

&lt;h2&gt;
  
  
  🏷️ All posts in this series: #HandyFEMApp
&lt;/h2&gt;

&lt;p&gt;*Follow the build: #HandyFEMApp *&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>git</category>
      <category>devops</category>
    </item>
    <item>
      <title>From Specs to Tickets: Automating Jira Setup with Node.js and the Jira API</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Sun, 31 May 2026 15:19:38 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/from-specs-to-tickets-automating-jira-setup-with-nodejs-and-the-jira-api-3j9f</link>
      <guid>https://dev.to/constanza_diaz_dev/from-specs-to-tickets-automating-jira-setup-with-nodejs-and-the-jira-api-3j9f</guid>
      <description>&lt;h2&gt;
  
  
  The plan was simple
&lt;/h2&gt;

&lt;p&gt;Take the specs we'd written, turn them into Jira epics, stories and subtasks, and start sprinting.&lt;/p&gt;

&lt;p&gt;It took longer than expected. Here's what actually happened — and what I learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why automate Jira setup at all?
&lt;/h2&gt;

&lt;p&gt;HandyFEM has 8 epics, 37 stories and ~160 subtasks. Creating that manually would take a full day and be error-prone. More importantly: the specs were already written in a structured format. Translating structured data into Jira issues is exactly the kind of repetitive task that should be automated.&lt;/p&gt;

&lt;p&gt;So I wrote a &lt;strong&gt;Node.js&lt;/strong&gt; script to do it via the Jira REST API.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1 — Jira Spaces ≠ Jira Classic
&lt;/h2&gt;

&lt;p&gt;My account uses &lt;strong&gt;Jira Spaces&lt;/strong&gt; — Atlassian's newer interface. The classic Jira has CSV import built in. Jira Spaces doesn't.&lt;/p&gt;

&lt;p&gt;This isn't documented anywhere obviously. You discover it by looking for the import option and not finding it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; always check which version of Jira you have before planning your workflow. The API still works, but some endpoints behave differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2 — The API token wasn't the issue (until it was)
&lt;/h2&gt;

&lt;p&gt;First attempt: connection error. I assumed it was the token. It wasn't — it was an expired token from a previous session. Regenerating it fixed the connection.&lt;/p&gt;

&lt;p&gt;The real lesson: &lt;code&gt;curl -u email:token https://your-domain.atlassian.net/rest/api/3/myself&lt;/code&gt; is the fastest way to verify auth before running any script.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3 — &lt;code&gt;customfield_10014&lt;/code&gt; doesn't exist in team-managed projects
&lt;/h2&gt;

&lt;p&gt;In classic Jira, linking a story to an epic uses a field called &lt;code&gt;customfield_10014&lt;/code&gt; (Epic Link). In team-managed projects (Jira Spaces), this field doesn't exist. You use &lt;code&gt;parent&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;The error was clear once I saw it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"customfield_10014"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Field cannot be set. It is not on the appropriate screen, or unknown."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix: remove &lt;code&gt;customfield_10014&lt;/code&gt;, keep only &lt;code&gt;parent: { id: epicId }&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 4 — Board search doesn't work for team-managed projects
&lt;/h2&gt;

&lt;p&gt;The Agile API endpoint &lt;code&gt;/rest/agile/1.0/board?projectKeyOrId=HFM&lt;/code&gt; returns empty for team-managed projects, even when the board exists.&lt;/p&gt;

&lt;p&gt;Claude Code caught this one after the first failure — it explained the root cause and the fix: hardcode the board ID directly instead of searching for it.&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;// This doesn't work for team-managed projects:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;boards&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="s2"&gt;`/rest/agile/1.0/board?projectKeyOrId=HFM`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// This does:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SCRUM board&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;h2&gt;
  
  
  What the final setup looks like
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;8 epics&lt;/strong&gt; covering the full project lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Planning &amp;amp; architecture (done)&lt;/li&gt;
&lt;li&gt;Design System (in progress)&lt;/li&gt;
&lt;li&gt;MVP screens (in progress)&lt;/li&gt;
&lt;li&gt;Backend / Supabase&lt;/li&gt;
&lt;li&gt;Security &amp;amp; privacy&lt;/li&gt;
&lt;li&gt;PWA + SEO + emails&lt;/li&gt;
&lt;li&gt;Testing &amp;amp; launch&lt;/li&gt;
&lt;li&gt;AI Agentic features (v2)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;37 stories&lt;/strong&gt; with detailed descriptions and acceptance criteria.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;~160 subtasks&lt;/strong&gt; per story — including one for writing the blog post before closing the story. That last one matters: documentation that lives next to the work gets written. Documentation planned separately doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8 sprints&lt;/strong&gt; mapped to the logical build order — from DS implementation to public launch in Barcelona.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security: never hardcode tokens
&lt;/h2&gt;

&lt;p&gt;The scripts use &lt;code&gt;node --env-file=.env.local&lt;/code&gt; (native in Node v18+) to load credentials. No &lt;code&gt;dotenv&lt;/code&gt; dependency, no token in the codebase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.local docs/handyfem-jira-import.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both scripts are in &lt;code&gt;.gitignore&lt;/code&gt;. Claude Code was instructed to never write sensitive values directly — always give instructions for the developer to add them manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Start with a simple API test before writing the full script. One &lt;code&gt;curl&lt;/code&gt; call to verify auth and one to verify the project endpoint would have saved 30 minutes of debugging, and a lot of Claude tokens.&lt;/p&gt;

&lt;p&gt;Also: when automating Jira, always check whether your project is classic or team-managed first. The API surface is different enough to matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scripts
&lt;/h2&gt;

&lt;p&gt;Both scripts are in &lt;code&gt;docs/&lt;/code&gt; in the HandyFEM repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;handyfem-jira-import.js&lt;/code&gt; — creates epics, stories and subtasks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;handyfem-jira-sprints.js&lt;/code&gt; — creates sprints and assigns issues&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📚 HandyFEM App Series
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Previous:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://dev.to/constanza_diaz_dev/building-handyfems-design-system-with-claudeai-specs-components-and-visual-previews-2mk8/"&gt;Building HandyFEM’s Design System with Claude.ai: Specs, Components, and Visual Previews&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
🔗 &lt;strong&gt;Next:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://dev.to/constanza_diaz_dev/security-by-design-keeping-api-tokens-out-of-git-with-a-3-layer-setup-33a9/"&gt;Security by Design: Keeping API Tokens out of Git with a 3-Layer Setup&lt;/a&gt;&lt;/em&gt;  &lt;/p&gt;

&lt;p&gt;🏷️ All posts in this series: #HandyFEMApp&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Building in public. All mistakes included.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>frontend</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building HandyFEM's Design System with Claude.ai — Specs, Components and Visual Previews</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Sun, 31 May 2026 12:35:03 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/building-handyfems-design-system-with-claudeai-specs-components-and-visual-previews-2mk8</link>
      <guid>https://dev.to/constanza_diaz_dev/building-handyfems-design-system-with-claudeai-specs-components-and-visual-previews-2mk8</guid>
      <description>&lt;h2&gt;
  
  
  What is a Design System and why build it first?
&lt;/h2&gt;

&lt;p&gt;A Design System is a single source of truth for all visual and interaction decisions in an app — colors, typography, spacing, components, states, accessibility rules.&lt;/p&gt;

&lt;p&gt;The reason to build it before any screen is simple: if you change the primary color after building 7 screens, you're updating it in 40 places. If you change it in the DS, it propagates everywhere automatically.&lt;/p&gt;

&lt;p&gt;For HandyFEM I built the DS in two layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — Tokens:&lt;/strong&gt; CSS variables and Tailwind config values. Colors, spacing, border radius, shadows, transitions. Everything that gets used across components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Components:&lt;/strong&gt; Button, Input, Card, Badge, Avatar. Each one with all variants, all states, and full accessibility spec.&lt;/p&gt;




&lt;h2&gt;
  
  
  The token decisions: here are some design desicions.
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Colors
&lt;/h3&gt;

&lt;p&gt;The palette was already defined from HandyFEM's social media posts and brand materials — teal as primary, violet as accent, with neutrals for backgrounds and borders.&lt;/p&gt;

&lt;p&gt;The key decision was dropping the cream background in favor of clean white and light gray. Cream looks warm in isolation but gets muddy next to colored components. White/gray lets the teal and violet breathe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;--color-primary&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#4&lt;/span&gt;&lt;span class="nt"&gt;A7C7D&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;      &lt;span class="c"&gt;/* teal */&lt;/span&gt;
&lt;span class="nt"&gt;--color-accent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#776&lt;/span&gt;&lt;span class="nt"&gt;AAA&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;       &lt;span class="c"&gt;/* violet */&lt;/span&gt;
&lt;span class="nt"&gt;--color-bg-primary&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;#ffffff&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;--color-bg-secondary&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;#F5F5F5&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;--color-border&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;#E0DDD6&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;       &lt;span class="c"&gt;/* neutral gray */&lt;/span&gt;
&lt;span class="nt"&gt;--color-amber&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;#FCC970&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;        &lt;span class="c"&gt;/* ratings only */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Spacing
&lt;/h3&gt;

&lt;p&gt;Base 4px system. Every spacing value is a multiple of 4. This sounds rigid but in practice it makes layouts feel consistent without effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  Border radius
&lt;/h3&gt;

&lt;p&gt;The interesting decision here: buttons are &lt;code&gt;8px&lt;/code&gt; (rounded), not pill-shaped. The navbar is pill (&lt;code&gt;9999px&lt;/code&gt;). This combination — pill navbar + rounded buttons — is what Linear, Vercel and Notion use. It gives sophistication without being generic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The components
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DS-01 — Button
&lt;/h3&gt;

&lt;p&gt;Four variants: primary (teal), secondary (violet outline), ghost (text only), destructive (red outline). Three sizes: large (48px), medium (40px), small (32px for filters and chips).&lt;/p&gt;

&lt;p&gt;The non-obvious decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loading state blocks double-submit — critical for auth forms&lt;/li&gt;
&lt;li&gt;Destructive always requires an AlertDialog confirmation before firing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@media (hover: hover)&lt;/code&gt; wrapping on all hover effects — no sticky hover states on touch devices&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefers-reduced-motion&lt;/code&gt; removes scale animation, keeps color changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  DS-02 — Inputs
&lt;/h3&gt;

&lt;p&gt;Label always above, never floating. Floating labels are visually clever but have serious accessibility edge cases with screen readers and mobile keyboards. Not worth it.&lt;/p&gt;

&lt;p&gt;Validation fires &lt;code&gt;onBlur&lt;/code&gt; — never in real time while typing. Real-time validation is annoying when you haven't finished the word yet.&lt;/p&gt;

&lt;p&gt;The file upload component has drag-and-drop, immediate preview, and opens camera/gallery on mobile. This matters for the professional onboarding — portfolio photos are a core part of the profile.&lt;/p&gt;

&lt;h3&gt;
  
  
  DS-03 — Professional Cards
&lt;/h3&gt;

&lt;p&gt;Two layouts: horizontal for mobile (photo left, info right), vertical for desktop (photo top, info bottom in a 2-column grid). The border is &lt;code&gt;0.5px solid #E0DDD6&lt;/code&gt; — neutral gray, not the lavender that was in the first iteration. The lavender competed with the content. Gray doesn't.&lt;/p&gt;

&lt;p&gt;The "Verified" badge has a pulsing green dot. Small detail, big signal — it communicates that the profile is active and has been reviewed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftnii80j8jcclw0nroxet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftnii80j8jcclw0nroxet.png" alt="Design options given by claude.ai" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  DS-04 — Badges and Chips
&lt;/h3&gt;

&lt;p&gt;Status badges are pill-shaped. Category tags are rounded (6px). This distinction encodes hierarchy — pill for important single states, rounded for informational groups. Same pattern that GitHub and Linear use.&lt;/p&gt;

&lt;p&gt;Filter chips show an X icon when active. One pending polish note: the X is too small and slightly misaligned — will fix in code, not in spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  DS-05 — Avatar
&lt;/h3&gt;

&lt;p&gt;Color assigned by name, not randomly. The first character of the name maps to one of four background colors via &lt;code&gt;charCode % 4&lt;/code&gt;. Marta López is always lavender. Sara Ruiz is always teal. Consistent across the whole app, across all devices, forever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fca10i0ivuee8d6st1axo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fca10i0ivuee8d6st1axo.png" alt="design preview" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The workflow: spec → visual preview → approve → document
&lt;/h2&gt;

&lt;p&gt;For each component, the process was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define variants, states, and decisions in text&lt;/li&gt;
&lt;li&gt;Generate a live visual preview in Claude Design&lt;/li&gt;
&lt;li&gt;Review and adjust (border color, shape, sizing)&lt;/li&gt;
&lt;li&gt;Lock the decision in &lt;code&gt;docs/handyfem-specs.md&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Having the visual preview before writing code meant design decisions were made consciously, not discovered mid-implementation. The spec document in &lt;code&gt;/docs&lt;/code&gt; is now the reference for every component — Claude Code will use it to generate the actual React + Tailwind + shadcn/ui code.&lt;/p&gt;




&lt;h3&gt;
  
  
  What the spec document looks like
&lt;/h3&gt;

&lt;p&gt;Each component entry in &lt;code&gt;handyfem-specs.md&lt;/code&gt; includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All variants with exact hex values&lt;/li&gt;
&lt;li&gt;All states (default, hover, focus, error, disabled, loading)&lt;/li&gt;
&lt;li&gt;Token references (no hardcoded values)&lt;/li&gt;
&lt;li&gt;Accessibility requirements (aria attributes, focus rings, touch targets)&lt;/li&gt;
&lt;li&gt;shadcn/ui implementation notes&lt;/li&gt;
&lt;li&gt;Props interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;specs-driven development&lt;/strong&gt; — write the spec, then write the code. A well-written spec is also the prompt for Claude Code, which means the first generated output is much closer to final.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interaction details
&lt;/h2&gt;

&lt;p&gt;Four effects, chosen carefully:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navbar pill with scroll shrink&lt;/strong&gt; — uses &lt;code&gt;animation-timeline: scroll()&lt;/code&gt; to progressively shrink the navbar from 100% to 88% as the user scrolls. Degrades gracefully on Firefox (stays at 100%, still functional).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hero fade + slide up&lt;/strong&gt; — &lt;code&gt;opacity: 0 → 1&lt;/code&gt; with &lt;code&gt;translateY(16px → 0)&lt;/code&gt; on load. Staggered: title first, subtitle 100ms later, CTAs 200ms later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Card hover&lt;/strong&gt; — &lt;code&gt;translateY(-2px)&lt;/code&gt; with a teal-tinted shadow. Only on &lt;code&gt;@media (hover: hover)&lt;/code&gt; — touch devices don't get stuck hover states.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scroll-triggered fade&lt;/strong&gt; — sections enter the viewport with a fade + slide via &lt;code&gt;IntersectionObserver&lt;/code&gt;. Implemented as a reusable &lt;code&gt;useScrollReveal&lt;/code&gt; hook.&lt;/p&gt;

&lt;p&gt;All four respect &lt;code&gt;prefers-reduced-motion&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The Design System spec is done. Next step: Claude Code — implementing all of this as actual React components in the Next.js project, starting with &lt;code&gt;globals.css&lt;/code&gt;, &lt;code&gt;tailwind.config.ts&lt;/code&gt;, and the component files one by one.&lt;/p&gt;

&lt;p&gt;After that: building the 7 MVP screens using the DS as the foundation.&lt;/p&gt;

&lt;p&gt;But first. Let's make a proper plan, by using JIRA to organize all the work! &lt;/p&gt;




&lt;h2&gt;
  
  
  📚 HandyFEM App Series
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Previous:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://dev.to/constanza_diaz_dev/from-idea-to-specs-planning-handyfems-architecture-with-claudeai-specs-driven-development-1lfd"&gt;From Idea to Specs: Planning HandyFEM's Architecture with Claude.ai - Specs Driven Development&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
🔗 &lt;strong&gt;Next:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://dev.to/constanza_diaz_dev/from-specs-to-tickets-automating-jira-setup-with-nodejs-and-the-jira-api-3j9f/"&gt;From Specs to Tickets: Automating Jira Setup with Node.js and the Jira API&lt;/a&gt;&lt;/em&gt;  &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Building in public. All mistakes included.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>design</category>
      <category>tailwindcss</category>
      <category>ui</category>
    </item>
    <item>
      <title>From Idea to Specs: Planning HandyFEM's Architecture with Claude.ai - Specs Driven development.</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Fri, 29 May 2026 15:03:05 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/from-idea-to-specs-planning-handyfems-architecture-with-claudeai-specs-driven-development-35ac</link>
      <guid>https://dev.to/constanza_diaz_dev/from-idea-to-specs-planning-handyfems-architecture-with-claudeai-specs-driven-development-35ac</guid>
      <description>&lt;h2&gt;
  
  
  What is HandyFEM?
&lt;/h2&gt;

&lt;p&gt;HandyFEM is a web app I'm building to connect women professionals in technical trades (electricians, plumbers, carpenters...) with clients who are looking for them. It's both a real product I plan to launch and a portfolio project — so it has to be well-built, secure, and professional.&lt;br&gt;
I'm documenting the entire process as I go. This is the first post in the series.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with jumping straight into code
&lt;/h2&gt;

&lt;p&gt;Without a solid plan, we can find several problems, features that don't connect, components that have to be rebuilt, a design that makes sense locally but not globally.&lt;br&gt;
So I decided to do it properly — specs first, code second.&lt;br&gt;
I used Claude.ai as a thinking partner throughout this process. Not just to generate content, but to challenge my decisions, suggest alternatives, and help me document everything in a structured way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Reviewing the user flow
&lt;/h2&gt;

&lt;p&gt;I already had a flow diagram from a previous iteration of the project. We started there. I put my diagram to be judged by Claude and it identified a few issues or things that were unclear, even though I had the ideas in my mind. The most important decision came from a simple question I hadn't fully answered: can one person be both a client and a professional?&lt;br&gt;
I went with a &lt;strong&gt;single account with a base client role&lt;/strong&gt;, with the ability to activate a &lt;strong&gt;professional profile from the dashboard later&lt;/strong&gt;. Same pattern as LinkedIn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Defining the MVP scope
&lt;/h2&gt;

&lt;p&gt;With the corrected flow, we mapped out all the features. Then we cut ruthlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the MVP:
&lt;/h2&gt;

&lt;p&gt;Landing page&lt;br&gt;
Sign up / Log in + email verification&lt;br&gt;
Public directory with search and filters&lt;br&gt;
Professional public profile&lt;br&gt;
Unified dashboard with role toggle&lt;br&gt;
Professional onboarding (4-step flow)&lt;br&gt;
Basic chat&lt;/p&gt;

&lt;h2&gt;
  
  
  What's out (v2):
&lt;/h2&gt;

&lt;p&gt;Admin panel for profile moderation&lt;br&gt;
Payments&lt;br&gt;
Geolocation / map view&lt;br&gt;
Emergency button&lt;br&gt;
Push notifications&lt;/p&gt;

&lt;p&gt;The Admin Panel was a tough cut — it was in my original diagram and it's genuinely important for safety. But it adds significant complexity and the MVP can function without it (profiles go live directly). It'll be the first thing added in v2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Stack decisions (and why)
&lt;/h2&gt;

&lt;p&gt;I was already planning to use Next.js + Supabase, but I took the time to articulate why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Next.js&lt;/strong&gt; — the professional directory needs to be indexed by Google. If someone searches "female electrician Barcelona", I want HandyFEM to show up. That requires SSR, which React alone doesn't give you. Next.js also has API routes built in, so no separate backend for simple logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Supabase&lt;/strong&gt; — covers auth, PostgreSQL, realtime (for chat), and storage (for profile photos and portfolio) in one service. It has a generous free tier for an MVP and integrates cleanly with Next.js.&lt;br&gt;
shadcn/ui — component library that copies code directly into your project rather than installing as a dependency. Accessible by default, unstyled, and you apply your own design tokens. Very common in Next.js projects right now.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tailwind CSS — utility-first styling that works perfectly with shadcn.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4 — Writing the specs
&lt;/h2&gt;

&lt;p&gt;This is where most of the session went.&lt;br&gt;
I've been experimenting with a &lt;strong&gt;specs-driven development&lt;/strong&gt; approach — writing detailed specifications for each screen and component before writing any code. The idea is that a well-written spec becomes the prompt for Claude Design, and a well-prompted Claude Design produces clean, copy-paste-ready code on the first try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For each component in the Design System, we documented:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All variants and sizes&lt;/li&gt;
&lt;li&gt;All states (default, hover, focus, error, disabled, loading)&lt;/li&gt;
&lt;li&gt;Exact color tokens&lt;/li&gt;
&lt;li&gt;Accessibility requirements (aria labels, focus rings, touch targets)&lt;/li&gt;
&lt;li&gt;shadcn/ui implementation notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;And for each screen:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Layout and sections&lt;/li&gt;
&lt;li&gt;All interactive elements&lt;/li&gt;
&lt;li&gt;All states (loading skeletons, empty states, error states)&lt;/li&gt;
&lt;li&gt;SEO considerations&lt;/li&gt;
&lt;li&gt;Technical notes for Supabase queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full spec document is saved in /docs in the project repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Specs are not overhead — they are the work. The time I spent writing specs today will save me hours of refactoring later. And they double as documentation, which makes the project look serious in a portfolio context.&lt;/li&gt;
&lt;li&gt;Claude works best as a thinking partner, not an autocomplete. The most valuable moments weren't when it generated content — they were when it pushed back on my decisions or asked clarifying questions I hadn't considered.&lt;/li&gt;
&lt;li&gt;Cut early, cut ruthlessly. Every feature that goes into an MVP is a feature that needs to be designed, built, tested, and maintained. The Admin Panel will be better in v2 anyway — I'll have real users to learn from.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What's next&lt;br&gt;
Next post: taking all of this into Jira — turning the specs into epics, stories, and tasks so the build phase has clear structure.&lt;br&gt;
After that: the Design System in code, and then building screen by screen.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>buildinpublic</category>
      <category>claude</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From Idea to Specs: Planning HandyFEM's Architecture with Claude.ai - Specs Driven development.</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Fri, 29 May 2026 14:44:53 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/from-idea-to-specs-planning-handyfems-architecture-with-claudeai-specs-driven-development-1lfd</link>
      <guid>https://dev.to/constanza_diaz_dev/from-idea-to-specs-planning-handyfems-architecture-with-claudeai-specs-driven-development-1lfd</guid>
      <description>&lt;h2&gt;
  
  
  What is HandyFEM?
&lt;/h2&gt;

&lt;p&gt;HandyFEM is a web app I'm building to connect women professionals in technical trades (electricians, plumbers, carpenters...) with clients who are looking for them. It's both a real product I plan to launch and a portfolio project — so it has to be well-built, secure, and professional.&lt;br&gt;
I'm documenting the entire process as I go. This is the first post in the series.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with jumping straight into code
&lt;/h2&gt;

&lt;p&gt;Without a solid plan, we can find several problems, features that don't connect, components that have to be rebuilt, a design that makes sense locally but not globally.&lt;br&gt;
So I decided to do it properly — specs first, code second.&lt;br&gt;
I used Claude.ai as a thinking partner throughout this process. Not just to generate content, but to challenge my decisions, suggest alternatives, and help me document everything in a structured way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Reviewing the user flow
&lt;/h2&gt;

&lt;p&gt;I already had a flow diagram from a previous iteration of the project. We started there. I put my diagram to be judged by Claude and it identified a few issues or things that were unclear, even though I had the ideas in my mind. The most important decision came from a simple question I hadn't fully answered: can one person be both a client and a professional?&lt;br&gt;
I went with a &lt;strong&gt;single account with a base client role&lt;/strong&gt;, with the ability to activate a &lt;strong&gt;professional profile from the dashboard later&lt;/strong&gt;. Same pattern as LinkedIn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Defining the MVP scope
&lt;/h2&gt;

&lt;p&gt;With the corrected flow, we mapped out all the features. Then we cut ruthlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the MVP:
&lt;/h2&gt;

&lt;p&gt;Landing page&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign up / Log in + email verification&lt;/li&gt;
&lt;li&gt;Public directory with search and filters&lt;/li&gt;
&lt;li&gt;Professional public profile&lt;/li&gt;
&lt;li&gt;Unified dashboard with role toggle&lt;/li&gt;
&lt;li&gt;Professional onboarding (4-step flow)&lt;/li&gt;
&lt;li&gt;Basic chat&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's out (v2):
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Admin panel for profile moderation&lt;/li&gt;
&lt;li&gt;Payments&lt;/li&gt;
&lt;li&gt;Geolocation / map view&lt;/li&gt;
&lt;li&gt;Emergency button&lt;/li&gt;
&lt;li&gt;Push notifications
The Admin Panel was a tough cut — it was in my original diagram and it's genuinely important for safety. But it adds significant complexity and the MVP can function without it (profiles go live directly). It'll be the first thing added in v2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3 — Stack decisions (and why)
&lt;/h2&gt;

&lt;p&gt;I was already planning to use Next.js + Supabase, but I took the time to articulate why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Next.js&lt;/strong&gt; — the professional directory needs to be indexed by Google. If someone searches "female electrician Barcelona", I want HandyFEM to show up. That requires SSR, which React alone doesn't give you. Next.js also has API routes built in, so no separate backend for simple logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Supabase&lt;/strong&gt; — covers auth, PostgreSQL, realtime (for chat), and storage (for profile photos and portfolio) in one service. It has a generous free tier for an MVP and integrates cleanly with Next.js.&lt;br&gt;
shadcn/ui — component library that copies code directly into your project rather than installing as a dependency. Accessible by default, unstyled, and you apply your own design tokens. Very common in Next.js projects right now.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tailwind CSS — utility-first styling that works perfectly with shadcn.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4 — Writing the specs
&lt;/h2&gt;

&lt;p&gt;This is where most of the session went.&lt;br&gt;
I've been experimenting with a &lt;strong&gt;specs-driven development&lt;/strong&gt; approach — writing detailed specifications for each screen and component before writing any code. The idea is that a well-written spec becomes the prompt for Claude Design, and a well-prompted Claude Design produces clean, copy-paste-ready code on the first try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For each component in the Design System, we documented:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All variants and sizes&lt;/li&gt;
&lt;li&gt;All states (default, hover, focus, error, disabled, loading)&lt;/li&gt;
&lt;li&gt;Exact color tokens&lt;/li&gt;
&lt;li&gt;Accessibility requirements (aria labels, focus rings, touch targets)&lt;/li&gt;
&lt;li&gt;shadcn/ui implementation notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;And for each screen:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Layout and sections&lt;/li&gt;
&lt;li&gt;All interactive elements&lt;/li&gt;
&lt;li&gt;All states (loading skeletons, empty states, error states)&lt;/li&gt;
&lt;li&gt;SEO considerations&lt;/li&gt;
&lt;li&gt;Technical notes for Supabase queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full spec document is saved in /docs in the project repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Specs are not overhead — they are the work. The time I spent writing specs today will save me hours of refactoring later. And they double as documentation, which makes the project look serious in a portfolio context.&lt;/li&gt;
&lt;li&gt;Claude works best as a thinking partner, not an autocomplete. The most valuable moments weren't when it generated content — they were when it pushed back on my decisions or asked clarifying questions I hadn't considered.&lt;/li&gt;
&lt;li&gt;Cut early, cut ruthlessly. Every feature that goes into an MVP is a feature that needs to be designed, built, tested, and maintained. The Admin Panel will be better in v2 anyway — I'll have real users to learn from.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What's next&lt;br&gt;
Next post: taking all of this into Jira — turning the specs into epics, stories, and tasks so the build phase has clear structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  After that: the Design System in code, and then building screen by screen.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  📚 HandyFEM App Series
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Previous:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://dev.to/constanza_diaz_dev/from-prompt-to-practical-evolving-handyfems-user-flow-with-claudeai-mermaidlive-kg2"&gt;From Prompt to Practical: Evolving HandyFEM’s User Flow with Claude + Mermaid&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
🔗 &lt;strong&gt;Next:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://dev.to/constanza_diaz_dev/building-handyfems-design-system-with-claudeai-specs-components-and-visual-previews-2mk8"&gt;Building HandyFEM’s Design System with Claude.ai: Specs, Components, and Visual Previews&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>From Prompt to Practical: Evolving HandyFEM’s User Flow with Claude.ai + Mermaid.live</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Wed, 15 Oct 2025 12:14:42 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/from-prompt-to-practical-evolving-handyfems-user-flow-with-claudeai-mermaidlive-fj7</link>
      <guid>https://dev.to/constanza_diaz_dev/from-prompt-to-practical-evolving-handyfems-user-flow-with-claudeai-mermaidlive-fj7</guid>
      <description>&lt;p&gt;In my previous post about HandyFEM user flow I used Figma and ChatGPT to create a user flow for may app. This time I asked claude.ai, and I've got a much more complete version. &lt;br&gt;
First of all I explained Claude all about my app, and I tried creating a prompt to create my chart in RapidChart, because I saw a post about it, but unfortunately it didn't work as expected, the chart was never built, and I tried several times, I even tried to make a smaller one, but nothing was built at all. &lt;/p&gt;

&lt;p&gt;So I asked claude.ai for alternatives to RapidChart, and I decided to give a try to Mermaid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foou5l60tdd23dltt69k1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foou5l60tdd23dltt69k1.png" alt="First attempt of Mermaid code made by Claude.ai" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It gave me a good enough result, already more complete than what I've done before, but with less visual references... so I copy/pasted my previous diagram and suggested to integrate both ideas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsy9p8v1zmnp2tb2i9v52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsy9p8v1zmnp2tb2i9v52.png" alt="Asking Claude to integrate both charts" width="729" height="740"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;new diagram:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Color-coded:&lt;/strong&gt;&lt;br&gt;
Yellow = Buttons&lt;br&gt;
Light blue = Forms&lt;br&gt;
Teal = Screens&lt;br&gt;
Purple = Decision points&lt;br&gt;
Blue = Professional UI&lt;br&gt;
Green = Client UI&lt;br&gt;
Red = Admin UI&lt;br&gt;
Gray = System processes&lt;/p&gt;

&lt;p&gt;The diagram shows the actual interface elements and navigation flow, making it practical for your developers to reference when building the app!&lt;/p&gt;

&lt;p&gt;(Disclaimer: I had to iterate a few times in Claude, to get the correct flow, as the admin flow was incomplete).&lt;/p&gt;

&lt;p&gt;The following image is just an unreadable screenshot for a reference, it was too large to attach in real size:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UI/Navigation flow and User journey flow:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep0us9lzjb1xoao6nqi2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep0us9lzjb1xoao6nqi2.png" alt="Final UI/Navigation flow diagram (unreadable)" width="664" height="920"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mermaidchart.com/app/projects/75e90edc-ad33-442c-a2c1-e97de4c73342/diagrams/66e54f91-963a-4145-b23b-2f6d9ab2f876/share/invite/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkb2N1bWVudElEIjoiNjZlNTRmOTEtOTYzYS00MTQ1LWIyM2ItMmY2ZDlhYjJmODc2IiwiYWNjZXNzIjoiQ29tbWVudCIsImlhdCI6MTc2MDQ3NTI5OH0.Unm61oiCBHYJt1VUdePIxYAaHUlOri0mOFTKa-utKSM" rel="noopener noreferrer"&gt;View fullsize&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ux</category>
      <category>ai</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>From Prompt to Practical: Evolving HandyFEM’s User Flow with Claude.ai + Mermaid.live</title>
      <dc:creator>Constanza Diaz</dc:creator>
      <pubDate>Tue, 14 Oct 2025 21:43:23 +0000</pubDate>
      <link>https://dev.to/constanza_diaz_dev/from-prompt-to-practical-evolving-handyfems-user-flow-with-claudeai-mermaidlive-kg2</link>
      <guid>https://dev.to/constanza_diaz_dev/from-prompt-to-practical-evolving-handyfems-user-flow-with-claudeai-mermaidlive-kg2</guid>
      <description>&lt;p&gt;I’m building &lt;strong&gt;HandyFEM&lt;/strong&gt;, a directory + marketplace connecting women professionals in construction and repairs with clients. In my &lt;a href="https://dev.to/constanza_diaz_dev/building-a-full-stack-web-app-from-scratch-first-steps-gi8"&gt;previous post&lt;/a&gt;, I sketched an initial user flow using Figma and ChatGPT. This time I pushed further: I briefed &lt;strong&gt;Claude&lt;/strong&gt; with the full app context and iterated toward a &lt;strong&gt;color-coded, developer-ready UI/navigation diagram&lt;/strong&gt; using &lt;strong&gt;Mermaid&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post documents what I tried, what failed, and what finally worked—so you can reproduce the setup without losing hours to tooling dead ends.&lt;/p&gt;




&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;Create a &lt;strong&gt;single source of truth&lt;/strong&gt; for HandyFEM’s navigation and user journeys—clear enough for stakeholders, and concrete enough for developers to implement screens, forms, and decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Attempt 1: RapidChart (didn’t pan out)
&lt;/h2&gt;

&lt;p&gt;I first tried to auto-generate the diagram via &lt;strong&gt;RapidChart&lt;/strong&gt; prompts. Multiple attempts—including smaller subsets—didn’t render a chart at all. Interesting idea, but in my case it wasn’t reliable enough to ship.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tooling takeaway:&lt;/em&gt; if your diagram generator is brittle, switch to a text-based standard you can version-control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pivot: Mermaid for reproducible, versioned diagrams
&lt;/h2&gt;

&lt;p&gt;Claude suggested alternatives, and I decided to give &lt;strong&gt;Mermaid&lt;/strong&gt; a try. It immediately produced a &lt;strong&gt;decent first draft&lt;/strong&gt;—already more complete than my earlier diagram, even if it lacked some visual cues.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foou5l60tdd23dltt69k1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foou5l60tdd23dltt69k1.png" alt="First attempt of Mermaid code made by Claude.ai" width="799" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To enrich it, I &lt;strong&gt;pasted my previous flow&lt;/strong&gt; and asked Claude to &lt;strong&gt;integrate both&lt;/strong&gt;. That gave me structure + detail.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsy9p8v1zmnp2tb2i9v52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsy9p8v1zmnp2tb2i9v52.png" alt="Asking Claude to integrate both charts" width="729" height="740"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The final diagram (color-coded for quick scanning)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Legend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yellow&lt;/strong&gt; = Buttons.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Light blue&lt;/strong&gt; = Forms.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teal&lt;/strong&gt; = Screens.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purple&lt;/strong&gt; = Decision points.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blue&lt;/strong&gt; = Professional UI.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Green&lt;/strong&gt; = Client UI.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Red&lt;/strong&gt; = Admin UI.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gray&lt;/strong&gt; = System processes.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mapping keeps discussions concrete. Designers and developers can point to the exact element in the flow and agree on behavior.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: I iterated a few times in Claude to complete the &lt;strong&gt;admin&lt;/strong&gt; path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The image below is a small screenshot just for reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UI/Navigation flow and User journey flow:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep0us9lzjb1xoao6nqi2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep0us9lzjb1xoao6nqi2.png" alt="Final UI/Navigation flow diagram (unreadable)" width="664" height="920"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mermaidchart.com/app/projects/75e90edc-ad33-442c-a2c1-e97de4c73342/diagrams/66e54f91-963a-4145-b23b-2f6d9ab2f876/share/invite/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkb2N1bWVudElEIjoiNjZlNTRmOTEtOTYzYS00MTQ1LWIyM2ItMmY2ZDlhYjJmODc2IiwiYWNjZXNzIjoiQ29tbWVudCIsImlhdCI6MTc2MDQ3NTI5OH0.Unm61oiCBHYJt1VUdePIxYAaHUlOri0mOFTKa-utKSM" rel="noopener noreferrer"&gt;Full size:&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Minimal, reproducible snippet (Mermaid)
&lt;/h2&gt;

&lt;p&gt;Here’s a simplified fragment that captures the pattern I used—screens, forms, decisions, and role-scoped branches. You can drop this into any Mermaid-enabled markdown and expand it for your app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  %% Legend-inspired classes
  classDef screen fill:#0fb,stroke:#066;       %% Teal
  classDef form fill:#b3e5fc,stroke:#0288d1;   %% Light blue
  classDef decision fill:#b39ddb,stroke:#512da8; %% Purple
  classDef button fill:#ffeb3b,stroke:#f57f17; %% Yellow
  classDef pro fill:#90caf9,stroke:#1565c0;    %% Blue (Professional UI)
  classDef cli fill:#a5d6a7,stroke:#2e7d32;    %% Green (Client UI)
  classDef admin fill:#ef9a9a,stroke:#c62828;  %% Red (Admin UI)
  classDef system fill:#bdbdbd,stroke:#616161; %% Gray (System)

  %% Entry
  A[Landing Screen]:::screen --&amp;gt; B{Choose role}:::decision

  %% Client path
  B --&amp;gt;|Client| C[Client Dashboard]:::screen
  C --&amp;gt; D[Search Professionals Form]:::form
  D --&amp;gt; E[Results Screen]:::screen
  E --&amp;gt; F[Request Quote Button]:::button
  F --&amp;gt; G[Request Form]:::form
  G --&amp;gt; H[System: Notify matched pros]:::system

  %% Professional path
  B --&amp;gt;|Professional| P[Pro Dashboard]:::screen
  P --&amp;gt; Q[Complete Profile Form]:::form
  Q --&amp;gt; R[Verify Identity Button]:::button
  R --&amp;gt; S[System: KYC check]:::system
  S --&amp;gt; T{Approved?}:::decision
  T --&amp;gt;|Yes| U[Receive job requests]:::screen
  T --&amp;gt;|No| V[Manual review]:::admin

  %% Admin path
  B --&amp;gt;|Admin| A1[Admin Panel]:::admin
  A1 --&amp;gt; A2[Moderate Listings]:::admin
  A1 --&amp;gt; A3[Resolve Disputes]:::admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ty3xym6gtfyopxhyiwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ty3xym6gtfyopxhyiwu.png" alt="simplified chart" width="800" height="698"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this helps:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text-as-diagram&lt;/strong&gt; means you can version the flow in Git, review it in pull requests, and co-edit with your team.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color classes&lt;/strong&gt; encode semantics you can reuse across large diagrams.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branching by role&lt;/strong&gt; makes gaps obvious and audit-able.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Practical notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt shape matters.&lt;/strong&gt; My best results came from supplying Claude with the &lt;strong&gt;app glossary, roles, and UI primitives&lt;/strong&gt; (screen, form, decision, button, system).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate role by role.&lt;/strong&gt; I filled Client and Professional paths first, then Admin.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it modular.&lt;/strong&gt; Break huge flows into &lt;strong&gt;linked sub-diagrams&lt;/strong&gt; per feature if your tool or host chokes on size.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat it like code.&lt;/strong&gt; Store the &lt;code&gt;.md&lt;/code&gt; or &lt;code&gt;.mmd&lt;/code&gt; in your repo and make changes via PRs.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I’ll do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Split the mega-diagram into feature-level Mermaid files.
&lt;/li&gt;
&lt;li&gt;Connect each node to &lt;strong&gt;issue links&lt;/strong&gt; or &lt;strong&gt;Figma frames&lt;/strong&gt; for implementation hand-off.
&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;acceptance criteria&lt;/strong&gt; to decisions and forms to reduce ambiguity during dev.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you’re mapping a complex app, pairing a capable LLM with Mermaid gives you speed, structure, and a durable artifact your team can actually build from.&lt;/p&gt;




&lt;p&gt;👉 This post opens the series on building HandyFEM from scratch. The upcoming entries will cover Jira planning, Sprint 0 setup, and the integration of Vercel v0 into the workflow. Follow the hashtag &lt;code&gt;#HandyFEMapp&lt;/code&gt; if you don’t want to miss the progress!  &lt;/p&gt;




&lt;h2&gt;
  
  
  📚 HandyFEM App Series
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Previous:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://dev.to/constanza_diaz_dev/building-a-full-stack-web-app-from-scratch-first-steps-gi8"&gt;Building a Full Stack Web App from scratch: First Steps&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
🔗 &lt;strong&gt;Next:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://dev.to/constanza_diaz_dev/from-idea-to-specs-planning-handyfems-architecture-with-claudeai-specs-driven-development-1lfd"&gt;From Idea to Specs: Planning HandyFEM's Architecture with Claude.ai - Specs Driven development.&lt;/a&gt;&lt;/em&gt; &lt;/p&gt;

</description>
      <category>ux</category>
      <category>productivity</category>
      <category>ai</category>
      <category>handyfemapp</category>
    </item>
  </channel>
</rss>
