<?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: Mihai Adrian Mateescu </title>
    <description>The latest articles on DEV Community by Mihai Adrian Mateescu  (@mihai82adrian).</description>
    <link>https://dev.to/mihai82adrian</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%2F3784316%2F1cdeeb34-3979-4d8d-b90d-bddaae5f8392.png</url>
      <title>DEV Community: Mihai Adrian Mateescu </title>
      <link>https://dev.to/mihai82adrian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mihai82adrian"/>
    <language>en</language>
    <item>
      <title>Founder Compass: Designing a Stateless AI Profiler with Svelte 5 and Cloudflare Workers</title>
      <dc:creator>Mihai Adrian Mateescu </dc:creator>
      <pubDate>Wed, 25 Feb 2026 19:54:29 +0000</pubDate>
      <link>https://dev.to/mihai82adrian/founder-compass-designing-a-stateless-ai-profiler-with-svelte-5-and-cloudflare-workers-5g0o</link>
      <guid>https://dev.to/mihai82adrian/founder-compass-designing-a-stateless-ai-profiler-with-svelte-5-and-cloudflare-workers-5g0o</guid>
      <description>&lt;p&gt;I wanted to build an AI tool that respects user privacy by design — no accounts, no database, no stored profiles. I work at the intersection of finance and software in the DACH ecosystem, which shaped many of the architectural decisions described here. This article walks through how Founder Compass works from an engineering perspective: architecture, streaming strategy, and prompt design.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;12-question profiler that maps founder constraints (risk tolerance, runway comfort, business model preference) to a structured AI report&lt;/li&gt;
&lt;li&gt;Single Cloudflare Worker, no database: rate limiting runs on the Cache API with a SHA-256-hashed IP key and a 604,800-second TTL&lt;/li&gt;
&lt;li&gt;SSE streaming via &lt;code&gt;o4-mini&lt;/code&gt; — report starts rendering in the browser within ~2 seconds of submission&lt;/li&gt;
&lt;li&gt;Quiz state and generated report persist exclusively in localStorage — the only data that leaves the browser is the 12 anonymized answers, transmitted once on explicit user consent&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Real Reason Most Founders Struggle
&lt;/h2&gt;

&lt;p&gt;Most founders do not fail because they lack ambition, market knowledge, or technical skills.&lt;/p&gt;

&lt;p&gt;They fail because they choose a business model that is structurally incompatible with who they are.&lt;/p&gt;

&lt;p&gt;A person who genuinely needs financial security within three months cannot bootstrap a product business that takes 18 months to reach profitability. A solo operator who works best with deep focus cannot build a business that requires constant client management and relationship selling. These are not motivational problems. They are alignment problems.&lt;/p&gt;

&lt;p&gt;The mismatch tends to show up late — after the savings are spent, after the first customers are acquired at the wrong margin, after the founder realizes the model they chose requires skills, time, or risk tolerance they do not actually have.&lt;/p&gt;

&lt;p&gt;Catching it earlier changes the outcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Existing Tools Miss the Point
&lt;/h2&gt;

&lt;p&gt;Startup personality quizzes tell you what type you are. They rarely tell you whether your type is compatible with the business you are about to build.&lt;/p&gt;

&lt;p&gt;Generic AI tools like ChatGPT produce generic output — because they receive generic input. Without a structured intake that forces the user to articulate real constraints, the output defaults to frameworks that fit everyone and therefore help no one.&lt;/p&gt;

&lt;p&gt;Founders don't need more motivation. They need alignment.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: Stateless by Design
&lt;/h2&gt;

&lt;p&gt;Before writing a line of UI code, two architectural constraints were non-negotiable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No database, no user accounts.&lt;/strong&gt; Every quiz answer is submitted once, processed once, and discarded on the server. The generated report is never stored on any server — it is written directly into the browser's localStorage as it streams in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting must be GDPR-compliant.&lt;/strong&gt; A weekly quota (1 report per 7 days) is necessary to prevent abuse. But storing IP addresses creates a GDPR obligation. The solution: hash the IP with SHA-256, use the hash as a Cache API key, let the entry expire after 7 days. No raw IP is ever written to any storage. No deletion workflow needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────┐
│  Browser (Svelte 5 island, client:visible)                   │
│                                                              │
│  FounderCompassApp                                           │
│   ├── phase: 'quiz' | 'consent' | 'report'  ($derived)       │
│   ├── answers[12]                           ($state)         │
│   ├── lastGeneratedAt: number | null        ($state)         │
│   └── weeklyLocked: boolean                 ($derived)       │
│                                                              │
│  localStorage: quiz progress + completed report              │
│  (never leaves the browser)                                  │
└───────────────────────────┬──────────────────────────────────┘
                            │ POST /api/compass
                            │ { answers: [12 × {dimension, selectedKey, label}] }
                            │ transmitted once · processed once · discarded
                            │
┌───────────────────────────▼──────────────────────────────────┐
│  Cloudflare Worker: /api/compass                             │
│                                                              │
│  1. Hash client IP → SHA-256 hex (non-reversible)            │
│  2. Check Cache API for quota key (TTL: 604,800s / 7 days)   │
│  3. Build structured prompt from 12 answers                  │
│  4. Stream o4-mini response via SSE (event: delta)           │
│  5. Set Cache API quota key on success                       │
│                                                              │
│  No database · No user table · No stored answers             │
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  SSE Streaming from a Cloudflare Worker
&lt;/h2&gt;

&lt;p&gt;The report runs to 600–900 words. Waiting for the full LLM completion before displaying anything produces a 15–25 second blank screen — unacceptable. SSE allows the Worker to begin writing the response immediately as tokens arrive.&lt;/p&gt;

&lt;p&gt;Worker-side: forward &lt;code&gt;event: delta&lt;/code&gt; messages as they come in from the OpenAI API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;openaiStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`event: delta\ndata: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`event: done\ndata: {}\n\n`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client-side: accumulate deltas into a Svelte reactive string, which re-renders the Markdown in real time via &lt;code&gt;marked&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segment&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eventData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;eventData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&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="nx"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;eventData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&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;h2&gt;
  
  
  GDPR-Compliant Rate Limiting via Cache API
&lt;/h2&gt;

&lt;p&gt;Standard rate limiting writes an IP address to a storage layer. That's personal data under GDPR. The alternative: SHA-256 hash the IP, store the hash as a Cache API key with a &lt;code&gt;max-age&lt;/code&gt; of 604,800 seconds. It expires automatically, no cleanup job required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hashIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`compass-quota:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasWeeklyQuota&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&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;hashIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compass-quota-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://quota.internal/__compass_quota/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setWeeklyQuota&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&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;hashIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compass-quota-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://quota.internal/__compass_quota/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max-age=604800&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="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;The frontend adds a second independent guard: &lt;code&gt;lastGeneratedAt&lt;/code&gt; in localStorage. &lt;code&gt;isWeeklyCooldownActive()&lt;/code&gt; checks whether 7 days have elapsed client-side — instant feedback without a round-trip.&lt;/p&gt;




&lt;h2&gt;
  
  
  Svelte 5 Runes: One Integer Drives the Entire Flow
&lt;/h2&gt;

&lt;p&gt;The entire state machine has three phases, derived from a single counter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$derived&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quiz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;report&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;TOTAL&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quiz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;currentStep&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;TOTAL&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;report&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;currentStep&lt;/code&gt; increments on each "Next" click. No router, no nested conditionals, no separate page per question. With &lt;code&gt;$bindable&lt;/code&gt;, &lt;code&gt;QuizStep&lt;/code&gt; reads and writes parent state directly — no event dispatch boilerplate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Parent passes individual slots down:&lt;/span&gt;
&lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;selectedKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;selectedKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nl"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;customText&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentStep&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;customText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Prompt Design: Structure Forces Specificity
&lt;/h2&gt;

&lt;p&gt;The quality of the generated report depends entirely on the system prompt. The prompt mandates exactly five sections with German headers, using directive language to prevent the model from hedging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Du bist ein knallharter Digital Finance Architect und Startup-Mentor.
Du gibst keine generischen Ratschläge.

Antworte IMMER mit exakt diesen 5 Abschnitten:

## 1. Der Gründer-Archetyp
## 2. Das ideale Geschäftsmodell
   Wenn du dir unsicher bist: ERFINDE ein konkretes Geschäftsmodell.
   Kein "Es hängt davon ab."
## 3. Die finanziellen Unit Economics
## 4. Das größte Risiko (Blind Spot)
## 5. Nächster konkreter Schritt
   [Eine Handlung in den nächsten 7 Tagen]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mandatory section headers&lt;/strong&gt; prevent the model from collapsing or reordering sections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"ERFINDE ein konkretes Geschäftsmodell"&lt;/strong&gt; overrides the model's default hedging behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Section 5 mandates one concrete action within 7 days&lt;/strong&gt; — not a strategy, not a framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model used is &lt;code&gt;o4-mini&lt;/code&gt; with &lt;code&gt;max_output_tokens: 2500&lt;/code&gt;. Note: this model does not accept the &lt;code&gt;temperature&lt;/code&gt; parameter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Consent Step
&lt;/h2&gt;

&lt;p&gt;Before any data leaves the browser, the user sees a full consent screen: answer summary, rate limit notice, GDPR disclosure, and an explicit opt-in checkbox.&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%2Fm4mexzivmohq7ivu1vsb.webp" 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%2Fm4mexzivmohq7ivu1vsb.webp" alt="Founder Compass consent step — answer summary, usage notice with weekly rate limit, GDPR privacy disclosure, and submit button" width="800" height="722"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The submit button is disabled until the checkbox is checked. The weekly cooldown is shown inline if the user has already generated a report in the past 7 days.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stateless architecture simplifies both compliance and deployment&lt;/strong&gt; — no schema to migrate, no user table to secure, no GDPR deletion workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE streaming dramatically improves perceived performance&lt;/strong&gt; for AI tools — the report starts rendering within ~2 seconds instead of waiting 20+ seconds for full completion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured prompts matter more than model choice&lt;/strong&gt; — mandatory section headers and directive language produce specific, non-hedged output regardless of which model you use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy-first design can be a competitive advantage, not a constraint&lt;/strong&gt; — SHA-256 hashed IP + Cache API TTL achieves rate limiting with zero personal data stored&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/workers/runtime-apis/cache/" rel="noopener noreferrer"&gt;Cloudflare Workers Cache API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/models/o4-mini" rel="noopener noreferrer"&gt;OpenAI o4-mini model documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://svelte.dev/docs/svelte/$bindable" rel="noopener noreferrer"&gt;Svelte 5 Runes — $bindable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://html.spec.whatwg.org/multipage/server-sent-events.html" rel="noopener noreferrer"&gt;Server-Sent Events specification (WHATWG)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32016R0679" rel="noopener noreferrer"&gt;DSGVO Article 4 — definition of personal data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you’re building something similar or experimenting with stateless architectures, I’d love to hear how you approached it.&lt;br&gt;
Source code is part of my open portfolio repository.&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>cloudflare</category>
      <category>ai</category>
      <category>privacy</category>
    </item>
    <item>
      <title>I'm Finance Ops, not a developer. I built a KoSIT-valid XRechnung generator in the browser anyway</title>
      <dc:creator>Mihai Adrian Mateescu </dc:creator>
      <pubDate>Sat, 21 Feb 2026 22:56:14 +0000</pubDate>
      <link>https://dev.to/mihai82adrian/im-finance-ops-not-a-developer-i-built-a-kosit-valid-xrechnung-generator-in-the-browser-anyway-4bn2</link>
      <guid>https://dev.to/mihai82adrian/im-finance-ops-not-a-developer-i-built-a-kosit-valid-xrechnung-generator-in-the-browser-anyway-4bn2</guid>
      <description>&lt;p&gt;My day job is Finance Ops and accounting. I'm not a software engineer by training.&lt;/p&gt;

&lt;p&gt;But in DACH, the gap between "what accountants need" and "what developers build" is real — and expensive. So I closed it myself.&lt;br&gt;
&lt;strong&gt;Live demo (local-first, zero tracking):&lt;/strong&gt; &lt;a href="https://me-mateescu.de/tools/xrechnung/" rel="noopener noreferrer"&gt;https://me-mateescu.de/tools/xrechnung/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem I kept hitting
&lt;/h2&gt;

&lt;p&gt;Germany's &lt;strong&gt;E-Rechnungspflicht&lt;/strong&gt; is no longer theoretical. B2G (Business-to-Government) invoicing has required a valid XRechnung for years, and B2B is rolling out in phases (reception from 2025, issuance for &amp;gt;€800k revenue from 2027, and broadly from 2028).&lt;/p&gt;

&lt;p&gt;A PDF — even a perfect one — is not enough.&lt;/p&gt;

&lt;p&gt;Every tool I found was one of three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise ERP&lt;/strong&gt; (SAP, DATEV): full XRechnung support, hundreds of euros/month, built for teams, not freelancers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS invoicing&lt;/strong&gt; (Lexoffice, sevDesk): subscription-based, your invoice data lives on their servers, lock-in by design&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI tools&lt;/strong&gt;: open-source Java/Python libraries that assume you know what &lt;code&gt;java -jar&lt;/code&gt; means&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The missing middle: a freelancer who needs to invoice the municipality of Hamburg once a quarter. A one-person consultancy that just won its first public-sector contract. A Kleinunternehmer who needs §19 UStG compliance without paying for DATEV.&lt;/p&gt;

&lt;p&gt;That's who I built this for. Also me.&lt;/p&gt;




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

&lt;p&gt;A browser-native XRechnung 3.0 generator. No server. No account. No data leaves your machine.&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%2Fpi7jt9r7wihfiqke28tr.webp" 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%2Fpi7jt9r7wihfiqke28tr.webp" alt="XRechnung generator — form on the left, live invoice preview on the right" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validates against KoSIT Validator v1.6.2 (Config v3.0.2) + EN 16931.&lt;/strong&gt;&lt;br&gt;
Exports: &lt;strong&gt;UBL 2.1&lt;/strong&gt; and &lt;strong&gt;UN/CEFACT CII&lt;/strong&gt; syntax.&lt;br&gt;
Generates a DIN 5008-style PDF alongside the XML.&lt;br&gt;
Supports Kleinunternehmer (§19 UStG), Standard VAT, and Reverse Charge (§13b UStG).&lt;/p&gt;

&lt;p&gt;🔗 Live: &lt;a href="https://me-mateescu.de/tools/xrechnung/" rel="noopener noreferrer"&gt;https://me-mateescu.de/tools/xrechnung/&lt;/a&gt;&lt;br&gt;&lt;br&gt;
📦 Source: &lt;a href="https://github.com/Mihai-82Adrian/portfolio-astro" rel="noopener noreferrer"&gt;https://github.com/Mihai-82Adrian/portfolio-astro&lt;/a&gt; (MIT)&lt;/p&gt;




&lt;h2&gt;
  
  
  Why local-first — and how I proved it
&lt;/h2&gt;

&lt;p&gt;Invoice data is high-signal: client names, project descriptions, amounts, IBANs.&lt;br&gt;
Most SaaS tools solve the trust problem by asking you to trust them. This tool solves it by not touching your data at all.&lt;/p&gt;

&lt;p&gt;Everything — XML generation, PDF rendering, pre-validation — runs in the browser.&lt;br&gt;
Your seller defaults persist in localStorage. Nothing else.&lt;/p&gt;

&lt;p&gt;The proof isn't marketing copy. It's the DevTools Network tab:&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%2Fp7p3ancsyo4rlhqot3tf.webp" 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%2Fp7p3ancsyo4rlhqot3tf.webp" alt="Chrome DevTools Network panel showing zero Fetch/XHR requests during the entire invoice generation flow — only a local PDF blob download is visible" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Zero outbound requests. Not "we encrypt everything" — just: nothing leaves.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I actually built this (the honest version)
&lt;/h2&gt;

&lt;p&gt;My role here is closer to &lt;strong&gt;architect and orchestrator&lt;/strong&gt; than "I code all day."&lt;/p&gt;

&lt;p&gt;I work from the compliance constraints down: read the EN 16931 spec, map the domain model in TypeScript types, define the validation rules, then specify the implementation.&lt;/p&gt;

&lt;p&gt;Build note (full transparency): parts of the implementation were AI-assisted (Claude Code/Codex), under my architecture, with manual review and validation loops.&lt;/p&gt;

&lt;p&gt;This workflow forced me to understand the standard deeply — because you cannot prompt your way past a KoSIT schema validation error you don't understand. When the validator rejects your XML with &lt;code&gt;BT-31 cardinality violation&lt;/code&gt;, you need to know what BT-31 is.&lt;/p&gt;

&lt;p&gt;The hardest part wasn't the code. It was building a typed domain model that produces both UBL 2.1 and UN/CEFACT CII output from the same &lt;code&gt;Invoice&lt;/code&gt; object — without branching logic everywhere:&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%2Fpzymwn84paxgs5tge3bf.webp" 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%2Fpzymwn84paxgs5tge3bf.webp" alt="VS Code split view: UBL 2.1 XML on the left, UN/CEFACT CII XML on the right — both generated from identical invoice data" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same invoice. Two syntax outputs. The domain model is syntax-agnostic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The deployment: zero backend, Cloudflare edge
&lt;/h2&gt;

&lt;p&gt;Every push to &lt;code&gt;master&lt;/code&gt; triggers a GitHub Actions build and deploys straight to &lt;strong&gt;Cloudflare Pages&lt;/strong&gt;. No servers, no containers, no databases.&lt;/p&gt;

&lt;p&gt;The static assets are cached at Cloudflare's edge — so latency is basically: "the file is already there."&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%2F91f7bmes0efedgm96to8.webp" 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%2F91f7bmes0efedgm96to8.webp" alt="Cloudflare Pages dashboard showing automatic deployments enabled for the portfolio-astro project, with me-mateescu.de live on the Production environment" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;One thing:&lt;/strong&gt; read the full EN 16931 + CIUS context before touching any code.&lt;/p&gt;

&lt;p&gt;I assumed XRechnung was "just XML with some German quirks." It's not. It's layered: EN 16931 Core + CIUS + KoSIT profile requirements. The URN identifiers alone took me two days to get right — and they’re a common cause of validation failures even when invoice data is correct.&lt;/p&gt;

&lt;p&gt;Typed constants (TypeScript &lt;code&gt;const&lt;/code&gt; values) eliminated an entire class of errors. Worth knowing upfront.&lt;/p&gt;




&lt;h2&gt;
  
  
  Questions for the community
&lt;/h2&gt;

&lt;p&gt;If you've worked with XRechnung, EN 16931, or Peppol:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What validation errors do you see most often in production?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UBL or CII — what do your clients' ERPs actually consume?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What's the one missing feature before this is useful to you?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Full technical breakdown (architecture, code, domain model):&lt;br&gt;
👉 &lt;a href="https://me-mateescu.de/blog/xrechnung-generator-local-first-en16931/" rel="noopener noreferrer"&gt;https://me-mateescu.de/blog/xrechnung-generator-local-first-en16931/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>svelte</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
