Demo lives in a separate showcase project, not in the package
Context
The package shipped a "demo trio": the chatbot:demo command, DemoController, the GET /chatbot/demo route, a demo.blade.php view, a chatbot.demo config block (CHATBOT_DEMO env), and a branch in the service provider that bound LLMClient to a FakeClient with canned replies when chatbot.demo.enabled was true. Its job was to let someone confirm the widget renders end-to-end without spending tokens — it was the post-install verification step in the installation guide.
That convenience carried ongoing cost. The demo bound a fake driver and exposed a public route, so every surface that touched it (installation, security, configuration docs) had to repeat a "NEVER enable in production" warning, and the provider's client-resolution path carried a demo branch that had nothing to do with real operation. Meanwhile two things that look adjacent are not demo and earn their place: FakeClient / Chatbot::fake() is the public, documented testing API for consumers, and the Playwright fixture (chatbot.playwright_fixture) is the package's own browser e2e infrastructure.
A separate showcase project — one that exercises every package feature — serves people evaluating the package before they install it. That is a different audience from someone verifying the install inside their own app, and conflating the two is what kept demo code in the library.
Decision
The demo trio is removed from the package. Demos and feature showcases live in a separate, standalone project.
- Removed:
chatbot:demo,DemoController,routes/chatbot-demo.php,resources/views/demo.blade.php, thechatbot.democonfig block /CHATBOT_DEMOenv, the provider's demoFakeClientbinding branch, and theDemoCommandTest/DemoRouteTesttests. - Kept:
FakeClientandChatbot::fake()(public testing API), and the entire Playwright fixture. Neither is demo code. - Post-install verification moves to a short
Chatbot::fake()recipe in the docs — binding the fake in a throwaway route or test — which closes the "confirm it works without spending tokens" gap using an API that already exists and is already documented. - Clean break. No deprecation cycle. The removal ships as a breaking release with an
upgrading.mdentry; a staleCHATBOT_DEMO=truesimply becomes inert and/chatbot/demoreturns 404. A deprecation period was not worth it for a flag that was never meant to run in production.
Considered alternatives
- Keep the demo in the package. Rejected. It is permanent surface and a recurring "never in production" caveat for a feature that only matters in dev, and it duplicates what a dedicated showcase project does better.
- Deprecate over one release (warn + no-op, then remove). Rejected. It keeps demo code alive longer, which is the opposite of the goal, for negligible safety on a dev-only flag.
- Clean break but fail loud (throw if
chatbot.demo.enabledis still truthy). Rejected as unnecessary ceremony — the upgrade note covers the dev who had it on, and an inert key harms nothing.
Consequences
- This is a breaking change:
/chatbot/demo404s, thechatbot.democonfig key is inert, and a dev relying onCHATBOT_DEMO=truefor a fake driver now falls through to the real client (possibly with an empty API key). Theupgrading.mdentry documents the migration toChatbot::fake(). - ADR 0009 referenced
GET /chatbot/demoas aweb-wrapped route and, with the fixture, as a page that "models the real integration." After this change the Playwright fixture stands alone as that integration model; 0009's body is left intact as a historical record and carries a pointer to this ADR. - Until the separate showcase project exists, there is no in-package "see everything at once" experience — only the per-feature
Chatbot::fake()recipe. That gap is accepted deliberately.