Feedback module
The smallest module in the template — the demo case for a minimal, cleanly-removable module. A thumbs-up/thumbs-down + optional comment control that POSTs to a configurable HTTP endpoint (a projektor feedback source, or anything else that accepts the payload below). Env-gated: with either env var unset it renders nothing, so a fresh stamp of the template builds and tests clean with no live endpoint configured.
Touch-points
Section titled “Touch-points”apps/web/src/modules/billing/GatedSamplePage.tsx— one<FeedbackWidget />mount in the premium-content state.apps/web/src/modules/blog/BlogPostPage.tsx— one<FeedbackWidget />mount in the post footer (applies to every post, since it’s the shared template).apps/web/.env.example— documentsVITE_FEEDBACK_ENDPOINTandVITE_FEEDBACK_TOKEN.apps/web/src/vite-env.d.ts— types the twoVITE_*vars onImportMetaEnv.apps/web/src/modules/feedback/— all module code (client, component).
Removal steps
Section titled “Removal steps”- In
apps/web/src/modules/billing/GatedSamplePage.tsx, remove theFeedbackWidgetimport and its<FeedbackWidget />mount. - In
apps/web/src/modules/blog/BlogPostPage.tsx, remove theFeedbackWidgetimport and the<footer>block that mounts it. - Delete
apps/web/src/modules/feedback/. - Remove the
VITE_FEEDBACK_ENDPOINT/VITE_FEEDBACK_TOKENentries fromapps/web/.env.example, and theImportMetaEnvadditions fromapps/web/src/vite-env.d.ts. - Delete this page (
apps/docs/src/content/docs/modules/feedback.md) and the links to it from the modules index and the new-project checklist — a broken internal link fails the docs build. - Run
pnpm checkto confirm the rest of the suite is still green with the module gone.
Environment variables
Section titled “Environment variables”Both are read via import.meta.env (Vite’s standard client-env convention), so they
must be prefixed VITE_ and are set in apps/web/.env (gitignored; copy from
.env.example) or the deploy environment.
VITE_FEEDBACK_ENDPOINT— full URL the widget POSTs to. Unset (or empty) disables the widget entirely — it rendersnull.VITE_FEEDBACK_TOKEN— sent asAuthorization: Bearer <token>when set. This is public by design, not a secret. Vite bakes everyVITE_*variable into the client bundle at build time, so anyone can read it from the shipped JS. Treat it as a lightweight anti-spam token (e.g. to scope a projektor feedback source), never as an access-control credential — don’t reuse a real secret here.
What the receiving endpoint has to do
Section titled “What the receiving endpoint has to do”The widget is an unauthenticated public write surface: VITE_FEEDBACK_TOKEN is in the
shipped bundle, so it identifies the source, it does not authenticate the sender. The
client bounds the comment at 2000 characters and treats any non-2xx response as a
failure, but everything that actually matters is the endpoint’s job — rate-limit by IP,
cap the request body, and treat comment/page as untrusted text (escape on render;
never interpolate into HTML, SQL, or a shell). The widget itself never renders anything
the server sends back, only a fixed success or error string, so a hostile response
cannot inject content into the page.
Payload shape
Section titled “Payload shape”{ rating: 'up' | 'down' | null; // null in comment-only mode, or if neither thumb was picked comment?: string; // omitted when the comment box was left empty; capped at 2000 chars page: string; // the pathname the widget was mounted on}import { FeedbackWidget } from '../feedback/FeedbackWidget';
<FeedbackWidget /> // thumbs + comment (default)<FeedbackWidget mode="comment-only" /> // comment box only, no thumbs