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 │
└──────────────────────────────────────────────────────────────┘
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',
},
});
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;
}
}
}
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' } })
);
}
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'
);
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}
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]
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.
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
- Cloudflare Workers Cache API
- OpenAI o4-mini model documentation
- Svelte 5 Runes — $bindable
- Server-Sent Events specification (WHATWG)
- DSGVO Article 4 — definition of personal data
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)
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.