What shipped
Every release of the Whirlwind template, straight from the repository buyers clone. Updates are included with every licence.
Unreleased
›AI replies render as formatted text.
Models answer in Markdown, and the chat and content writer showed it raw, so replies read as
**stars**,#signs and dashes.components/ui/Markdown.jsxrenders the parts replies use (paragraphs, headings, lists, quotes, code, bold, italics and links) as React elements, so model output can never inject HTML, and links are limited to http(s) and mailto. It copes with half-streamed text, and leavessnake_case,__init__and2*3*4alone. What the user types in the chat is still shown exactly as typed.›One header on every page, and a way around once signed in.
The landing page and dashboard used one header, the docs and blog each had their own, and the legal and post-checkout pages had none, so a signed-in user could not reach the docs or blog from the dashboard, or pricing from the docs.
components/layout/Header.jsxis now the only header, on every page, with its links inconfig/site.jsnav.links. Signed in, a Dashboard button replaces Sign in beside the account menu, which gains "AI features". Every dashboard page gets tabs (DashboardNav.jsx, rendered byapp/dashboard/layout.jsx), and the blog keeps its categories as a row under the header. Links markeddocs: truefollow the same rule asapp/docsitself (siteConfig.showDocs), so the header and footer no longer link to docs that return 404 in production.HeaderDocs.js,HeaderBlog.jsand the@headlessui/reactdependency are removed.›Security: signed-in users could give themselves unlimited credits.
consume_credit,refund_creditandclaim_pending_purchasesareSECURITY DEFINER, and Supabase lets theanonandauthenticatedroles call public-schema functions unless that is revoked. Nothing revoked it, and the anon key ships to every browser, so any user could callrefund_crediton their own id with a large amount and run AI generations on the owner's keys indefinitely, or drain another user's balance withconsume_credit. Migration007_lockdown_and_trial_budget.sqlrevokes all three from everyone butservice_role; the app already called them only from API routes with the service-role client, so nothing else changes. A new flow test, "a signed-in user cannot call the credit functions", fails on any database without 007. Run 007 on every existing project.›A daily ceiling on free-trial spend.
Signups cost only an email address, so the per-user trial could not stop someone farming accounts against your AI keys.
config/billing.jstrialDailyCreditCap(default 1000) caps credits spent per UTC day by all users without access combined; past it, unpaid users get a 429 and paying users are unaffected. The counter lives in the database (trial_spend,consume_trial_budget) so it holds across serverless instances. The docs' deploy checklist now also points at Supabase's own sign-in rate limits and CAPTCHA, which are the controls that apply to the sign-in request itself.›The audit now catches an AI route that charges nothing.
"Credit discipline" checked that routes which charge also refund, but never that a route calling a model charges at all, so a route whose
charge()an agent removed was simply not examined and the step passed. It now fails any API route that callsAIClientor a provider SDK without charging, unless the route is markedcredits: exemptwith a reason.›
doctorandsetup --checknow see migrations they used to miss.doctorchecked seven tables and reported "Healthy" on databases withoutemail_log(005) or the trial defaults from 006, which change nothing the REST API can observe.setup --check --json, the command agents are told to verify with, hard-codedmigrations: nulland returnedok: trueon a database that was behind. Migration 007 addswhirlwind_schema_version(); both commands now compare it with the newest template migration on disk and fail with the files to run.check-docsfails if a future template migration does not bump the version.
1.0.0
›The agent never handles your keys.
The documented setup flow used to have the agent collect each key in chat and pass it to
npm run setup --non-interactive, which meant every secret went through the agent's context and transcript.SETUP_WORKFLOW.md,CLAUDE.md,AGENTS.mdand the skill now have the agent tell you which key goes where and then verify withnpm run setup --check --jsonandnpm run doctor, while you runnpm run setupyourself in your own terminal and paste the keys at its prompts.--non-interactivestays for CI, where the values are already in the environment. No script behaviour changed; the script never printed a secret and still writes.env.localwith mode 600.›In-app docs now describe what ships.
app/docshad generic Supabase guides for email/password and OAuth sign-in, a schema with made-upprofilescolumns, a streaming example that called a route that does not exist,npx supabase db pushas the migration step, and markdown syntax that rendered as literal asterisks. Every topic was rewritten from the code: magic-link auth as wired, the email-first checkout flow and the five webhook events, the eight real tables and their functions,rateLimitMiddleware,npm run deploypreflight, and the actualconfig/theme.jskeys. The two unwired auth guides became one "Adding OAuth or Passwords" page that says so in its first line. The blog placeholder no longer mentions Whirlwind or a boilerplate.›Two more vibe presets:
studioandledger.Five presets total, each combining a hero, features and pricing layout with its own font pair, accent and background. The appearance tests derive from
vibePresets, so a new preset is covered without touching the spec.
0.9.0
Packaging + verification fixes. Every fix below was reproducible in the shipped 0.8.0 zip.
›CI could not pass, in two further ways, both introduced when the audit was wired into it.
First,
audit.mjsexits 2 for warnings-only and 1 for real failures — the conventiondeploy.mjsalready gates on — but the workflow treated any non-zero as failure. The buyer-path step creates.env.local, which the audit warns about by design, so the step warned at itself and died with zero actual failures. CI now fails only on 1 and surfaces warnings as a run notice. Second, under CI the Playwright suite serves the production build (dev optimises fonts differently and would void the webfont assertions), butnext startis production runtime, wherevalidateEnvthrows on missing credentials on purpose: the landing page 500'd, Playwright waited for a response under 400, and both test steps died on the 120s webServer timeout. The appearance step now gets correctly-shaped placeholder credentials — presence is allrequireEnvchecks, and those tests need no real services — while the flow step exits early with a notice when no secrets are set, because without real credentials there is no server for its tests to skip against. The whole pipeline was then replayed in a clean clone: install, lint, docs check, build + audit, smoke test, 16 appearance tests, flow-step skip.›The billing switch changed behaviour but not the interface.
Setting
model: "license"made the routes stop metering, yet every credit display was unaware of it: the dashboard still showed a balance under "Remaining this month", the image button still advertised a per-image price, and the chat toolbar still reported credits left. A perpetual licence showing a number counting down to zero reads as a product about to stop working. The dashboard card now reads "Licence / Perpetual licence · no usage limit" with Active/Inactive fromhas_access, and the two feature readouts hide themselves when nothing is metered. Verified in a real browser in both modes, because the dashboard is a client component and its server response is only a loading shell — curl cannot see the card at all.Two smaller things fell out of it. The card's balance fell back to a hardcoded
?? 100, stale since the trial became 20; it now shows an em dash when the figure is genuinely unknown. And the per-image price was written out in two route files and three prose mentions — it moved tobillingConfig.imageCredits, which both routes and the button now read, so the price cannot disagree with itself.›A first subscription granted no credits and recorded no subscription.
users.stripe_customer_idis written byclaim_pending_purchase, which runs when the buyer clicks their magic link. Stripe does not wait: on a first subscriptioncustomer.subscription.createdandinvoice.paidarrive within seconds of checkout and before any claim, so looking the user up by customer id found nobody — the credit reset was skipped and thesubscriptionstable stayed empty. From the second month onward it worked, because by then the id had been stored, which is exactly how it stayed invisible. Observed live: the handler loggedNo user for customer cus_… — no resetwhile the database showed that very id on the user, stored moments later by the claim.The lookup now falls back to the Stripe customer's email — the key the rest of the template is built on — and stores the id on the way through, so the detour happens once per customer. It also no longer swallows its own errors: the previous version used
.single(), which errors when no row matches, and destructured onlydata, so a failed query and an absent user were indistinguishable.Verified by reproducing the failure: unlink the customer id, replay the real
invoice.paidfrom a live subscription, and credits go 20 → 500 withLinked … to Stripe customer …in the log. The subscription branch then wrote its row too, where it had written none.›A subscription event missing its period fields answered Stripe with a 500.
new Date(undefined * 1000).toISOString()throws, and a throw here means Stripe retries an event that can never succeed. Newer Stripe API versions have begun movingcurrent_period_*onto subscription items, so this is a real upgrade hazard rather than a hypothetical. Those two conversions now go through a helper that returns null instead of throwing; the same payload that produced a 500 now answers 200.›The free allowance equalled the paid one, so subscribing bought nothing.
Signup granted 100 credits and a paid month reset to 100: the dashboard figure was identical before and after paying, which is how it was noticed.
006_trial_credits.sqlmakes the free grant a trial of 20 — twenty text generations or four images, enough to see all three bundled features — andconfig/billing.jsraises the paid allowance to 500. Existing users are untouched: changing balances people already hold would take credits from anyone mid-trial, and anyone who has paid gets the paid figure on their next invoice anyway.The two numbers necessarily live apart — one is a database default applied the instant a user row appears, the other is config you can change without a migration — so the audit now asserts that they disagree and that
config/billing.js'strialCreditsmatches what the signup trigger actually grants. Both failure modes negative-tested.Adding a sixth migration also made the numbering guard earn its keep: it immediately failed twelve stale references across six documents, which is precisely the drift it was written for after the same thing happened with 004 and 005.
›Nothing ever renewed the credit allowance.
The dashboard said "Remaining this month" and the webhook handled six Stripe events, none of them
invoice.paid. So credits were granted once by the signup trigger and never again: a subscriber paid every month, ran out in month two, and stayed out. Separately, a completed purchase grantedhas_accesswithout granting any credits at all, so anyone who spent their allowance and bought more paid and received nothing usable — the upsell path. Found by buying Pro on a spent-out account and still seeing zero.invoice.paidnow resets the balance tomonthlyCreditsand clears a stalerefunded_at, scoped to subscription invoices so the one-time invoices thatinvoice_creationnow produces don't hand out free allowances.›Credits or a one-time licence is now a switch, not an assumption.
config/billing.jssetsmodeltocredits(the default, metered, needs a recurring price) orlicense(users.has_accessis the gate, nothing meters). The five paid routes calledconsume_creditdirectly and each had its own copy of the refuse-and-refund logic; they now go throughlib/credits.js, so the model is decided in one place and the feature components read the same flag rather than locking a paying licensee out of a UI the server would serve.Three guards needed fixing to keep up, and all three had the same shape — a check anchored to something hand-maintained:
audit.mjs's undefined-identifier step is keyed on a curatedHELPERSlist that had never heard ofchargeorrefund, so four routes calling them unimported passed clean; deriving the list from everylib/export replaced that with five false positives (refundin the terms page's "a full refund.", code samples in the docs, a scaffold template,supabaseas a parameter name), so the list stays curated but a new step now fails if anylib/export is neither checked nor explicitly waived. Its usage regex also treated a full stop as property access. And the credit-discipline step matched onlyconsume_credit, so it declared the image routes uncharged the moment they were refactored. Every one of these was negative-tested.›A refund revoked access but not the credits.
charge.refundedsethas_access: falseand cancelled subscriptions, and stopped there. Nothing gates onhas_access: the five credit-charging AI routes call the credit function, and that function only checks the remaining balance. So a refunded customer kept their whole balance and could spend every credit of it against the operator's provider bill. Found by refunding a real test purchase and noticing the balance was still 100. The handler now zeroesusage.credits_remaining, leavingcredits_usedalone as the record of what was actually consumed.audit.mjsasserts the refund branch touches the balance — statically, because a webhook event cannot be delivered from a test — andflows.spec.jscovers the half that is testable: a zero balance refuses further charges rather than going negative. Both negative-tested.Two notes on what this does not change.
/dashboardis still gated on being signed in rather than onhas_access, so a refunded user still reaches it and sees aFREEbadge; that is a deliberate freemium-style degradation and the credit system is what protects revenue. And Stripe correctly keeps showing the charge as paid-then-refunded — refunding does not un-pay it.›The webfont test failed whenever it ran first against a cold server.
webServer.urlwaits for the dev server to answer, which in dev is not the same as the page being compiled: the first response for/can arrive before the font CSS is in it,document.fontsis empty, and the assertion fails with nothing actually wrong. Because it is the first test inappearance.spec.js, it passed in a full parallel run — another worker had already warmed/— and failed 3 times out of 3 when run on its own. Verified both ways: against a warm server it passed, cold it did not.tests/global-setup.jsnow compiles/before any test runs. A spurious failure in the suite that is meant to be evidence of correctness costs more than the test is worth.›The test suite and the admin dashboard were undocumented.
npm testappeared nowhere inREADME.mdorSETUP.md— one incidental mention inCLAUDE.md, inside the description ofnpm run deploy. For a template whose argument is correctness, the suite proving it was invisible.SETUP.mdhas a section now: what each suite covers, that 25 passing means credentials are configured and 18-with-7-skipped means they are not, which varsflows.spec.jsreads, that a live Stripe key skips the checkout tests deliberately, the dedicated port,test:ui, and the browser-install command./dashboard/adminhad zero mentions across every doc: a whole feature — signups, paying customers, conversion, credit consumption, recent activity — that no buyer could discover. Documented in both files, including that it answers 404 rather than 403 to anyone outsideADMIN_EMAILS, so a 404 while expecting access means an address mismatch rather than a broken page.›Fifteen env vars the code reads were absent from
.env.example.Among them
RESEND_API_KEY,EMAIL_FROM,CRON_SECRET,CREDITS_LOW_THRESHOLD,ADMIN_EMAILS,HEALTH_CHECK_TOKENand every model override — so three whole features (lifecycle emails,/dashboard/admin, the detailed health view) were invisible to a buyer reading the file the docs tell them to copy. All fifteen are now present and grouped by the feature they switch on, with their real defaults, and every value still ships empty.check-docs.mjsonly ever guarded one direction: every env var *named in the docs* had to exist in.env.example. It now also guards the inverse — everyprocess.env.Xthe app reads must be documented there, withNODE_ENVandNEXT_PHASEexcluded as runtime-provided. Negative-tested both ways. It immediately flagged an illustrativeprocess.env.Xinside a comment increate-portal/route.js, which is reworded rather than allow-listed; an allow-list entry would have hidden the next real one.›One-time purchases produced no invoice.
create-checkoutnever setinvoice_creation, and a Stripe Checkout payment creates a charge and a receipt but no Invoice object unless asked. The customer portal's invoice history was therefore empty for this template's headline flow — someone who paid once had nothing to file against an expense claim. Now enabled formode: "payment", guarded because Stripe rejects the parameter for subscriptions, which invoice themselves. Verified against the live API.›The lifecycle emails were undocumented.
SETUP.mdgained a section: that they are the app's own emails rather than Supabase's, that the copy lives inTEMPLATESinlib/email.js, that/api/cron/lifecycle-emailssends them on the daily schedule invercel.jsonand needsCRON_SECRETin production, whatCREDITS_LOW_THRESHOLDdoes, and thatemail_log's unique constraint makes them send once per user per template — so re-testing one means deleting that row.›
stripe listenfailing withapi_key_expiredhad no entry.The Stripe CLI authenticates with its own session key from
stripe login, which Stripe expires after about 90 days — not withSTRIPE_SECRET_KEYfrom.env.local, which comes from the dashboard and does not expire. So the normal experience of setting a project up and returning to it months later is a 401 that looks like the template's Stripe config is broken when nothing about it has changed.TROUBLESHOOTING.mdnow covers the error, namesstripe loginas the fix, states that the two keys are unrelated, points at~/.config/stripe/config.tomlwheretest_mode_key_expires_atrecords the lapse date, and notesnpm run doctor -- --deepas the way to confirm the env key separately. The same entry also records thatlistenprints a new signing secret every session, so restarting it means updatingSTRIPE_WEBHOOK_SECRETagain.›The required email-template step had an undocumented prerequisite.
Having made "point the auth emails at
/auth/confirm" a required setup step, the docs still didn't say that Supabase gates template editing behind custom SMTP on current projects — so a buyer reached a step marked required and found the dashboard wouldn't let them do it. SMTP setup now comes first inSETUP.mdandSETUP_WORKFLOW.md, with Resend's host, port, username and sender spelled out. Two things that follow from it are documented too:onboarding@resend.devonly delivers to the address that owns the Resend account, which is fine for signing in as yourself and breaks the post-purchase magic link to a customer (so a verified domain is needed before testing checkout); and Supabase SMTP is a different system fromRESEND_API_KEYin.env.local, which drives the app's own lifecycle emails throughlib/email.js. The same API key serves both, which is exactly why the two get confused.TROUBLESHOOTING.mdgained entries for "Supabase won't let me edit the email templates" and "the sign-in email arrives for me but not for a customer".›The documented
type=value broke sign-in for every new user.The auth email templates must point at
/auth/confirmwith atoken_hash, and the route's own doc comment,SETUP_WORKFLOW.md,SETUP.mdandTROUBLESHOOTING.mdall specifiedtype=magiclink. Measured against a live project:| address |
type=email|type=magiclink|type=signup| | --- | --- | --- | --- | | already exists | verifies | verifies | rejected | | first time | verifies | rejected | rejected |magiclinkverifies only for an address Supabase already knows, so a template using it works for whoever set the project up and fails for their first real customer — an intermittent-looking bug with a dashboard cause.signup, which an earlier pass of this changelog recommended for the "Confirm signup" template, is rejected outright.type=emailverifies in both states and is now the only value documented, in all five places plus the e2e open-redirect probe so no file in the repo models the broken one.Established by minting tokens with
auth.admin.generateLinkand driving the real route, not by reading the Supabase docs.›Sign-in could not work on a fresh Supabase project, and the setup docs never said so.
/auth/confirmverifies magic links server-side, which needs Supabase to puttoken_hashin the link. The default email templates use{{ .ConfirmationURL }}, which routes through Supabase's own endpoint and carries notoken_hash, so the route answers?signin_error=missing_token.SETUP.mdmentioned editing the Magic Link email only as copy polish — "the default copy is confusing" — filed under going live rather than setup, so anyone following the human path hit a dead sign-in on their first attempt.Worse,
SETUP_WORKFLOW.mddid mark the step required, but only for the Magic Link template.signInWithOtpruns withoutshouldCreateUser: false, so Supabase picks the template by whether the address already exists: a new email gets Confirm signup, a returning user gets Magic Link. Fixing one makes sign-in work for whoever set the project up and fail for their first real customer, which reads as an intermittent bug.Both template bodies are now a required step in
SETUP.mdnext to the redirect allowlist, both are inSETUP_WORKFLOW.md,TROUBLESHOOTING.mdhas an entry keyed on the literalsignin_error=missing_tokenstring a stuck buyer will search for, andnpm run deployreminds about them alongside the Redirect URLs. Also added: set Site URL to the local origin while testing, since{{ .SiteURL }}resolves from it.Neither guard could have caught this.
doctorcannot read Supabase email templates (that needs a Management API token, not the service-role key), andcheck-docs.mjsvalidates that documented paths and env vars exist, not that a documented procedure is sufficient.›
SETUP.mdundercounted the schema.It said "all three migrations are idempotent" and that the dashboard should show "four new tables". Five migrations ship and they create eight tables — the three
ai_*tables andemail_logwere missing from the checkpoint, so a buyer verifying their database against the docs would have confirmed a half-applied schema as correct.›The verification command handed to agents hung on a prompt.
CLAUDE.mdandskills/whirlwind/SKILL.mdboth tell an agent to confirm setup withnode scripts/setup.mjs --check --json, but--checkonly suppressed writing, not asking: it fell through to the interactive prompts and stopped atNEXT_PUBLIC_APP_NAME:instead of returning JSON. An agent following the documented path waited forever, and the flag that actually worked headless —--non-interactive --json— appeared only parenthetically, as the CI alternative.--checknow takes the same early return as--non-interactive: it validates what is already in.env.localand the environment, prompts for nothing, writes nothing, and exits 1 when the minimum is not met. The interactive path is untouched. Found by unpacking the release artifact and setting it up as a buyer would, which is the first time that had been done.›
npm run lintfailed on a pristine tree, so the CI lint step could never pass.Ten
react/no-unescaped-entitieserrors acrossapp/success/page.js,app/tos/page.jsandcomponents/marketing/CheckoutModal.jsx— raw apostrophes and double quotes in JSX text.next lintexits non-zero on errors, so thenpm run lint --if-presentstep failed for anyone who pushed the template to GitHub. The two page files were byte-identical to the shipped 0.9.0, so this was never a local quirk. Escaped to'and", which renders identically. Three warnings remain (a react-hooks dependency and two anonymous default exports); warnings do not fail the step and are left alone.›The flow tests could never run, on anyone's machine.
Playwright's runner is a separate process from the dev server it starts, and only Next reads
.env.local;flows.spec.jsreadsprocess.envdirectly to decide what it can test. So the seven tests covering signup credits, credit atomicity, RLS and checkout validation skipped themselves even on a fully configured machine, andnpm testreported a confident green while covering 18 of 25 tests and none of the money paths.playwright.config.jsnow loads.env.local(then.env) with real environment variables taking precedence, so CI secrets are never clobbered. Ten lines, no new dependency.›
npm run doctorchecked tables but never functions.CORE_TABLESwas probed; the rpc functions the app calls were not. A project whose migrations only partly applied can hold every table and still be missingrefund_credit— which five AI routes call to return a credit when a provider call fails. Without it the credit is silently kept and doctor stayed green, so the buyer learns about it from their users.CORE_FUNCTIONSjoins the contract and doctor probes both functions with arguments that match no row. Found a real instance the moment it shipped.›
npm run deploynever mentioned rate limiting.The in-memory limiter is fine on one local process and useless across serverless instances, each of which keeps its own
Map— so the per-user limits on the AI routes quietly stop holding at the exact point where every call costs money. Upstash is optional everywhere else and the preflight checked Stripe live-vs-test and the app URL but not this. Deploy now warns once the app URL is a real one and Upstash is unset.›CI never ran the checks most likely to break.
The
npm run auditstep ran without--build, so step 33 — the compiled-CSS and theme assertions — never executed; that is why the chunk-reading bug above survived. The separate build step and the audit step are now onenpm run audit -- --build, which builds exactly once and checks more. The smoke test also asserts that/dashboardredirects to the sign-in prompt, not merely that it 307s, since middleware now redirects unconfigured requests too.›
flows.spec.jsnow runs in CI when credentials are available.The suite already skips every test whose requirements are absent, so the new step is green-and-skipped by default and becomes real coverage of the money paths the moment
TEST_SUPABASE_*andTEST_STRIPE_*secrets are set. A step that silently goes green reads as "the money paths passed" when they never ran, so it emits a GitHub notice naming what was not covered — and a second one if the Stripe secret isn't ansk_test_key. Documented in the README's "CI included" bullet, including the warning to point those secrets at a throwaway project: the suite writes rows and creates checkout sessions.›
npm run type-checknever worked.tsc --noEmitwith notsconfig.jsonprints the compiler's help text and exits non-zero; the repo is JavaScript with ajsconfig.jsonfor path aliases and has no TypeScript sources. The dead script is removed rather than left to mislead.›The env contract was duplicated.
scripts/env-contract.mjsandlib/validateEnv.jseach carried their own copy of the required/optional lists, with a comment in both asking whoever edited one to remember the other. The contract now lives inlib/env-contract.mjsand both import it. It sits underlib/rather thanscripts/because the app imports it, and a buyer who deletesscripts/after setup must not break their build;scripts/env-contract.mjsre-exports it so the scripts are unchanged.›
check-docs.mjsderives the shipped-migration list instead of hardcoding it.Template migrations now carry a
-- whirlwind:templatemarker and the guard scans for it, so adding one cannot silently disarm the check built to catch adding one. The old list also doubled as a "did a migration go missing" check, which a derived list can't do by name, so that is now a contiguity assertion: the marked migrations must run 001..N with no gaps.lib/supabase/migrations/README.mdjoinedDOC_SOURCES— it carried its own stale numbering advice and had gone unchecked precisely because it wasn't on the list — and the content-generator example no longer tells an agent to create005_content_items.sql.›Dependencies are pinned.
Every range in
package.jsonis now exact, at the version the lockfile already resolved, sonpm installmonths from now installs what was actually tested rather than whatever minor has shipped since. Resolved versions are unchanged.›
CRON_SECRETis compared in constant time.The lifecycle-email endpoint used
===on the Authorization header. Low practical risk over HTTP, but the fix is three lines and this is the kind of thing a buyer's security review flags.›A fresh clone could not build, and its landing page answered 500.
Four API routes plus
lib/supabaseClient.jsconstructed Stripe and Supabase clients at module scope.next buildcollects every route, so building threw on a tree with no credentials — meaningnpm run audit -- --buildcould never pass pristine, exactly as 0.9.0 predicted. Worse,supabaseClient.jssits on the chainapp/page.js → Header → ButtonSignin, so the landing page itself 500'd during SSR before a buyer had any chance to run setup. All five are now lazily initialised, following the patternlib/auth.jsalready used. The browser client additionally falls back to a placeholder URL and key rather than throwing: every caller reaches it from auseEffector an event handler, and throwing there tore down the surrounding subtree — the header and hero stopped rendering and the theme toggle never finished mounting. It now logs one clear console error and leaves the page interactive. `npm run build,npm run audit -- --build` and all 18 e2e tests pass on a tree with no.env.localat all.›
middleware.js500'd every/dashboardrequest before setup.The file already fails closed around
getUser()— its comment says so — butcreateServerClientthrows one step earlier on missing credentials, outside the guard. Unconfigured,/dashboardreturned 500 instead of redirecting to sign-in. The credential check now happens before the client is built, and both paths share oneredirectToSigninhelper.›The
--buildaudit read only one CSS chunk.Next splits the compiled stylesheet, and
readdirSync(...).find()returned the small chunk, so step 33 reported.ww-displayand both DaisyUI themes as missing from CSS that contained all three. It reads every.cssfile now. This had never fired in anger because the build it depends on always failed first.›Checkout no longer fails open on an empty price allow-list.
create-checkoutskipped price validation entirely whenSTRIPE_ALLOWED_PRICE_IDSand bothNEXT_PUBLIC_STRIPE_PRICE_ID*vars were absent, which let a caller name any price object in the account. An empty allow-list is a server misconfiguration, so it now returns 500 and logs which var to set, matching how the route already handles a missingNEXT_PUBLIC_APP_URL.›Six docs told agents to create a migration number that already ships.
005_email_log.sql(the lifecycle-email dedup tablelib/email.jsdepends on) has been in the template since 0.8.0, but CLAUDE.md, AGENTS.md, SKILL.md, SETUP_WORKFLOW.md and.cursor/rules/whirlwind.mdcall still said "001–004 ship" and "start custom migrations at 005" — so the first custom feature an agent generated would collide with it.check-docs.mjsexists to catch exactly this (it was added when the same thing happened to 004), but itsTEMPLATE_MIGRATIONSlist was hardcoded and never gained 005, so the guard was blind to the recurrence. 005 added to the list, all six docs corrected to 001–005 / start at 006, and the guard extended to also catch the "new tables go in NNN+" phrasing that slipped past its other patterns.›
playwright.config.jsdefaulted to port 3000.With
reuseExistingServeron, the suite silently adopted whatever was already listening — usually a different project — and reported a wall of failures about an app you weren't testing. The default is now 3847 and the port is passed to Next explicitly.PORTstill overrides.›
@playwright/testis pinned exactly.The
^1.50.1caret would resolve to ≥1.59 the moment the lockfile was regenerated, and Playwright dropped WebKit support for macOS 13 in 1.58 and macOS 14 in 1.59 — an OS-support cliff a caret shouldn't be allowed to cross on its own. Relatedly, deploy's "browsers not installed" hint now saysnpx playwright install chromiumrather than the bare form, which also pulls the WebKit build that breaks on those macOS versions but that this suite never drives.›
scripts/verify.mjsis nowscripts/audit.mjs, and theverifynpm alias is gone.0.8.0 renamed the command to
npm run auditbut left the file namedverify.mjsand keptnpm run verifyas an alias, so the repo referenced "audit" everywhere while the file on disk said "verify" — a buyer whose audit failed went looking forscripts/audit.mjsand found nothing. One script, one name. (Supersedes the 0.8.0 note below that saysnpm run verifystill works — it no longer does.)›Three theme-toggle e2e tests could never pass.
appearance.spec.jsselects the toggle by[data-testid="theme-toggle"]and readsdata-theme-state, but neither attribute existed anywhere in the template, so all three failed with "element(s) not found" on a fresh clone. The mounted-guard behaviour they describe was implemented correctly incomponents/layout/Header.jsxall along — only the test hooks were missing. Added both attributes (pendingbefore mount, thenlight/dark).›
.env.examplewas missing from the distribution.check-docs.mjscrashed on it (sonpm run auditstep 15 failed on a fresh download), setup docs told buyers to copy a file that didn't exist, and the file wasn't even gitignored — it was dropped in packaging. Restored, with every value empty (a truthy placeholder here brokenpm run buildonce before — see 0.4.x) andNEXT_PUBLIC_APP_NAME/_DESCRIPTIONcommented out so they can't overrideconfig/site.js.check-docs.mjsalso gained an allow-list entry for theNEXT_PUBLIC_*_KEYanti-pattern examples the security docs name only to forbid.›
npm run deploynow runs the whole suite.It previously gated on doctor alone; audit and the e2e tests were easy to forget at exactly the moment they matter most. Deploy now runs
verify.mjs(hard gate, warnings don't block), attemptsnpx playwright test(downgrades to a warning with instructions when browsers aren't installed;--skip-teststo skip), thendoctor --deep.--forcestill overrides everything.›
doctor --deepnow pings Gemini too.It verified Claude and GPT model IDs but not Gemini's, though
lib/ai-client.jsships all three. Gemini is probed through the same OpenAI-compatible endpoint the client uses (GEMINI_BASE_URLrespected), so a retiredGEMINI_MODELnow fails the preflight instead of failing your users.›
npm run buildrequired real credentials just to compile.requireEnv()threw on any missing var wheneverNODE_ENV=production— butnext buildalso sets that, so a fresh tree couldn't compile before setup andnpm run audit -- --buildcould never pass pristine. It now throws only at production runtime (NEXT_PHASEdistinguishes compiling from serving), so a misconfigured deploy still fails fast atnext start. (The remainder this entry left for a future release — four API routes constructing Supabase and Stripe clients at module scope, so building still needed some value in those vars — is closed above: those clients are lazy now and empty-env builds work.)›
doctor --deepread HTTP 403 as "model is live."Any status under 500 that wasn't 401/404/429 counted as success, so a key without model permission — or a corporate proxy denial — reported a healthy model. 403 is now a warning that says it can't confirm the model.
›Stale audit output.
verify.mjs's header still described "checks 1–15" under an old filename, and every clean run emitted a permanent warning suggesting model verification be wired into doctor — which--deephas done for a while. The header now matches reality and the warning is a pass note pointing atnpm run doctor -- --deep.
0.8.0
Correctness pass. Every fix below was reproducible in a shipped build.
›Section headers were inconsistent.
FAQ hand-rolled its own eyebrow markup with hardcoded
[QUESTIONS]brackets — so the brackets appeared even under vibes whoselabelStyleis plain — and hardcoded "FREQUENTLY ASKED / QUESTIONS" in caps. Testimonials used a smaller heading ramp than every other section. Both now use<Eyebrow>and the shared `text-5xl md:text-6xl lg:text-7xlramp, with copy inconfig/site.js → faqSection`. Two new audit checks enforce one heading ramp across sections and require eyebrows to go through the shared component.›Style consistency sweep.
The mobile nav hardcoded `uppercase tracking-wider
while the desktop nav used.ww-label` — so under aurora or editorial the mobile menu shouted and the desktop one didn't. The Testimonials CTA bypassed the type classes entirely (no vibe radius, no optical centring) and hardcodedGET [YOUR PRODUCT]as its label. FAQ was the last section on a fixedpy-32; the account dropdown and checkout panel usedrounded-lginstead of the vibe's radius. All now followconfig/theme.js, andnpm run auditgained three checks so this is enforced rather than periodically rediscovered.›Buttons ignored the vibe's corner radius.
Only the hero set
--ww-radius-btninline, so sign-in, checkout, CTA, testimonial and dashboard buttons stayed square even under aurora (radiusBtn 9999px). Now applied in CSS toa/button.ww-displayand.ww-label:where(a, button), so no call site has to remember it.›The sign-in modal was translucent.
.ww-cardunder aurora is 55% opaque with a backdrop blur — right for a card on a page, wrong for a dialog, where content behind bleeds through over the dimmed backdrop. Any.ww-cardinside[role="dialog"]is now opaque regardless of vibe.›Effects wired in.
ShinyTextshipped unused; it now sweeps the hero eyebrow. Two new components:<Magnetic>(elements lean toward the cursor — usestranslaterather thantransformso it can't create a containing block for fixed descendants) on the primary CTA, and<CountUp>on pricing figures. Both no-op under reduced motion, and CountUp renders the real number if JS never runs.›Features and Pricing layout variants
, matching the hero treatment. Each vibe now selects a composition for all three sections: terminal → centred hero / spotlight grid / pricing cards; aurora → split hero / alternating rows / comparison table; editorial → left-ranged hero / numbered list / single-column pricing. Nine compositions from three vibes, verified in the rendered HTML.
›Section copy moved out of components.
Featureshardcoded "INFRASTRUCTURE DONE / YOU BUILD THE MAGIC" andPricinghardcoded "ONE PAYMENT / BUILD FOREVER" — Whirlwind's own pitch, and actively wrong for the subscription products most people build. Both now read fromconfig/site.js(featuresSection,pricing.eyebrow/heading/highlight), which also gainedpricing.currencySymbolfor non-USD products.›
ButtonCheckoutaccepts a wholeplanobject, deriving price id, mode and label from it, so the pricing layouts stay thin. The old individual props still work.
- ·
usePlans()anduseFeatures()filter holes and malformed entries — a stray trailing comma inconfig/site.jsproduces a sparse array, which previously crashed the production build rather than degrading. ›Hero layout variants.
Vibes previously changed only decoration, so every generated product shared one composition and read as a recolour. There are now three hero layouts and the vibe picks one:
centered(terminal) symmetrical with an animated backdrop,split(aurora) copy left and a product panel right,editorial(editorial) left-ranged with a hairline rule and no motion. Shared pieces live inhero/parts.jsxso copy edits land in every variant; adding a fourth is one file plus one line inhero/index.jsx.config/site.jsgainedhero.eyebrowand configurable CTA labels and hrefs.›Vibes now change typography, not just decoration.
Every vibe used Bebas + IBM Plex Mono, so generated products looked like recolours of each other. Each preset now carries a font pairing — terminal: Bebas/IBM Plex Mono, aurora: Outfit/Inter, editorial: Fraunces/Inter — plus its own section spacing and shadow treatment. All five faces load via next/font; the vibe resolves which two are used. The all-caps optical nudge on buttons is now scoped to the Bebas vibe, since Outfit and Fraunces have real descenders.
›Hero and Footer hardcoded
/whirlwindlogo.png— a file that only ever existed on Whirlwind's own site. In a generated product it 404s, and the alt text overflows the fixed 96px box (the truncated "LedgerLigh" in a demo). Both now use
<Logo>, which falls back to a themed mark.›Testimonials advertised Whirlwind inside the customer's product
— "SHIPPED WITH WHIRLWIND" was hardcoded, with
@user1/@user2placeholder quotes. Copy and quotes now come fromconfig/site.js, and the section renders nothing whenquotesis empty.›The blog was broken out of the box.
The example
authorsentry was all empty strings while articles setauthor: "author name"as a string — butAvatar.jsreadsarticle.author.slug. Every article page also readarticle.image.urlRelativeunguarded while the example article ships with its image commented out, so/blog/<slug>returned a 500. Author is now a real object derived fromconfig/site.js, and image reads are optional.›Display-face buttons looked bottom-heavy.
Bebas Neue has tall ascenders and almost no descender, so its line box sits low relative to the visible glyphs and symmetrical padding reads as off-centre.
.ww-displayon links and buttons now trims the line box — fixed in CSS rather than across the 21 call sites.›Modals rendered inside their containing block.
The sign-in modal lives in the header, and the header uses
backdrop-blur-xl— which makes it the containing block forposition: fixed. The modal therefore centred itself inside a 72px header bar rather than the viewport, with the backdrop clipped to the same strip. Both modals now render intodocument.bodyviacreatePortal, andnpm run auditfails any dialog that is not portalled. This is a CSS spec behaviour rather than a Tailwind quirk: any ancestor withtransform,filterorbackdrop-filterdoes the same thing.›
safeNextPathwas used inButtonSigninwithout being imported— a ReferenceError thrown the moment anyone clicked sign-in. The build compiled fine, because Next.js compiles each module without resolving free identifiers, so nothing caught it until a user interacted with the page.
npm run auditnow checks the inverse of import resolution: every helper that is used must be imported or declared in that file.
Added
›Tests.
Playwright suites at
tests/e2e/.appearance.spec.jsneeds no credentials and runs in CI — webfonts, favicon, theme toggle, vibe casing, invisible sections, 404s, open redirects, hydration.flows.spec.jscovers the money paths (signup provisions credits,consume_creditis atomic under concurrency, RLS denies, checkout validates) and skips itself cleanly when Supabase or test Stripe keys are absent, so a fresh clone is green rather than misleadingly red.›
npm run audit— the 58-check static audit, renamed from
verifyto be findable by buyers.npm run verifystill works.›
lib/analytics.js— six named Plausible events so the funnel is measurable, defined in one place so they can't drift into a dozen spellings. No-ops when Plausible isn't configured.
›
/dashboard/admin— read-only stats (signups, paying, conversion, credit usage, recent signups), gated on
ADMIN_EMAILS. Returns 404 rather than 403 to anyone not listed, since a 403 confirms the page exists. No destructive actions by design.›LICENSE now addresses client and agency work
explicitly — each client project counts as one Derivative Work, Advanced covers unlimited, the client receives the work rather than a Template license, and delivering the unmodified Template remains redistribution. The word "client" previously appeared zero times, which is the first thing a freelancer asks.
- ·CI runs the appearance suite and the audit on every push.
›Lifecycle email
(
lib/email.js). Deliberately scoped to what nothing else sends: Stripe already sends receipts, and Supabase already sends magic link / confirmation / password reset. This adds the day-7 onboarding nudge and the credits-low / credits-empty warnings. - Idempotency is enforced by a unique constraint onemail_log (user_id, template), not application logic — on serverless, several instances can reach the send path at once, so the log row is claimed first and the email only sent if the insert succeeded. A failed send releases the claim so a retry can work. - Credit warnings are fire-and-forget: an email provider outage can never delay or fail a generation. - WithoutRESEND_API_KEYevery function no-ops and returns a reason. -/api/cron/lifecycle-emailsis protected byCRON_SECRETand refuses to run in production when unset — an open endpoint that sends mail is a spam vector aimed at your own domain reputation.vercel.jsonschedules it daily.
Security
›Stripe checkout URLs came from the
Originheader.The caller controls that header, so anyone could create a checkout whose
success_urlpointed at their own site — a convincing "payment complete" page on a domain you do not control. Now built fromNEXT_PUBLIC_APP_URL, with the header used only in development.›Billing portal
return_urlcame from the request body, unchecked, so Stripe would return the customer wherever it pointed. Now re-anchored to your own origin. The same line had an operator-precedence bug:
process.env.X + "/dashboard" || "http://localhost:3000/dashboard"—+binds tighter than||, so withXunset it produced the truthy string"undefined/dashboard"and the fallback could never run.›Open redirect in the magic-link flow.
/auth/confirmpassed the attacker-controllablenextquery parameter straight intonew URL(next, req.url), and/auth/callbackpassed it towindow.location.assign(). Both send the user off-site forhttps://evil.com,//evil.comor/\evil.com— a phishing link that traverses your real sign-in flow before landing on the attacker's page, which is what makes it convincing. A sharedsafeNextPath()inlib/utils.jsnow rejects anything that is not a same-origin path, andButtonSigninsanitises before embeddingnextinemailRedirectTo.- ·
/auth/confirmalso validates the OTPtypeagainst a known set instead of forwarding it, and no longer reflects raw provider error messages into the address bar. ›
/api/healthleaked reconnaissance.The unauthenticated endpoint reported
NODE_ENV, process uptime, and which of Supabase/Stripe/AI were configured. It now returns{status, timestamp}publicly; the detail needsHEALTH_CHECK_TOKENin production.›Middleware 500-ed on a Supabase blip.
getUser()was uncaught, so a transient network failure returned a 500 on every/dashboardrequest instead of redirecting. It now fails closed.›
lib/api.jsremoved— an axios wrapper nothing imported, missing
"use client"despite callingtoast, so importing it from a server component would have failed confusingly. Every component usesfetch.axioswent with it (19 dependencies now, from 26).- ·
CheckoutModal's focus timer is cleared on unmount. ›Blog routes crashed on unknown URLs.
/blog/[articleId],/blog/category/[categoryId]and/blog/author/[authorId]used.find()and then read a property off the result — so any unrecognised URL threw a TypeError and returned a 500 instead of a 404. All three now callnotFound()./docs/[slug]rendered an inline "Page Not Found" with an HTTP 200 (a soft 404 search engines index); it now returns a real 404.›The sign-in modal had no accessibility.
No
role="dialog", noaria-modal, no Escape handler, no focus management — screen readers never announced it and keyboard users could not dismiss it.CheckoutModalhad everything except the Escape handler. Both are complete now.›Two variables for one site URL.
next-sitemapandconfig/site.jsreadNEXT_PUBLIC_APP_URLwhilelib/seo.jsreadNEXT_PUBLIC_SITE_URL.seo.jsnow accepts either. The sitemap also excludes/docsunless the docs flag is set, since those URLs 404 in production.›README
claimed GPT-4, a DALL-E image studio, and an in-memory-only rate limiter. All three were out of date.
›
npm run doctor -- --deepnow verifies AI model IDs.Doctor checked that AI keys were set but never that the configured models still exist. A retired model returns 404, which reads like an auth failure — this is how both
claude-3-5-sonnet-latestanddall-e-3reached production. It now sends a 1-token request per provider and names the env var to change.›Feature scaffold produced off-theme components.
skills/whirlwind/templates/component.jsxusedtext-blackon the primary button and hardcodedred-500/red-600for errors, so every feature a buyer generated was unreadable in some themes and ignoredprimaryColor. Nowtext-primary-contentandtext-error.›Session lookups swallowed network failures.
Three
getSession().then()calls — including the app-wide auth provider — had no rejection handler, so a network blip stranded the loading state permanently. All now recover.›
NEXT_PUBLIC_SUPPORT_EMAILhad the same override problem as
NEXT_PUBLIC_APP_NAME; commented out.AI_DEFAULT_PROVIDER,AI_TIMEOUT_MSandGEMINI_BASE_URLare now documented in.env.example.›Product name never reached page titles.
.env.exampleshippedNEXT_PUBLIC_APP_NAME=Your AI Product, andconfig/site.jsreadsprocess.env.NEXT_PUBLIC_APP_NAME || "<your name>"— so the env placeholder won over the name setup wrote. Every<title>, the blog heading, the legal pages and the OG tags said "Your AI Product" while the landing page was correct. Both placeholders are now commented out.›Two parallel product configs.
14 files (blog, docs, ToS, privacy, checkout, and
lib/seo.js, which drives every page title) imported@/config— a rootconfig.jswith its ownappName/appDescription— while the landing page imported@/config/site. Setup only editsconfig/site.js.config.jsis now a thin derivation of it.›Phantom config reads.
config.mailgun.supportEmailandconfig.twitterHandlewere read but never defined; optional chaining hid it, so the support-email fallback silently never fired and Twitter cards carried no author. Both now resolve.›Six unused dependencies removed
—
react-syntax-highlighter(8.9 MB),react-tooltip,zustand,swr,react-copy-to-clipboard,form-data. Deadconfig.stripe.plansandconfig.colorsblocks removed with them, plus commented-out imports pointing at assets that never shipped.›Every
dark:variant was inert.tailwind.config.jssetdarkMode: "class", but next-themes writesdata-themeand never adds a.darkclass — so all 19dark:utilities across 5 files did nothing.darkModenow keys off the configured dark theme, and the light/dark pairs they guarded collapse to single DaisyUI semantic tokens.›The sign-in modal was hardcoded dark and off-brand.
It used
bg-[#0a0a1a], indigo borders, cyan accents and white text regardless ofprimaryColorordefaultMode— unreadable in a light-mode app and the wrong colour in every app. Rebuilt on theme tokens. Its backdrop had also been commented out and the panel positioned 40vh down the viewport; both fixed, and the literal "SIGN IN" text is now sentence case so.ww-displaycan apply the vibe's casing.- ·Modal backdrops use the
neutraltoken instead ofbg-black; error and success text usetext-error/text-successinstead of hardcoded reds and greens. ›Image optimizer was an open proxy.
next.config.jsallowedremotePatterns: [{ hostname: "**" }], so anyone could request/_next/image?url=<any-host>and make your deployment fetch and transform arbitrary remote images — consuming bandwidth and image-transformation quota. Now restricted to your Supabase storage host, derived fromNEXT_PUBLIC_SUPABASE_URL.›Accessible names
added to the message, image-prompt and email inputs. They relied on
placeholderalone, which disappears on focus and does not reliably provide an accessible name.›Credit discipline.
custom/chat,custom/create-imageandcustom/generate-contentconsumed credits and never refunded them when the provider call failed — onlyai/generateTextdid. All five paid routes now refund; the streaming route refunds only when no tokens were delivered.›
ai/generateImagecharged nothingwhile
custom/create-imagecharged 5 for the same work, so any signed-up account could generate images on the operator's OpenAI bill at 5/minute indefinitely. It now consumes and refunds the same 5 credits.›
dall-e-3was removed from the OpenAI API on 2026-05-12.Both image routes now use
gpt-image-1(override withOPENAI_IMAGE_MODEL), with the corrected size and quality enums and handling for base64 responses — the new models returnb64_jsonwhere DALL-E returned a URL, so a naive model swap would have returnedundefinedimage URLs.›Error boundaries added
:
app/error.jsx,app/not-found.jsxandapp/global-error.jsx. Without them any runtime error rendered Next.js's unstyled default screen.›Version drift
: SKILL.md frontmatter said 0.4.0 and skill.json said 0.7.0 while package.json said 0.8.0. All four sources now agree, and
scripts/verify.mjsasserts it.›OpenClaw compatibility
: skill-relative paths use
{baseDir}, gating metadata added as single-line JSON frontmatter, and SKILL.md now states the working directory explicitly rather than assuming the agent is in the project root.›Agent-doc accuracy.
Eight places across
CLAUDE.md,AGENTS.md,SETUP.md,.cursor/rules/whirlwind.mdc,SKILL.md,SETUP_WORKFLOW.mdand the worked example told agents that migrations001–003ship and that custom ones start at004— which is now004_ai_features.sql. Following those instructions overwrote the migration backing the three bundled AI features.SETUP.mdalso omitted004from the manual run list, so anyone following the human path got a database those features can't write to. All corrected to001–004shipped / start at005.›
scripts/check-docs.mjsextendedto catch that whole class: quoted migration ranges must match the shipped set, "start at NNN" must point at the next free number, placeholder filenames must not collide, every shipped migration must be present, the three bundled features and
/dashboard/featuresmust exist, and everythemeConfig.*field the code reads must be declared. The assertions anchor to the template's known migration filenames rather than the highest number on disk, so a product that adds005_invoices.sqldoesn't fail its own CI.›
SETUP_WORKFLOW.mdStep 7.5now requires
daisyThemeLightalongsidedaisyTheme(setting one alone leaves the other mode mismatched), notes thatlabelStyle: nullfollows the vibe, documentshero.highlight, and points at thepricing/faq/cta/footersections ofconfig/site.jsthat the old sketch omitted.›Dark/light toggle actually works.
<html data-theme>was hardcoded tothemeConfig.daisyThemewhile next-themes toggled aclassthat DaisyUI v4 doesn't read — clicking changed nothing. next-themes now ownsdata-themeand swaps betweendaisyTheme/ newdaisyThemeLight, with a matchingwhirlwind-lightcustom theme.Header.jsxgained a mounted guard so the first click isn't swallowed by an unresolved theme.›
enableSystemno longer overrides your config.It's enabled only when
defaultMode: "system", so"dark"means dark.›Bebas Neue is actually loaded.
tailwind.config.jsnamed it butapp/layout.jsnever callednext/fontfor it, so headings fell back to the system sans-serif for anyone without the font installed locally. Both families now load and Tailwind references the generated CSS variables.›
config/theme.jsis now genuinely the single source of truth for vibes.globals.csspreviously hardcoded the same radius/casing/tracking numbers in[data-vibe]blocks, so editing a preset changed nothing.layout.jswritesthemeConfig.cssVarsinline on<html>; the CSS consumes them. Adding a fourth vibe is a config edit. Presets also ownlabelCase,labelTrackingandlabelStyle—<Eyebrow>follows the vibe unless you setlabelStyleexplicitly.›New
.ww-labelclass + per-vibe label casing, for nav links and meta text that previously couldn't follow the vibe. All 87 hardcoded
font-bebas uppercase tracking-*sites now route through.ww-display/.ww-label.›
<Reveal>can no longer hide content.It started at
opacity: 0and relied on auseEffectto reveal; if hydration failed, whole sections stayed invisible at full height. It now renders visible and only arms the hidden state once an observer is attached, with a 2s failsafe.›Favicon works.
lib/seo.jssetmetadata.iconsexplicitly, overriding the Next.js file convention and pointing at five paths that don't ship. Removed;app/icon.svgnow ships and is served automatically.›AI model IDs current
(
claude-sonnet-5,gpt-5-mini,gemini-3.6-flash); the old defaults were retired and returned 404s that look like auth failures.AIClientnow returns{ text, usage, model }with token counts for credit accounting, wraps provider errors with actionable messages, and exposesAIClient.verify()fornpm run doctor. All call sites and the feature scaffold inskills/whirlwind/templates/updated.›
/docsis dev-onlyunless
NEXT_PUBLIC_SHOW_TEMPLATE_DOCS="true"— the flag the docs already advertised but nothing implemented. Customers were shipping Whirlwind's documentation on their own sites.›
components/ui/Logo.jsxadded;
Headeruses it.- ·Docs code samples referenced
@/lib/supabase/clientand@/lib/ai/client; the real paths are@/lib/supabaseClientand@/lib/ai-client.
0.7.0
Whirlwind ships with product, not just plumbing.
›3 working AI features
(migration
004_ai_features.sql+ routes + components), showcased at/dashboard/features: AIChat (streaming multi-turn chat, persisted conversations), ContentWriter (blog/LinkedIn/email presets, saved history), ImageStudio (DALL-E 3 with gallery, 5-credit cost). All follow the canonical authenticate → rate-limit → consume-credit → generate → persist order; the chat route demonstrates doing that with a ReadableStream.›The 4-question theme step is real.
SETUP_WORKFLOW.mdStep 7.5 has the agent ask brand color, vibe, mode, and label style, and writeconfig/theme.js. NewlabelStyleoption +<Eyebrow>component control bracketed vs. plain section labels.›CI + docs-drift guard.
.github/workflows/ci.ymlbuilds via the exact buyer path, boots the server, and smoke-tests landing/docs/health/auth-redirect.scripts/check-docs.mjsfails the build when docs mention paths or env vars that don't exist (it already caught three stale references in 0.5.x docs, now fixed).- ·Dashboard links to the feature showcase.
0.6.0
UI kit, vibe presets, and styling/docs fixes.
›New
components/ui/kit(zero dependencies):
GradientText,ShinyText,SpotlightCard,Reveal,Marquee,AnimatedBackground— all themed from DaisyUI tokens and reduced-motion safe. Documented at/docs/ui-components.›Vibe presets.
config/theme.jsgainsvibe: "terminal" | "aurora" | "editorial"controlling radius, display-type treatment, card style, and the hero background effect.config/theme.jsis now CommonJS and is the single source of truth —tailwind.config.jsrequires it (previously the two files could drift).›Landing page restyle.
Hero: staggered word reveal, animated gradient highlight, perspective grid with scan line, glowing CTA. Features: spotlight cards with scroll-stagger. Testimonials: infinite marquee. Pricing: rotating glow border on the featured plan.
›Styling fixes.
.text-gradientwas silently dropped by Tailwind (referenced undefinedsecondarycolor);hsl(var(--p))usages fixed tooklch(...)for DaisyUI v4 (scrollbar, ::selection, testimonial accent);*-primary-focusclasses now exist (were no-ops);text-blackon primary buttons replaced withtext-primary-contentfor light-theme safety; hardcoded#22d3eeremoved from Hero/ClientLayout.›Env fix.
.env.exampleshipped a truthyMAILCHIMP_API_KEYplaceholder that madenpm run buildfail aftercp .env.example .env.local. Now empty..envremoved from the distribution.›Docs corrections
(in-app
/docs): Node 20 (was 18),.env.local(was.env),npm run setup/doctornow the recommended path, real paths (lib/ai-client.js,app/api/webhook/stripe/,SupabaseAuthProvider,lib/supabase/migrations/), realAIClientclass API in AI + streaming examples (docs previously showed non-existentgenerateText/streamTextimports), removed Resend section (not used by the code), Docker section notes no Dockerfile ships, theming docs rewritten forconfig/theme.js.
0.5.0
Deterministic, agent-agnostic setup.
›New
scripts/setup engine.npm run setup(collect keys → write.env.local→ run migrations),npm run doctor(read-only health check: env, migrations, test-vs-live keys),npm run deploy(production preflight + go-live guidance). Setup actions are owned by reviewed scripts, not improvised by an AI agent.›Works with or without an agent.
Front doors for each:
CLAUDE.md(Claude Code),AGENTS.md(Cursor/Codex),.cursor/rules/whirlwind.mdc, and the existing OpenClaw skill — all thin pointers to the same scripts. Or run the commands yourself in a terminal.›SKILL.md / SETUP_WORKFLOW.md
reconciled so setup actions route through the scripts (no more freehand
.envwrites or pasted migrations).›Docs reframe.
README.mdandSETUP.mdlead with the scripts and present OpenClaw as one option, not a requirement.›Fixes.
SETUP.mdnow allow-lists/auth/confirmfor the magic-link flow (was only/auth/callback);.whirlwind/gitignored;NEXT_PUBLIC_SITE_URLdocumented in.env.example.
0.4.0
- ·Prior release (auth on
/auth/confirm, credit RPCs, Upstash rate limiter, consolidated skill).