DEV Community

Cover image for Founder Compass: Designing a Stateless AI Profiler with Svelte 5 and Cloudflare Workers
Mihai Adrian Mateescu
Mihai Adrian Mateescu

Posted on • Originally published at me-mateescu.de

Founder Compass: Designing a Stateless AI Profiler with Svelte 5 and Cloudflare Workers

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.

TL;DR

  • 12-question profiler that maps founder constraints (risk tolerance, runway comfort, business model preference) to a structured AI report
  • 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
  • SSE streaming via o4-mini — report starts rendering in the browser within ~2 seconds of submission
  • 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

The Real Reason Most Founders Struggle

Most founders do not fail because they lack ambition, market knowledge, or technical skills.

They fail because they choose a business model that is structurally incompatible with who they are.

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.

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.

Catching it earlier changes the outcome.


Why Existing Tools Miss the Point

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.

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.

Founders don't need more motivation. They need alignment.


The Architecture: Stateless by Design

Before writing a line of UI code, two architectural constraints were non-negotiable.

No database, no user accounts. 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.

Rate limiting must be GDPR-compliant. 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.

┌──────────────────────────────────────────────────────────────┐
│  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             │
└──────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

SSE Streaming from a Cloudflare Worker

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.

Worker-side: forward event: delta messages as they come in from the OpenAI API:

const stream = new ReadableStream({
  async start(controller) {
    const enc = new TextEncoder();

    for await (const event of openaiStream) {
      const text = event.choices?.[0]?.delta?.content ?? '';
      if (text) {
        controller.enqueue(
          enc.encode(`event: delta\ndata: ${JSON.stringify({ text })}\n\n`)
        );
      }
    }

    controller.enqueue(enc.encode(`event: done\ndata: {}\n\n`));
    controller.close();
  },
});

return new Response(stream, {
  headers: {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
  },
});
Enter fullscreen mode Exit fullscreen mode

Client-side: accumulate deltas into a Svelte reactive string, which re-renders the Markdown in real time via marked:

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const segments = buffer.split('\n\n');
  buffer = segments.pop() || '';

  for (const segment of segments) {
    let eventType = '', eventData = '';
    for (const line of segment.split('\n')) {
      if (line.startsWith('event: ')) eventType = line.slice(7).trim();
      else if (line.startsWith('data: ')) eventData = line.slice(6).trim();
    }
    if (eventType === 'delta' && eventData) {
      const parsed = JSON.parse(eventData);
      report = (report || '') + parsed.text;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

GDPR-Compliant Rate Limiting via Cache API

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 max-age of 604,800 seconds. It expires automatically, no cleanup job required.

async function hashIP(ip: string): Promise<string> {
  const data = new TextEncoder().encode(`compass-quota:${ip}`);
  const buf = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(buf))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

async function hasWeeklyQuota(ip: string): Promise<boolean> {
  const hash = await hashIP(ip);
  const cache = await caches.open('compass-quota-v1');
  const cached = await cache.match(
    new Request(`https://quota.internal/__compass_quota/${hash}`)
  );
  return cached !== undefined;
}

async function setWeeklyQuota(ip: string): Promise<void> {
  const hash = await hashIP(ip);
  const cache = await caches.open('compass-quota-v1');
  await cache.put(
    new Request(`https://quota.internal/__compass_quota/${hash}`),
    new Response('1', { headers: { 'Cache-Control': 'max-age=604800' } })
  );
}
Enter fullscreen mode Exit fullscreen mode

The frontend adds a second independent guard: lastGeneratedAt in localStorage. isWeeklyCooldownActive() checks whether 7 days have elapsed client-side — instant feedback without a round-trip.


Svelte 5 Runes: One Integer Drives the Entire Flow

The entire state machine has three phases, derived from a single counter:

let phase = $derived<'quiz' | 'consent' | 'report'>(
  currentStep < TOTAL ? 'quiz' :
  currentStep === TOTAL ? 'consent' :
  'report'
);
Enter fullscreen mode Exit fullscreen mode

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

// Parent passes individual slots down:
bind:selectedKey={answers[currentStep].selectedKey}
bind:customText={answers[currentStep].customText}
Enter fullscreen mode Exit fullscreen mode

Prompt Design: Structure Forces Specificity

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:

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]
Enter fullscreen mode Exit fullscreen mode

Key decisions:

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

The model used is o4-mini with max_output_tokens: 2500. Note: this model does not accept the temperature parameter.


The Consent Step

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.

Founder Compass consent step — answer summary, usage notice with weekly rate limit, GDPR privacy disclosure, and submit button

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.


Key Takeaways

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

References


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

Top comments (1)

Collapse
 
mihai82adrian profile image
Mihai Adrian Mateescu

Thanks for reading!

For anyone curious about the practical side beyond the architecture:

• Live tool:
me-mateescu.de/tools/founder-compass

• Full article and context on my site:
me-mateescu.de/blog/founder-compas...

I’m currently building a small ecosystem of privacy-first finance & founder tools, so if you’re experimenting with similar ideas or architectures, I’d genuinely enjoy comparing notes.