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.

02 / THE SHARED PATTERN

Lovable accelerated the shell. Product-specific boundaries still had to be designed.

The repositories share a recognizable generated foundation: React 19, TanStack Start, Router and Query, Vite, Nitro, Tailwind CSS, Radix-derived components, Supabase clients and auth middleware, plus Lovable’s build and deployment configuration. That commonality made navigation, forms, responsive surfaces, server functions, and cloud wiring fast to assemble.

Shared Lovable prototype build and runtime architectureA builder describes and reviews changes in Lovable. Lovable commits generated source to a GitHub repository, then deploys the repository to a web runtime. In production, a browser interface calls server or edge functions, which coordinate durable databases, realtime channels, AI gateways, or payment services. Browser-local storage remains an option for anonymous state that does not need to be shared.BUILD LOOPLOVABLE PROJECTprompt · inspect · refinevisual feedback loopGITHUB REPOSITORYgenerated source + historyinspectable outside the builderLOVABLE DEPLOYbuild + web runtimecustom or lovable.app domainVISITOR RUNTIMEBROWSER UIReact interaction surfaceanonymous state can stay localSERVER FUNCTIONSvalidate · authorize · mutatesecrets stay off the clientSHARED SERVICESdatabase · realtime · AIpayments when requiredLOCALSTORAGEprivate, device-bound continuitythe shared pattern is not a shared backend:each prototype chooses the smallest persistence and authority boundary it needs
One build loop, several product-shaped runtimesThe three prototypes share a prompt-driven build loop: Lovable turns conversations into source changes, GitHub keeps the inspectable history, and Lovable deploys the application. At runtime, browser UI calls server functions and uses a database or external service only where the product needs durable shared state.
Shared architecture, condensedtext
builder loop
Lovable project → GitHub source → Lovable deploy

application shell
React 19 + TanStack Start / Router / Query
Vite + Nitro + TypeScript
Tailwind CSS + Radix UI primitives

shared services, selected per product
Supabase auth + PostgreSQL + Row Level Security
Lovable AI gateway where generation is part of the product
Paddle or Stripe where money enters the system

The pattern is not a shared backend. The Last Press puts game authority in PostgreSQL; Psych Lab treats an AI-produced questionnaire as an artifact that must validate before use; Borrowed Brain keeps an anonymous decision on-device and makes cloud persistence optional. The scaffolding repeats. The source of truth does not.

LayerShared choiceWhere the products diverge
InterfaceReact 19, TanStack Router, Tailwind, Radix primitivesLive game, creator studio, or staged decision chamber
Server boundaryTanStack server functions and validated inputsAtomic SQL, AI artifact generation, or structured debate calls
PersistenceSupabase where shared state is neededAuthoritative game state, creator/respondent records, or optional saved decisions
CommerceProvider SDK behind a server boundaryPaddle membership, Stripe plans/marketplace, or none

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 Last Press authoritative round flowPlayer browsers send press commands while a server-time endpoint corrects their display clocks. A PostgreSQL function locks the active season and player profile, checks the cooldown and remaining allowance, resets the shared deadline, and appends the press in one transaction. The committed season and press rows are broadcast through realtime. When the timer expires, settlement can choose the last presser, but only another press or an admin action invokes the current settlement code; there is no independent scheduled executor.COMMANDSPLAYER BROWSERSpress intentnever the final countCLOCK + REALTIMEserver-time correctioncommitted updates onlySERVER MUTATION1 · lock season + profile2 · cooldown + allowance3 · reset ends_at + append pressone committed resultserializes racing clientsROUND RECORDseason · ends_at · last presserdatabase source of truthREALTIME EVENTbroadcast committed stateall clients convergeTIMER EXPIRESlast presser can become winnersettlement must be invokedNO SCHEDULED EXECUTORnext press / admin must wake itexpired season can remain openthe atomic press transaction is sounder than the lifecycle around it: expiry needs an independent, idempotent scheduled settlement path
The server owns each press; nothing independently wakes settlementEach press is a server-authoritative transaction: lock the active season and player profile, enforce cooldown and allowance, reset the deadline, and append the press. Realtime lets browsers converge, but an expired season still needs code to invoke settlement—and the audited build has no independent scheduler.

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.

Atomic press transaction, condensedsql · source
-- 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.

Server-clock and realtime convergence, condensedtsx · source
// 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.

Owner-only row, unrestricted profile columnssql · source
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.

Psych Lab authoring and respondent flowA creator prompt is sent through a streaming AI gateway. Returned JSON is extracted, mechanically coerced, and validated against a Zod schema. Validation errors trigger targeted repair attempts. A human then edits and approves the draft before publication behind a join code or marketplace listing. On the other side of a clear AI boundary, respondent answers are scored with fixed arithmetic, reverse scoring, bands, and attention checks. Respondent answers never return to the AI authoring path.AI-ASSISTED AUTHORINGPROMPTestablished or novelAI STREAMone JSON specCOERCErepair mechanical shapebefore judgmentZOD CONTRACTitems · scales · bands · visualsvalid → draftREPAIRtargeted errorsup to 4 draftsinvalidAI STOPS HEREHUMAN EDIT + APPROVALreview every item and interpretationpublish → join code / listingRESPONDENT PATH · NO MODEL CALLJOIN + ANSWERanonymous device identityvoluntary Likert responsesFIXED SCORINGreverse · sum / mean · bandsdeterministic attention checksRESULTnumeric bands + pre-written textoptional paid report / historyPRIVACY BOUNDARYrespondent answers never cross back into the AI authoring lane
AI writes the instrument; it does not judge the respondentPsych Lab uses AI to draft a questionnaire specification, then mechanically coerces and validates it with Zod, repairing failures before a human edits and publishes. Respondent answers stay beyond that authoring boundary: reverse scoring, bands, attention checks, and results are deterministic arithmetic rather than a new model judgment.

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.

Four-stage validation and repair loop, condensedts · source
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.

Participant scoring stays deterministic, condensedts · source
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.

A persisted job still runs inside the request, condensedts · source
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.

Borrowed Brain deliberation and persistence flowThe user defines a problem and constraints, answers interrogation questions, and receives five independent positions. Those positions cross-examine one another, revise into final positions, and populate a decision board that preserves agreements, disagreements, risks, and next moves. An anonymous user stores the session in browser local storage. An authenticated user may instead save it to Supabase for durable cross-device access.SETUPproblem · stakes · limitsdesired decisionINTERROGATIONexpose assumptionssharpen the questionINDEPENDENT POSITIONSfive lenses answer aloneno shared draft to anchor onCROSS-EXAMchallenge evidenceanswer objectionsFIVE LENSES · SEPARATE FIRST PASSES12345FINAL POSITIONSrevised after challengepreserve real disagreementDECISION BOARDagreements · tensions · risksoptions + next movePERSISTENCE CHOICEANONYMOUS · LOCALSTORAGEprivate to this browser and deviceOPTIONAL ACCOUNT · SUPABASEdurable, authenticated cross-device save
Five independent positions become one inspectable decisionA session begins with a concrete problem and constraints, then interrogates the premise before five lenses form independent positions. Cross-examination forces the positions to answer one another, after which final positions feed a decision board. Anonymous sessions remain on the device; signing in can optionally save the same session to Supabase.

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.

Schema-checked AI output with one retry, condensedts · source
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.

Three-round debate orchestration, condensedtsx · source
// 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.

Local-first session and optional cloud sharing, condensedts · source
// 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?”

ProductModel roleModel is not authoritative forControl after generation
The Last PressNone in the game loopTimer, allowance, winner, membershipPostgreSQL locks and provider webhooks
Psych LabDrafts a questionnaire artifactParticipant answers and scoresCoercion, Zod, repair, review, deterministic arithmetic
Borrowed BrainAsks, argues, revises, synthesizesThe user’s final choice or professional adviceZod 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.

Fresh-clone audit procedureshell
# 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
EvidenceThe Last PressPsych LabBorrowed Brain
Pinned commit169df55b47cfa4dadf92f
Production buildPassPassPass
Strict TypeScriptNot separately recordedPassPass
Repository testsNone foundNone foundNone found
Lint529 problems: 523 errors, 6 warnings1,372 problems: 1,362 errors, 10 warnings374 problems: 368 errors, 6 warnings
Declared OSS licenseNoneNoneNone
CodeQLHosted analysis passedHosted analysis passed; 6 open high alertsHosted analysis passed; public alert list required authentication

09 / LAUNCH GATES

Turn the most important assumptions into required evidence.

  1. 01Declare how each repository may be used, modified, and redistributed.
  2. 02Add automated unit, integration, and browser tests around each product’s authority path.
  3. 03Make typecheck, lint, tests, build, and unresolved security alerts merge gates.
  4. 04Give timer settlement and long AI generation durable, idempotent executors with recovery.
  5. 05Audit RLS, grants, share tokens, public projections, and webhook replay behavior.
  6. 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.

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.