DAY 004 / THREE LOVABLE PROTOTYPES
Three Lovable prototypes: turning product questions into working systems
How The Last Press, Psych Lab, and Borrowed Brain turn game state, questionnaire authoring, and decision support into deployed products—and what their audits still block.
01 / THREE SHIPPED PROTOTYPES
The same build loop produced three very different authority problems.
The Last Press, Psych Lab, and Borrowed Brain are not mockups. Each is a deployed React product with authentication, persistent data, and a real interaction loop. Their public repositories also show why “deployed” and “ready to launch” are different claims.
I audited each repository at one immutable commit, installed its locked dependencies in a fresh clone, ran the available build and static checks, inspected its security signals, and then tested only the live behaviors reported here. Source inspection establishes how the code is designed; live observation establishes only what the deployed page actually showed during the audit.
GLOBAL GAME
The Last Press
One shared countdown, scarce presses, realtime resets, and a last-presser winner.
AUTHORING SYSTEM
Psych Lab
AI-assisted questionnaire drafting with deterministic participant scoring.
DECISION TOOL
Borrowed Brain
Fourteen reasoning lenses interrogate and debate one consequential choice.
03 / THE LAST PRESS
The database owns the button. Nothing durable owns the moment the clock expires.
The public UI calls the game The Last Person: everyone watches one global timer, an eligible player spends a scarce press to reset it, and the most recent presser wins when time reaches zero. Free accounts receive one monthly press; membership adds ten. Paddle supplies subscription checkout, webhooks, cancellation, and the customer portal—not game state authority.
The strongest implementation choice is the press itself. The authenticated server function calls one SECURITY DEFINER PL/pgSQL function. That function refills and locks the player row, checks bans, allowance, and a two-second rate limit, settles any already-expired season, locks the chosen season, resets its expiration, decrements the allowance, updates season participation, and appends the press record inside one database transaction.
-- Condensed from press_button(_user_id).
PERFORM public.refill_allowance(_user_id);
SELECT * INTO p FROM public.profiles
WHERE id = _user_id FOR UPDATE;
PERFORM public.settle_seasons();
SELECT * INTO s FROM public.seasons
WHERE status IN ('active', 'pending')
ORDER BY CASE WHEN status = 'active' THEN 0 ELSE 1 END
LIMIT 1 FOR UPDATE;
new_exp := now() + (s.duration_ms || ' milliseconds')::interval;
UPDATE public.seasons SET
status = 'active', timer_expires_at = new_exp,
last_press_at = now(), last_presser_id = _user_id,
total_presses = total_presses + 1
WHERE id = s.id;
UPDATE public.profiles
SET presses_remaining = presses_remaining - 1
WHERE id = _user_id;
INSERT INTO public.presses (...)
VALUES (...);Browsers do not decrement a canonical timer. They fetch season timestamps, correct local clock drift from a server-time function, render a smooth local countdown, poll every fifteen seconds, and invalidate the query when Supabase Realtime reports a season change or press.
// The browser smooths the display; the server supplies the clock.
const query = useQuery({
queryKey: ["game"],
queryFn: fetchSnapshot,
refetchInterval: 15_000,
});
useEffect(() => {
void getServerTime().then((result) => setClockOffset(result.now));
}, []);
const channel = supabase
.channel("last-person-live-…")
.on("postgres_changes", { table: "seasons", event: "*" }, invalidate)
.on("postgres_changes", { table: "presses", event: "INSERT" }, invalidate)
.subscribe();A second boundary is more serious than a display defect. The owner-only profile policy limits which row a signed-in user may update, but the table grant does not limitwhich columns. The same row contains membership, ban state, and remaining presses. Direct Supabase access can therefore bypass the intended server functions.
GRANT SELECT, UPDATE ON public.profiles TO authenticated;
CREATE POLICY "update own profile"
ON public.profiles FOR UPDATE TO authenticated
USING (id = auth.uid())
WITH CHECK (id = auth.uid());
-- Row ownership is checked, but writable columns are not restricted.
-- The same row contains is_member, banned, and presses_remaining.The mobile audit also reproduced a 422-pixel document inside a 390-pixel viewport, clipping the giant timer. On /players/Saber, “Closest press” renderedInfinity:NaN:NaN:NaN. The pinned profile code checks whether any presses exist, then applies Math.min to the subset with positive remaining time; if all values are zero, that subset is empty and Math.min(...[]) returns infinity.
04 / PSYCH LAB
AI authors the instrument; fixed code scores the person.
Psych Lab lets a creator describe an established instrument or a new construct, generate a questionnaire specification, review the draft, publish it behind a join code, collect responses, and sell plans or marketplace access through Stripe. The important design choice is that participant answers never return to the authoring model.
Questionnaire generation streams a potentially large JSON response from the Lovable AI gateway. Before the result can become a test, deterministic coercion normalizes mechanical shape errors, Zod and cross-field checks validate the complete contract, and targeted repair messages feed exact failures back to the model. There are at most four drafting attempts; later attempts lower the temperature and the final attempt escalates to GPT-5.4 when the creator selected another model.
const FALLBACK_MODEL = "openai/gpt-5.4";
const MAX_ATTEMPTS = 4;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const model =
attempt === MAX_ATTEMPTS && requestedModel !== FALLBACK_MODEL
? FALLBACK_MODEL
: requestedModel;
const temperature = attempt >= 3 ? Math.min(requestedTemperature, 0.2) : requestedTemperature;
const text = await callModel(messages, model, temperature);
const candidate = coerceSpec(extractJson(text));
const { spec, errors } = validateSpec(candidate, { requireVisuals: true });
if (spec) return { spec, attempts: attempt, history };
messages.push({ role: "user", content: buildRepairMessage(errors, attempt >= 3) });
}“Human edit” is narrower than a full psychometric editor at this commit. Creators can review every item, scoring band, interpretation, and the full JSON; they can override visual style and marketplace metadata, regenerate art direction, import a revised valid JSON spec, and choose whether to publish. The Test Detail page does not expose inline editing for every question even though a validated saveTestSpec server function exists.
Once a respondent submits answers, ordinary TypeScript validates scale bounds, applies reverse scoring, aggregates by sum or mean, maps scores into declared bands, and evaluates attention checks. The model does not diagnose, rank, or reinterpret that person at runtime.
const scoreOf = (id: string, reverse: boolean) => {
const raw = Number(responses[id]);
return reverse ? max + min - raw : raw;
};
const values = items.map((item) => scoreOf(item.id, item.reverse_scored));
const score = method === "sum"
? values.reduce((a, b) => a + b, 0)
: values.reduce((a, b) => a + b, 0) / values.length;
const failedItems = attentionChecks
.filter((item) => Number(responses[item.id]) !== Number(item.expected_answer))
.map((item) => item.id);
return { score, band: bandFor(score, ranges), failedItems };Supabase stores tests, attempts, generation jobs, usage meters, subscriptions, listings, premium reports, creator earnings, and audit data under RLS. Stripe checkout and signed webhooks update plans, one-time report access, add-on credits, marketplace earnings, refunds, and disputes through a server-only connector gateway.
The generation_jobs table makes progress visible, but it does not make execution durable. The initiating server request inserts a running row and then directly awaits the whole AI job. There is no queue consumer or background worker to resume an interrupted run.
const { data: job } = await supabase
.from("generation_jobs")
.insert({ creator_id: userId, status: "running", ...request })
.select("id")
.single();
// The row persists progress, but this request still owns the work.
const result = await runGenerationJob({
jobId: job.id,
userId,
...request,
});
return result.ok
? { jobId: job.id, testId: result.testId }
: { jobId: job.id, errors: result.errors };05 / BORROWED BRAIN
A staged argument produces a decision board, not a decision.
Borrowed Brain contains fourteen authored reasoning lenses and six preset councils. A user enters one decision, adds optional context, seats two to five brains or accepts an AI recommendation, answers one question from each brain, watches a three-round debate, and receives a board of agreements, root disagreements, assumptions, strongest arguments, the least reversible mistake, and a smallest next action.
Each brain is more than a tone prompt. Its source record defines priorities, beliefs, decision rules, characteristic questions, blind spots, conditions for changing its mind, numeric tendencies, and a time horizon. The orchestrator requests JSON for a named schema, extracts it defensively, validates it with Zod, and gives one repair attempt with a compact account of the failed fields.
export async function generateStructured<T>(
schema: z.ZodType<T>,
system: string,
user: string,
): Promise<T> {
let lastIssue = "";
for (let attempt = 0; attempt < 2; attempt++) {
const raw = await callGateway(messagesFor(system, user, lastIssue));
const parsed = schema.safeParse(extractJson(raw));
if (parsed.success) return parsed.data;
lastIssue = parsed.error.issues
.slice(0, 6)
.map((issue) => issue.message)
.join("; ");
}
throw new AiError("The table could not organise its thoughts.", 500);
}The debate is intentionally staged. Round one asks each brain independently, in parallel. Round two asks a clerk to identify genuine disagreements by assumption, risk tolerance, time horizon, probability, values, opportunity cost, or definition of success. Round three gives every brain the transcript and records whether the argument actually changed its mind.
// Round 1: independent positions arrive in parallel.
const positions = await Promise.all(
brainIds.map((brainId) => generatePositionForBrainFn({ data: { ...base, brainId } })),
);
// Round 2: one clerk finds real root disagreements.
const debate = await generateCrossExaminationFn({ data: { ...base, positions } });
// Round 3: each brain can move—or explicitly hold—after hearing the table.
const finalPositions = await Promise.all(
brainIds.map((brainId) =>
generateFinalPositionForBrainFn({ data: { ...base, brainId, positions, debate } }),
),
);An anonymous session lives in localStorage, so the core flow does not require an account. Signing in is required only to save to Supabase, revisit a decision, record the eventual outcome, or share a redacted/full board. That is a thoughtful conversion boundary, but the cloud mutations depend on RLS for ownership rather than also filtering byuser_id, and public share slugs use Math.random rather than a cryptographically strong token.
// Anonymous work is device-bound until the user chooses to save.
const KEY = "borrowed-brain:sessions";
window.localStorage.setItem(KEY, JSON.stringify(allSessions));
// A signed-in update identifies the row; ownership is enforced by RLS.
await context.supabase
.from("decisions")
.update(patch)
.eq("id", decisionId);
// Public links use a short, non-cryptographic random suffix.
share_slug =
shareMode === "private"
? null
: decisionId.slice(0, 8) + Math.random().toString(36).slice(2, 8);06 / AI BOUNDARIES
The useful question is not “does it use AI?” but “what may the model decide?”
| Product | Model role | Model is not authoritative for | Control after generation |
|---|---|---|---|
| The Last Press | None in the game loop | Timer, allowance, winner, membership | PostgreSQL locks and provider webhooks |
| Psych Lab | Drafts a questionnaire artifact | Participant answers and scores | Coercion, Zod, repair, review, deterministic arithmetic |
| Borrowed Brain | Asks, argues, revises, synthesizes | The user’s final choice or professional advice | Zod schemas, explicit uncertainty, user-authored decision |
Psych Lab draws the cleanest boundary: AI can help author the measuring instrument, but a respondent’s result is reproduced from fixed code. Borrowed Brain necessarily leaves more judgment inside the model, so it compensates with distinct worldviews, explicit assumptions, confidence, change-of-mind records, and a final “You decide” step. Those are useful design constraints, not evidence that model output is true.
07 / DATA AND MONEY
Every durable row is a promise about authority, privacy, and recovery.
- The Last Press
- Public profiles, seasons, press history, and realtime state make the game observable. The atomic RPC is the right mutation boundary; the broad profile UPDATE grant is not. Paddle records membership state, but a timer executor must close seasons independently of viewer traffic.
- Psych Lab
- Creator identity, generated specs, participant attempts, usage, subscriptions, purchases, and earnings share one Supabase domain. Stripe’s signed webhook is the money authority; generation-job rows should be paired with resumable execution before users depend on them.
- Borrowed Brain
- Anonymous work remains on one device. Supabase receives it only after an authenticated save, and sharing has four disclosure modes. That minimizes collection, but share-token entropy, redaction, and RLS ownership need explicit tests.
The shared Supabase template is productive because auth, RLS, Realtime, JSONB, and generated types arrive together. It is also easy to mistake “RLS is enabled” for “the data boundary is correct.” Policies, grants, server-function filters, public views, and storage of sensitive free text must be reviewed as one system.
08 / INDEPENDENT VERIFICATION
All three compile. None has earned a green release gate.
# The Last Press and Psych Lab
npm ci
npm run build
npm run lint
# Psych Lab also received a separate strict TypeScript check.
npx tsc --noEmit
# Borrowed Brain
bun install --frozen-lockfile
bun run build
bun x tsc --noEmit
bun run lint
# Repository inventory also checked for:
# - LICENSE / package license declarations
# - test scripts and test/spec files
# - hosted CodeQL checks and open alerts
# - live route reachability, separately from source behavior| Evidence | The Last Press | Psych Lab | Borrowed Brain |
|---|---|---|---|
| Pinned commit | 169df55 | b47cfa4 | dadf92f |
| Production build | Pass | Pass | Pass |
| Strict TypeScript | Not separately recorded | Pass | Pass |
| Repository tests | None found | None found | None found |
| Lint | 529 problems: 523 errors, 6 warnings | 1,372 problems: 1,362 errors, 10 warnings | 374 problems: 368 errors, 6 warnings |
| Declared OSS license | None | None | None |
| CodeQL | Hosted analysis passed | Hosted analysis passed; 6 open high alerts | Hosted analysis passed; public alert list required authentication |
09 / LAUNCH GATES
Turn the most important assumptions into required evidence.
- 01Declare how each repository may be used, modified, and redistributed.
- 02Add automated unit, integration, and browser tests around each product’s authority path.
- 03Make typecheck, lint, tests, build, and unresolved security alerts merge gates.
- 04Give timer settlement and long AI generation durable, idempotent executors with recovery.
- 05Audit RLS, grants, share tokens, public projections, and webhook replay behavior.
- 06Re-run mobile, accessibility, privacy, billing, abuse, and domain-specific safety reviews.
“No public launch” is not a verdict on whether the prototypes are worth continuing. It is a sequencing decision: preserve the fast product learning, then add evidence where users would otherwise be asked to trust a timer, a psychological result, or consequential advice.
10 / PINNED SOURCE MAP
The claims above stay attached to inspectable files.
- The Last Press held/degraded deployment, retained as review evidence
- Psych Lab held/degraded deployment and canonical redirect, retained as review evidence
- Borrowed Brain live deployment
- The Last Press CodeQL analysis
- Psych Lab CodeQL analysis
- Borrowed Brain CodeQL analysis
Repository claims in this note come from the three pinned public commits and fresh-clone checks. Live claims are explicitly labeled observations from the deployed URLs. A passing build or hosted scanner is reported as exactly that; it is not promoted into proof of correctness, security, psychometric validity, or production readiness.
11 / WHAT I WOULD KEEP
Fast prototyping is most valuable when it reveals the next hard boundary.
I would keep the Last Press database transaction, Psych Lab’s separation between AI authoring and deterministic scoring, and Borrowed Brain’s local-first path plus explicit debate structure. Those are product-shaped decisions, not generic generated scaffolding.
I would also keep the audit posture: pin the source, reproduce the build, test the live path, read the policies, inspect the worker boundary, and preserve the difference between “I saw it load” and “I proved it safe.” Lovable made three ambitious ideas tangible quickly. The next milestone is to make their most consequential promises enforceable.
The shared lesson is simple: use the prototype to discover where authority belongs, then make that boundary survive races, retries, refreshes, adversaries, and time.