Felipe Figueiredo
← All Projects

Project

Strategic Council

Seven adversarial AI advisors argue over a business idea. The engineering is in what holds when the model, the network, or hydration misbehaves.

Next.js 16React 19TypeScriptTailwind CSS 4ZodFramer MotionGeminiClaudeUpstash RedisCloudflare TurnstileVitestVercel
Strategic Council

Ask a language model to evaluate a business idea and it agrees with you. Ask it seven times and it agrees with you seven times. Strategic Council forces the disagreement into the structure: seven advisors with fixed remits and conflicting stances — Favorable, Cautious, Critical — argue past each other before a collective verdict lands, with five-year projections across three scenarios.

The interesting engineering is not the prompt. It is everything that has to hold when the model, the network, or the browser does something unexpected.

Technical Decisions

A Bug That Only Existed in Italian

The page worked in Portuguese and broke when a visitor picked Italian. The cause was one line reading localStorage during render. The server has no window, so it rendered the default locale while the client rendered Italian — and React 19 discards the entire tree on a hydration mismatch rather than patching it. Moving the read into an effect cured the crash and replaced it with a flash of the wrong language and a cascading render. useSyncExternalStore is the actual answer: it renders the server snapshot during hydration and swaps in the real value in the same commit. No mismatch, no effect, no fourth copy of the locale to keep in sync.

Schema That Checks Meaning, Not Just Shape

Model output is parsed against Zod before anything renders — but a length check is not enough. Seven advisors where one slug repeats passes length(7) and then renders two React children with the same key while a third advisor vanishes silently. The schema requires the seven slugs to be distinct and the five projection years to be distinct, so that failure is caught at the boundary instead of surfacing as a console warning and a missing card.

The Idea Is Data, Not Instruction

The rules travel in systemInstruction, the submitted idea sits inside a delimited block, and the delimiter is stripped from the input so it cannot be closed early. An “ignore all previous instructions” typed into the form becomes something the council describes, not something it obeys. Concatenating the two into one string is the default and it is the vulnerability.

Errors Travel as Codes

The API answers { code: 'LLM_BUSY' } and the client owns the wording. Before that, a four-language interface returned prose from the server, so a Spanish visitor could hit a Portuguese error, and an SDK failure shipped error.message straight to the browser. A localised product cannot let the server write user-facing text.

A Ceiling a Bug Cannot Route Around

Per-IP rate limiting in the proxy, Cloudflare Turnstile on the route, and a global daily counter in Redis — because a rotating pool of IPv6 addresses defeats a per-IP limit on its own, and one /64 hands out more addresses than anyone can exhaust. Every guard is enabled by an environment variable, so a missing one used to turn the guard off and keep serving a paid endpoint to the open internet. In production the route now refuses to serve instead of failing open.

One Grid, Two Shapes

The advisor board rendered as two sibling trees toggled with md:hidden — both permanently in the DOM, so every advisor mounted twice: doubled animations, and a screen reader announcing all seven advisors and then all seven again. A 24-column track produces the same four-over-three arrangement from a single list.

Interactive SVG Mandala

The seven-segment landing is SVG with generated arc paths and clipPath definitions. Hover is pure CSS clip-path transitions — no canvas, no hit-testing, no JavaScript in the interaction path. Framer Motion only handles entrances.

Stateless by Design

Sessions live in sessionStorage — no database, no auth, no server-side state, which also means a shared link has nothing to show. The results page says so in the visitor's language and offers the way forward, rather than rendering an empty board.

Verification

71 tests under Vitest cover the schema refinements, the injection defence, the provider retry policy, the error-code contract, and a check that every locale defines every key — the failure that only appears once someone switches language.