Vibecode Feedspace
track this build4 phases, 12 steps, beginner friendly0%The form, the queue, the wall and the embed widget are one sitting, as the two tools here show. What is not: 150+ sources with 120+ on auto-sync, on an extraction engine Feedspace owns in-house, and widgets that fill themselves from curation rules. Only collecting written reviews through your own form? Build it. Want the ones you have elsewhere kept current? Don't.
You are building a lean indie version of Feedspace. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Feedspace · indie build A tool for collecting written, audio and video testimonials and embedding them, where the entire design constraint is that the record must stay honest: originals preserved, incentives and relationships recorded, edits visible, and suppression measured rather than hidden. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | the work is collection, provenance and embedding | | Media | Direct-to-storage uploads with a transcoding worker | video uploads through your server will fall over on the first long one | | Provenance | Original text and media preserved, immutable, alongside any edit | the original is the evidence and must survive every subsequent change | | Embedding | A small script rendering from a signed public endpoint | widgets are the delivery mechanism and must not slow down the host page | | Disclosure | Incentive and relationship recorded at collection and rendered with the testimonial | a disclosure added later is a disclosure nobody made | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server, the widget build and the transcoding worker. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Immutable originals, append-only publication history and consent records want real constraints. - Get it: Docker or your package manager. - Verify: psql connects and an insert-only table can be enforced - [ ] **Object storage with direct upload** · pennies per month at small volume - Why: Video testimonials are large and uploading them through your application server is how the first long submission takes the site down. - Get it: Any provider supporting presigned direct uploads. Configure a lifecycle policy before storing anything. - Verify: A large file uploads directly from a browser without passing through your server - [ ] **Which rules apply to you, written down** · free to read; an hour of advice if this is commercial - Why: This build encodes the United States position, which took effect on 21 October 2024 with civil penalties currently $51,744 per violation. Your jurisdiction may differ or be stricter, and the tool's behaviour should match the stricter one. - Get it: Identify where your customers are, read the applicable consumer protection guidance, and write down what you concluded. - Verify: A written note names the jurisdictions and what each requires of testimonials - [ ] **Ten real testimonials including an awkward one** · free - Why: The interesting cases are the ones the tool is meant to handle: a review from an employee, one where the person got a discount, and one that is lukewarm rather than glowing. - Get it: Collect real ones with permission, and deliberately include an insider and an incentivised submission to test the disclosure path. - Verify: Ten exist, including at least one insider and one incentivised ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Legal advice about your jurisdiction. - Automated video editing and captioning. - Review platform integrations. - Sentiment ranking of testimonials. - 150+ review sources with 120+ on auto-sync, on an extraction engine Feedspace owns and runs in-house - the upkeep · sources change and keeping the fetching working is someone else's job - rule-based curation · matching new reviews join a widget or wall page by themselves, so the embed never goes stale - in-browser video and audio recording that works on mobile without an app, and the media pipeline behind it - aggregate rating badges that stay accurate because the auto-syncs keep feeding them If one of those is essential to you, that is the reason to keep paying for Feedspace, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Feedspace The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a testimonial collection and display tool that cannot be used to mislead to replace Feedspace. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Testimonial software is a compliance product wearing a marketing product's clothes. In the United States the Federal Trade Commission's rule on consumer reviews and testimonials took effect on 21 October 2024, with civil penalties currently set at $51,744 per violation. It prohibits buying or selling fake reviews, incentivising reviews that express a particular sentiment, publishing reviews by company insiders without disclosing the relationship, and suppressing negative reviews while presenting the remainder as representative. Every one of those is a feature a testimonial tool could easily provide by accident. A publish toggle is review suppression if it is used to hide the negative ones. An edit field is fabrication if it changes what someone said. A reward for leaving a review is an incentive that has to be disclosed. So the build records, for every testimonial: whether anything was given in exchange, what relationship the person has to the company, and the unedited original. It shows suppression as a number the operator has to look at. It is designed so that using it honestly is easier than using it deceptively, because the alternative is a tool whose main function is to produce evidence against its owner. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · the work is collection, provenance and embedding - Media: Direct-to-storage uploads with a transcoding worker · video uploads through your server will fall over on the first long one - Provenance: Original text and media preserved, immutable, alongside any edit · the original is the evidence and must survive every subsequent change - Embedding: A small script rendering from a signed public endpoint · widgets are the delivery mechanism and must not slow down the host page - Disclosure: Incentive and relationship recorded at collection and rendered with the testimonial · a disclosure added later is a disclosure nobody made ### Data model (create this before Phase 1) - testimonials(id, kind, author_name, author_email, submitted_at, source, status) · source is direct, imported or manual - originals(testimonial_id, text, media_path, captured_at) · immutable, written once, never updated - revisions(testimonial_id, ordinal, text, edited_by, reason, at) · every change to displayed text, with the original always available - disclosures(testimonial_id, incentive_kind, incentive_value, relationship) · relationship is customer, employee, investor, family or other - consents(testimonial_id, scope, granted_at, evidence, withdrawn_at) · scope is what they agreed you could do with it - publications(testimonial_id, published, changed_by, reason, at) · append only, so unpublishing is a recorded act - collection_stats(period, received, published, unpublished) · the suppression figure, computed and shown rather than available on request ### Phase 1 · Collection that captures the disclosures with the words Build: A collection form taking text, audio or video, which also asks whether anything was received in exchange and what the person's relationship to the company is. Store the original immutably. Capture explicit consent for the uses you intend. Trap: Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. Done when: A submission without an incentive answer and a relationship answer is refused, tested directly. The original text or media is written once and cannot be updated, enforced by the schema. Consent scope is recorded with evidence of when and how it was given. A video submission of reasonable length uploads without passing through your application server. Do not build yet: No editing, display or embedding. ### Phase 2 · Editing that stays honest Build: Allow light editing with every revision recorded and the original always retrievable. Detect an edit that changes meaning rather than only length or punctuation, and require a confirmation naming the reason. Show edited testimonials as edited. Trap: An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. Done when: Every edit produces a revision and the original remains retrievable, verified after five edits. A change that alters meaning rather than length requires a confirmation with a reason. The displayed testimonial indicates it was edited. Deleting a revision is impossible, verified by attempting it. Do not build yet: No display, embedding or import yet. ### Phase 3 · Publishing, and the number nobody wants to see Build: Publish and unpublish testimonials, recording every change with who and why. Compute and display the ratio of received to published for each period. Show unpublished counts beside any aggregate rating you display. Trap: A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. Done when: Publishing changes are append only with a reason recorded. The received-to-published ratio is displayed in the main dashboard rather than hidden in a report. An aggregate rating states whether it includes unpublished testimonials. Unpublishing a testimonial does not delete it or its disclosures. Do not build yet: No embedding or import yet. ### Phase 4 · Showing them, with the disclosure attached Build: An embeddable widget rendering testimonials with the relationship and incentive disclosed in the same view, not behind a link. Keep the script small and asynchronous. Handle the widget being embedded on a page you do not control. Trap: Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. Done when: An embedded testimonial from an employee or an incentivised reviewer shows that fact adjacent to the text, verified on a test page. The widget script is small and loads asynchronously without blocking. The widget renders correctly inside a host page with conflicting styles. A withdrawn consent removes the testimonial from every embed within minutes. Do not build yet: No import or export yet. ### Phase 5 · Imports, which arrive without their provenance Build: Import existing testimonials from a file or a platform, marking each as imported with an unknown consent and disclosure status until reviewed. Refuse to publish an imported testimonial until its provenance questions are answered. Trap: An import that lands testimonials in the same state as directly collected ones erases the distinction between a testimonial you have consent evidence for and one you copied from somewhere, and once they are mixed you cannot tell them apart again. Mark on import, block publication until reviewed, and keep the original source recorded. Done when: Imported testimonials cannot be published until consent and disclosure questions are answered, tested by attempting it. The original source is recorded per import. Bulk review is possible without answering identically for every row. Export contains testimonials, originals, revisions, disclosures and consent evidence. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about your jurisdiction. This build encodes one regulator's rule and yours may differ or be stricter. - Automated video editing and captioning, which is most of what the hosted products actually charge for. - Review platform integrations, where each is a separate API and terms agreement. - Sentiment analysis that ranks testimonials, which is a short step from selecting only the flattering ones. ### README must contain - That incentive and relationship are captured at collection because they cannot be reconstructed later - That the original is immutable and every edit is a retained revision - That the received-to-published ratio is displayed rather than available on request - That disclosures render adjacent to the testimonial in every embed - That imported testimonials cannot be published until their provenance is answered ===== AGENTS.md ===== # Agent instructions · Feedspace indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Direct-to-storage uploads with a transcoding worker, Original text and media preserved, immutable, alongside any edit, A small script rendering from a signed public endpoint, Incentive and relationship recorded at collection and rendered with the testimonial. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. ===== BUILD_PLAN.md ===== # Build plan · Feedspace A tool for collecting written, audio and video testimonials and embedding them, where the entire design constraint is that the record must stay honest: originals preserved, incentives and relationships recorded, edits visible, and suppression measured rather than hidden. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Collection that captures the disclosures with the words A testimonial cannot be submitted without the facts you will later need to disclose. ### Steps 1. Require the disclosure fields at submission Incentive and relationship. Both required, answered by the person themselves. Files: `src/collect.ts` 2. Write the original immutably No update path in the schema. It is the evidence. Files: `src/schema.sql` 3. Upload media direct to storage Through your server is how the first long video takes the site down. Files: `src/uploads.ts` ### Done when - [ ] A submission without disclosures is refused - [ ] The original cannot be updated - [ ] Consent scope and evidence are recorded - [ ] A long video uploads without touching the app server ### Watch out - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. ## Phase 2 · Editing that stays honest You can tidy a testimonial without being able to change what it says. ### Steps 1. Record every revision with a reason Append only. The original is always one click away. Files: `src/revisions.ts` 2. Distinguish trimming from rewriting Length changes and meaning changes are different acts and deserve different friction. Files: `src/diff.ts`, `src/diff.test.ts` 3. Mark edited testimonials as edited On display, not in an admin panel nobody reads. Files: `src/render.ts` ### Done when - [ ] Every edit produces a retained revision - [ ] The original is always retrievable - [ ] A meaning change requires a reason - [ ] Revisions cannot be deleted ### Watch out - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. ## Phase 3 · Publishing, and the number nobody wants to see You can choose what to show, and you can see what that choice looks like. ### Steps 1. Record publication changes append only Who, when and why. Unpublishing is an act, not a state. Files: `src/publications.ts` 2. Compute and show the suppression ratio In the dashboard. A number in a report nobody opens is not a control. Files: `src/stats.ts` 3. Qualify every aggregate An average excluding unpublished testimonials must say so beside the number. Files: `src/aggregate.ts` ### Done when - [ ] Publication changes are append only with reasons - [ ] The suppression ratio appears in the dashboard - [ ] Aggregates state what they exclude - [ ] Unpublishing preserves the testimonial and disclosures ### Watch out - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. ## Phase 4 · Showing them, with the disclosure attached An embedded testimonial carries its disclosure wherever it appears. ### Steps 1. Render disclosures adjacent, not behind a link Same view, comparable visual weight to the author's name. Files: `widget/render.ts` 2. Keep the widget small and asynchronous It is on someone else's page. It must not slow it down. Files: `widget/embed.ts` 3. Isolate from host page styles A widget that inherits the host's styling breaks unpredictably. Files: `widget/isolate.ts` ### Done when - [ ] Disclosures render adjacent to the testimonial - [ ] The widget loads asynchronously and small - [ ] It renders correctly against conflicting styles - [ ] Withdrawn consent removes it from embeds quickly ### Watch out - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. ## Not in this build - Legal advice about your jurisdiction. - Automated video editing and captioning. - Review platform integrations. - Sentiment ranking of testimonials. ## After v1, if you want it - A request flow that asks past customers at the right moment, which is where collection volume comes from - Response to a testimonial, since replying publicly is often better than hiding a lukewarm one - Per-page widget variants, once the disclosure rendering is proven ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Testimonials, originals and consent evidence. Personal data about identified people. DATABASE_URL=postgres://localhost:5432/testimonials # Required. Audio and video. Direct uploads, with a lifecycle policy set before anything is stored. MEDIA_BUCKET=s3://testimonials-media # Required. Where the widget is served from. Separate origin, so a host page cannot reach into it. WIDGET_ORIGIN=https://embed.example.com # Required. Present as a constant so the answer is visible in configuration. There is no other value. SHOW_SUPPRESSION_RATIO=always # Required. Consent evidence outlives the testimonial, because it is what proves you were allowed to publish it. CONSENT_RETENTION_YEARS=7
You are building a lean indie version of Feedspace. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Feedspace · indie build A tool for collecting written, audio and video testimonials and embedding them, where the entire design constraint is that the record must stay honest: originals preserved, incentives and relationships recorded, edits visible, and suppression measured rather than hidden. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | the work is collection, provenance and embedding | | Media | Direct-to-storage uploads with a transcoding worker | video uploads through your server will fall over on the first long one | | Provenance | Original text and media preserved, immutable, alongside any edit | the original is the evidence and must survive every subsequent change | | Embedding | A small script rendering from a signed public endpoint | widgets are the delivery mechanism and must not slow down the host page | | Disclosure | Incentive and relationship recorded at collection and rendered with the testimonial | a disclosure added later is a disclosure nobody made | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server, the widget build and the transcoding worker. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Immutable originals, append-only publication history and consent records want real constraints. - Get it: Docker or your package manager. - Verify: psql connects and an insert-only table can be enforced - [ ] **Object storage with direct upload** · pennies per month at small volume - Why: Video testimonials are large and uploading them through your application server is how the first long submission takes the site down. - Get it: Any provider supporting presigned direct uploads. Configure a lifecycle policy before storing anything. - Verify: A large file uploads directly from a browser without passing through your server - [ ] **Which rules apply to you, written down** · free to read; an hour of advice if this is commercial - Why: This build encodes the United States position, which took effect on 21 October 2024 with civil penalties currently $51,744 per violation. Your jurisdiction may differ or be stricter, and the tool's behaviour should match the stricter one. - Get it: Identify where your customers are, read the applicable consumer protection guidance, and write down what you concluded. - Verify: A written note names the jurisdictions and what each requires of testimonials - [ ] **Ten real testimonials including an awkward one** · free - Why: The interesting cases are the ones the tool is meant to handle: a review from an employee, one where the person got a discount, and one that is lukewarm rather than glowing. - Get it: Collect real ones with permission, and deliberately include an insider and an incentivised submission to test the disclosure path. - Verify: Ten exist, including at least one insider and one incentivised ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Legal advice about your jurisdiction. - Automated video editing and captioning. - Review platform integrations. - Sentiment ranking of testimonials. - 150+ review sources with 120+ on auto-sync, on an extraction engine Feedspace owns and runs in-house - the upkeep · sources change and keeping the fetching working is someone else's job - rule-based curation · matching new reviews join a widget or wall page by themselves, so the embed never goes stale - in-browser video and audio recording that works on mobile without an app, and the media pipeline behind it - aggregate rating badges that stay accurate because the auto-syncs keep feeding them If one of those is essential to you, that is the reason to keep paying for Feedspace, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Feedspace The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a testimonial collection and display tool that cannot be used to mislead to replace Feedspace. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Testimonial software is a compliance product wearing a marketing product's clothes. In the United States the Federal Trade Commission's rule on consumer reviews and testimonials took effect on 21 October 2024, with civil penalties currently set at $51,744 per violation. It prohibits buying or selling fake reviews, incentivising reviews that express a particular sentiment, publishing reviews by company insiders without disclosing the relationship, and suppressing negative reviews while presenting the remainder as representative. Every one of those is a feature a testimonial tool could easily provide by accident. A publish toggle is review suppression if it is used to hide the negative ones. An edit field is fabrication if it changes what someone said. A reward for leaving a review is an incentive that has to be disclosed. So the build records, for every testimonial: whether anything was given in exchange, what relationship the person has to the company, and the unedited original. It shows suppression as a number the operator has to look at. It is designed so that using it honestly is easier than using it deceptively, because the alternative is a tool whose main function is to produce evidence against its owner. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · the work is collection, provenance and embedding - Media: Direct-to-storage uploads with a transcoding worker · video uploads through your server will fall over on the first long one - Provenance: Original text and media preserved, immutable, alongside any edit · the original is the evidence and must survive every subsequent change - Embedding: A small script rendering from a signed public endpoint · widgets are the delivery mechanism and must not slow down the host page - Disclosure: Incentive and relationship recorded at collection and rendered with the testimonial · a disclosure added later is a disclosure nobody made ### Data model (create this before Phase 1) - testimonials(id, kind, author_name, author_email, submitted_at, source, status) · source is direct, imported or manual - originals(testimonial_id, text, media_path, captured_at) · immutable, written once, never updated - revisions(testimonial_id, ordinal, text, edited_by, reason, at) · every change to displayed text, with the original always available - disclosures(testimonial_id, incentive_kind, incentive_value, relationship) · relationship is customer, employee, investor, family or other - consents(testimonial_id, scope, granted_at, evidence, withdrawn_at) · scope is what they agreed you could do with it - publications(testimonial_id, published, changed_by, reason, at) · append only, so unpublishing is a recorded act - collection_stats(period, received, published, unpublished) · the suppression figure, computed and shown rather than available on request ### Phase 1 · Collection that captures the disclosures with the words Build: A collection form taking text, audio or video, which also asks whether anything was received in exchange and what the person's relationship to the company is. Store the original immutably. Capture explicit consent for the uses you intend. Trap: Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. Done when: A submission without an incentive answer and a relationship answer is refused, tested directly. The original text or media is written once and cannot be updated, enforced by the schema. Consent scope is recorded with evidence of when and how it was given. A video submission of reasonable length uploads without passing through your application server. Do not build yet: No editing, display or embedding. ### Phase 2 · Editing that stays honest Build: Allow light editing with every revision recorded and the original always retrievable. Detect an edit that changes meaning rather than only length or punctuation, and require a confirmation naming the reason. Show edited testimonials as edited. Trap: An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. Done when: Every edit produces a revision and the original remains retrievable, verified after five edits. A change that alters meaning rather than length requires a confirmation with a reason. The displayed testimonial indicates it was edited. Deleting a revision is impossible, verified by attempting it. Do not build yet: No display, embedding or import yet. ### Phase 3 · Publishing, and the number nobody wants to see Build: Publish and unpublish testimonials, recording every change with who and why. Compute and display the ratio of received to published for each period. Show unpublished counts beside any aggregate rating you display. Trap: A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. Done when: Publishing changes are append only with a reason recorded. The received-to-published ratio is displayed in the main dashboard rather than hidden in a report. An aggregate rating states whether it includes unpublished testimonials. Unpublishing a testimonial does not delete it or its disclosures. Do not build yet: No embedding or import yet. ### Phase 4 · Showing them, with the disclosure attached Build: An embeddable widget rendering testimonials with the relationship and incentive disclosed in the same view, not behind a link. Keep the script small and asynchronous. Handle the widget being embedded on a page you do not control. Trap: Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. Done when: An embedded testimonial from an employee or an incentivised reviewer shows that fact adjacent to the text, verified on a test page. The widget script is small and loads asynchronously without blocking. The widget renders correctly inside a host page with conflicting styles. A withdrawn consent removes the testimonial from every embed within minutes. Do not build yet: No import or export yet. ### Phase 5 · Imports, which arrive without their provenance Build: Import existing testimonials from a file or a platform, marking each as imported with an unknown consent and disclosure status until reviewed. Refuse to publish an imported testimonial until its provenance questions are answered. Trap: An import that lands testimonials in the same state as directly collected ones erases the distinction between a testimonial you have consent evidence for and one you copied from somewhere, and once they are mixed you cannot tell them apart again. Mark on import, block publication until reviewed, and keep the original source recorded. Done when: Imported testimonials cannot be published until consent and disclosure questions are answered, tested by attempting it. The original source is recorded per import. Bulk review is possible without answering identically for every row. Export contains testimonials, originals, revisions, disclosures and consent evidence. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about your jurisdiction. This build encodes one regulator's rule and yours may differ or be stricter. - Automated video editing and captioning, which is most of what the hosted products actually charge for. - Review platform integrations, where each is a separate API and terms agreement. - Sentiment analysis that ranks testimonials, which is a short step from selecting only the flattering ones. ### README must contain - That incentive and relationship are captured at collection because they cannot be reconstructed later - That the original is immutable and every edit is a retained revision - That the received-to-published ratio is displayed rather than available on request - That disclosures render adjacent to the testimonial in every embed - That imported testimonials cannot be published until their provenance is answered ===== AGENTS.md ===== # Agent instructions · Feedspace indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Direct-to-storage uploads with a transcoding worker, Original text and media preserved, immutable, alongside any edit, A small script rendering from a signed public endpoint, Incentive and relationship recorded at collection and rendered with the testimonial. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. ===== BUILD_PLAN.md ===== # Build plan · Feedspace A tool for collecting written, audio and video testimonials and embedding them, where the entire design constraint is that the record must stay honest: originals preserved, incentives and relationships recorded, edits visible, and suppression measured rather than hidden. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Collection that captures the disclosures with the words A testimonial cannot be submitted without the facts you will later need to disclose. ### Steps 1. Require the disclosure fields at submission Incentive and relationship. Both required, answered by the person themselves. Files: `src/collect.ts` 2. Write the original immutably No update path in the schema. It is the evidence. Files: `src/schema.sql` 3. Upload media direct to storage Through your server is how the first long video takes the site down. Files: `src/uploads.ts` ### Done when - [ ] A submission without disclosures is refused - [ ] The original cannot be updated - [ ] Consent scope and evidence are recorded - [ ] A long video uploads without touching the app server ### Watch out - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. ## Phase 2 · Editing that stays honest You can tidy a testimonial without being able to change what it says. ### Steps 1. Record every revision with a reason Append only. The original is always one click away. Files: `src/revisions.ts` 2. Distinguish trimming from rewriting Length changes and meaning changes are different acts and deserve different friction. Files: `src/diff.ts`, `src/diff.test.ts` 3. Mark edited testimonials as edited On display, not in an admin panel nobody reads. Files: `src/render.ts` ### Done when - [ ] Every edit produces a retained revision - [ ] The original is always retrievable - [ ] A meaning change requires a reason - [ ] Revisions cannot be deleted ### Watch out - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. ## Phase 3 · Publishing, and the number nobody wants to see You can choose what to show, and you can see what that choice looks like. ### Steps 1. Record publication changes append only Who, when and why. Unpublishing is an act, not a state. Files: `src/publications.ts` 2. Compute and show the suppression ratio In the dashboard. A number in a report nobody opens is not a control. Files: `src/stats.ts` 3. Qualify every aggregate An average excluding unpublished testimonials must say so beside the number. Files: `src/aggregate.ts` ### Done when - [ ] Publication changes are append only with reasons - [ ] The suppression ratio appears in the dashboard - [ ] Aggregates state what they exclude - [ ] Unpublishing preserves the testimonial and disclosures ### Watch out - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. ## Phase 4 · Showing them, with the disclosure attached An embedded testimonial carries its disclosure wherever it appears. ### Steps 1. Render disclosures adjacent, not behind a link Same view, comparable visual weight to the author's name. Files: `widget/render.ts` 2. Keep the widget small and asynchronous It is on someone else's page. It must not slow it down. Files: `widget/embed.ts` 3. Isolate from host page styles A widget that inherits the host's styling breaks unpredictably. Files: `widget/isolate.ts` ### Done when - [ ] Disclosures render adjacent to the testimonial - [ ] The widget loads asynchronously and small - [ ] It renders correctly against conflicting styles - [ ] Withdrawn consent removes it from embeds quickly ### Watch out - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. ## Not in this build - Legal advice about your jurisdiction. - Automated video editing and captioning. - Review platform integrations. - Sentiment ranking of testimonials. ## After v1, if you want it - A request flow that asks past customers at the right moment, which is where collection volume comes from - Response to a testimonial, since replying publicly is often better than hiding a lukewarm one - Per-page widget variants, once the disclosure rendering is proven ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Testimonials, originals and consent evidence. Personal data about identified people. DATABASE_URL=postgres://localhost:5432/testimonials # Required. Audio and video. Direct uploads, with a lifecycle policy set before anything is stored. MEDIA_BUCKET=s3://testimonials-media # Required. Where the widget is served from. Separate origin, so a host page cannot reach into it. WIDGET_ORIGIN=https://embed.example.com # Required. Present as a constant so the answer is visible in configuration. There is no other value. SHOW_SUPPRESSION_RATIO=always # Required. Consent evidence outlives the testimonial, because it is what proves you were allowed to publish it. CONSENT_RETENTION_YEARS=7
You are building a production product version of Feedspace. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Feedspace · product brief ## Problem The form, the queue, the wall and the embed widget are one sitting, as the two tools here show. What is not: 150+ sources with 120+ on auto-sync, on an extraction engine Feedspace owns in-house, and widgets that fill themselves from curation rules. Only collecting written reviews through your own form? Build it. Want the ones you have elsewhere kept current? Don't. ## Product outcome A testimonial tool where the honest path is the easy one: originals preserved, incentives and relationships disclosed wherever the words appear, and the number of testimonials you chose not to publish visible on your own dashboard. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a small always-on host - somewhere to keep uploaded video and audio - ffmpeg to convert recordings into a web-playable file ## Explicit non-goals for v1 - Legal advice about your jurisdiction. - Automated video editing and captioning. - Review platform integrations. - Sentiment ranking of testimonials. - 150+ review sources with 120+ on auto-sync, on an extraction engine Feedspace owns and runs in-house - the upkeep · sources change and keeping the fetching working is someone else's job - rule-based curation · matching new reviews join a widget or wall page by themselves, so the embed never goes stale - in-browser video and audio recording that works on mobile without an app, and the media pipeline behind it - aggregate rating badges that stay accurate because the auto-syncs keep feeding them ## Success criteria - A submission without disclosures is confirmed refused - The original is confirmed to have no update path - The suppression ratio is confirmed visible in the dashboard - Disclosures are confirmed rendered adjacent in the widget - An unreviewed import is confirmed unpublishable ===== BRIEF.md ===== # Build brief · Feedspace The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a testimonial collection and display tool that cannot be used to mislead to replace Feedspace. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Testimonial software is a compliance product wearing a marketing product's clothes. In the United States the Federal Trade Commission's rule on consumer reviews and testimonials took effect on 21 October 2024, with civil penalties currently set at $51,744 per violation. It prohibits buying or selling fake reviews, incentivising reviews that express a particular sentiment, publishing reviews by company insiders without disclosing the relationship, and suppressing negative reviews while presenting the remainder as representative. Every one of those is a feature a testimonial tool could easily provide by accident. A publish toggle is review suppression if it is used to hide the negative ones. An edit field is fabrication if it changes what someone said. A reward for leaving a review is an incentive that has to be disclosed. So the build records, for every testimonial: whether anything was given in exchange, what relationship the person has to the company, and the unedited original. It shows suppression as a number the operator has to look at. It is designed so that using it honestly is easier than using it deceptively, because the alternative is a tool whose main function is to produce evidence against its owner. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · the work is collection, provenance and embedding - Media: Direct-to-storage uploads with a transcoding worker · video uploads through your server will fall over on the first long one - Provenance: Original text and media preserved, immutable, alongside any edit · the original is the evidence and must survive every subsequent change - Embedding: A small script rendering from a signed public endpoint · widgets are the delivery mechanism and must not slow down the host page - Disclosure: Incentive and relationship recorded at collection and rendered with the testimonial · a disclosure added later is a disclosure nobody made ### Data model (create this before Phase 1) - testimonials(id, kind, author_name, author_email, submitted_at, source, status) · source is direct, imported or manual - originals(testimonial_id, text, media_path, captured_at) · immutable, written once, never updated - revisions(testimonial_id, ordinal, text, edited_by, reason, at) · every change to displayed text, with the original always available - disclosures(testimonial_id, incentive_kind, incentive_value, relationship) · relationship is customer, employee, investor, family or other - consents(testimonial_id, scope, granted_at, evidence, withdrawn_at) · scope is what they agreed you could do with it - publications(testimonial_id, published, changed_by, reason, at) · append only, so unpublishing is a recorded act - collection_stats(period, received, published, unpublished) · the suppression figure, computed and shown rather than available on request ### Phase 1 · Collection that captures the disclosures with the words Build: A collection form taking text, audio or video, which also asks whether anything was received in exchange and what the person's relationship to the company is. Store the original immutably. Capture explicit consent for the uses you intend. Trap: Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. Done when: A submission without an incentive answer and a relationship answer is refused, tested directly. The original text or media is written once and cannot be updated, enforced by the schema. Consent scope is recorded with evidence of when and how it was given. A video submission of reasonable length uploads without passing through your application server. Do not build yet: No editing, display or embedding. ### Phase 2 · Editing that stays honest Build: Allow light editing with every revision recorded and the original always retrievable. Detect an edit that changes meaning rather than only length or punctuation, and require a confirmation naming the reason. Show edited testimonials as edited. Trap: An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. Done when: Every edit produces a revision and the original remains retrievable, verified after five edits. A change that alters meaning rather than length requires a confirmation with a reason. The displayed testimonial indicates it was edited. Deleting a revision is impossible, verified by attempting it. Do not build yet: No display, embedding or import yet. ### Phase 3 · Publishing, and the number nobody wants to see Build: Publish and unpublish testimonials, recording every change with who and why. Compute and display the ratio of received to published for each period. Show unpublished counts beside any aggregate rating you display. Trap: A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. Done when: Publishing changes are append only with a reason recorded. The received-to-published ratio is displayed in the main dashboard rather than hidden in a report. An aggregate rating states whether it includes unpublished testimonials. Unpublishing a testimonial does not delete it or its disclosures. Do not build yet: No embedding or import yet. ### Phase 4 · Showing them, with the disclosure attached Build: An embeddable widget rendering testimonials with the relationship and incentive disclosed in the same view, not behind a link. Keep the script small and asynchronous. Handle the widget being embedded on a page you do not control. Trap: Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. Done when: An embedded testimonial from an employee or an incentivised reviewer shows that fact adjacent to the text, verified on a test page. The widget script is small and loads asynchronously without blocking. The widget renders correctly inside a host page with conflicting styles. A withdrawn consent removes the testimonial from every embed within minutes. Do not build yet: No import or export yet. ### Phase 5 · Imports, which arrive without their provenance Build: Import existing testimonials from a file or a platform, marking each as imported with an unknown consent and disclosure status until reviewed. Refuse to publish an imported testimonial until its provenance questions are answered. Trap: An import that lands testimonials in the same state as directly collected ones erases the distinction between a testimonial you have consent evidence for and one you copied from somewhere, and once they are mixed you cannot tell them apart again. Mark on import, block publication until reviewed, and keep the original source recorded. Done when: Imported testimonials cannot be published until consent and disclosure questions are answered, tested by attempting it. The original source is recorded per import. Bulk review is possible without answering identically for every row. Export contains testimonials, originals, revisions, disclosures and consent evidence. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about your jurisdiction. This build encodes one regulator's rule and yours may differ or be stricter. - Automated video editing and captioning, which is most of what the hosted products actually charge for. - Review platform integrations, where each is a separate API and terms agreement. - Sentiment analysis that ranks testimonials, which is a short step from selecting only the flattering ones. ### README must contain - That incentive and relationship are captured at collection because they cannot be reconstructed later - That the original is immutable and every edit is a retained revision - That the received-to-published ratio is displayed rather than available on request - That disclosures render adjacent to the testimonial in every embed - That imported testimonials cannot be published until their provenance is answered ===== ARCHITECTURE.md ===== # Architecture · Feedspace ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | the work is collection, provenance and embedding | | Media | Direct-to-storage uploads with a transcoding worker | video uploads through your server will fall over on the first long one | | Provenance | Original text and media preserved, immutable, alongside any edit | the original is the evidence and must survive every subsequent change | | Embedding | A small script rendering from a signed public endpoint | widgets are the delivery mechanism and must not slow down the host page | | Disclosure | Incentive and relationship recorded at collection and rendered with the testimonial | a disclosure added later is a disclosure nobody made | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Collection | Capturing words, media and disclosures together. | Any form, provided incentive and relationship are required at submission. | | Provenance | Immutable originals and revision history. | Not swappable. The original is the evidence and an update path destroys it. | | Publication | Append-only publish state and the suppression ratio. | Any implementation that makes the ratio visible rather than available. | | Widget | Rendering with disclosures adjacent. | Any renderer, provided the disclosure is not behind a link. | | Import | Marking and blocking unreviewed testimonials. | Any importer, provided imported and collected never become indistinguishable. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Testimonials, originals and consent evidence. Personal data about identified people. - `MEDIA_BUCKET` · required · Audio and video. Direct uploads, with a lifecycle policy set before anything is stored. - `WIDGET_ORIGIN` · required · Where the widget is served from. Separate origin, so a host page cannot reach into it. - `SHOW_SUPPRESSION_RATIO` · required · Present as a constant so the answer is visible in configuration. There is no other value. - `CONSENT_RETENTION_YEARS` · required · Consent evidence outlives the testimonial, because it is what proves you were allowed to publish it. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Feedspace product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Direct-to-storage uploads with a transcoding worker, Original text and media preserved, immutable, alongside any edit, A small script rendering from a signed public endpoint, Incentive and relationship recorded at collection and rendered with the testimonial. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. - An import that lands testimonials in the same state as directly collected ones erases the distinction between a testimonial you have consent evidence for and one you copied from somewhere, and once they are mixed you cannot tell them apart again. Mark on import, block publication until reviewed, and keep the original source recorded. ===== MILESTONES.md ===== # Delivery milestones · Feedspace Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Collection that captures the disclosures with the words A testimonial cannot be submitted without the facts you will later need to disclose. ### Steps 1. Require the disclosure fields at submission Incentive and relationship. Both required, answered by the person themselves. Files: `src/collect.ts` 2. Write the original immutably No update path in the schema. It is the evidence. Files: `src/schema.sql` 3. Upload media direct to storage Through your server is how the first long video takes the site down. Files: `src/uploads.ts` ### Done when - [ ] A submission without disclosures is refused - [ ] The original cannot be updated - [ ] Consent scope and evidence are recorded - [ ] A long video uploads without touching the app server ### Watch out - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. ## M2 · Editing that stays honest You can tidy a testimonial without being able to change what it says. ### Steps 1. Record every revision with a reason Append only. The original is always one click away. Files: `src/revisions.ts` 2. Distinguish trimming from rewriting Length changes and meaning changes are different acts and deserve different friction. Files: `src/diff.ts`, `src/diff.test.ts` 3. Mark edited testimonials as edited On display, not in an admin panel nobody reads. Files: `src/render.ts` ### Done when - [ ] Every edit produces a retained revision - [ ] The original is always retrievable - [ ] A meaning change requires a reason - [ ] Revisions cannot be deleted ### Watch out - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. ## M3 · Publishing, and the number nobody wants to see You can choose what to show, and you can see what that choice looks like. ### Steps 1. Record publication changes append only Who, when and why. Unpublishing is an act, not a state. Files: `src/publications.ts` 2. Compute and show the suppression ratio In the dashboard. A number in a report nobody opens is not a control. Files: `src/stats.ts` 3. Qualify every aggregate An average excluding unpublished testimonials must say so beside the number. Files: `src/aggregate.ts` ### Done when - [ ] Publication changes are append only with reasons - [ ] The suppression ratio appears in the dashboard - [ ] Aggregates state what they exclude - [ ] Unpublishing preserves the testimonial and disclosures ### Watch out - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. ## M4 · Showing them, with the disclosure attached An embedded testimonial carries its disclosure wherever it appears. ### Steps 1. Render disclosures adjacent, not behind a link Same view, comparable visual weight to the author's name. Files: `widget/render.ts` 2. Keep the widget small and asynchronous It is on someone else's page. It must not slow it down. Files: `widget/embed.ts` 3. Isolate from host page styles A widget that inherits the host's styling breaks unpredictably. Files: `widget/isolate.ts` ### Done when - [ ] Disclosures render adjacent to the testimonial - [ ] The widget loads asynchronously and small - [ ] It renders correctly against conflicting styles - [ ] Withdrawn consent removes it from embeds quickly ### Watch out - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. ## M5 · Imports, which arrive without their provenance (production only) Testimonials brought in from elsewhere are marked as what they are. ### Steps 1. Mark imports and block publication Until provenance is answered. Mixing them with collected ones is irreversible. Files: `src/import.ts` 2. Record the source per import Where it came from, when, and who imported it. Files: `src/import.ts` 3. Export the complete record Originals, revisions, disclosures and consent. It is the evidence. Files: `src/export.ts` ### Done when - [ ] An unreviewed import cannot be published - [ ] The import source is recorded - [ ] Bulk review does not force identical answers - [ ] Export contains the complete provenance record ### Watch out - An import that lands testimonials in the same state as directly collected ones erases the distinction between a testimonial you have consent evidence for and one you copied from somewhere, and once they are mixed you cannot tell them apart again. Mark on import, block publication until reviewed, and keep the original source recorded. ===== OPERATIONS.md ===== # Operations · Feedspace ## Backup Consent evidence and originals are the records you may need to produce years later. Back them up with the retention that outlives the testimonials themselves, and test that a restore returns the originals rather than only the current revisions. ## Restore Restore and confirm originals and consent evidence survived, since a restore that returns only current revisions has lost exactly the material that demonstrates the testimonials are genuine. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The suppression ratio is the metric to watch, and it is for the operator rather than for you. A ratio drifting upward over a quarter is the pattern the rule was written about, whatever the individual reasons were. ## Incident checklist A testimonial published without its disclosure is the failure that matters. The widget renders disclosures from the same record, so if one is missing the disclosure was never captured, which points at the collection form rather than the display. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A submission without disclosures is confirmed refused - [ ] The original is confirmed to have no update path - [ ] The suppression ratio is confirmed visible in the dashboard - [ ] Disclosures are confirmed rendered adjacent in the widget - [ ] An unreviewed import is confirmed unpublishable ## Launch constraint Do not market omitted Feedspace capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Testimonials, originals and consent evidence. Personal data about identified people. DATABASE_URL=postgres://localhost:5432/testimonials # Required. Audio and video. Direct uploads, with a lifecycle policy set before anything is stored. MEDIA_BUCKET=s3://testimonials-media # Required. Where the widget is served from. Separate origin, so a host page cannot reach into it. WIDGET_ORIGIN=https://embed.example.com # Required. Present as a constant so the answer is visible in configuration. There is no other value. SHOW_SUPPRESSION_RATIO=always # Required. Consent evidence outlives the testimonial, because it is what proves you were allowed to publish it. CONSENT_RETENTION_YEARS=7
# Feedspace · indie build A tool for collecting written, audio and video testimonials and embedding them, where the entire design constraint is that the record must stay honest: originals preserved, incentives and relationships recorded, edits visible, and suppression measured rather than hidden. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | the work is collection, provenance and embedding | | Media | Direct-to-storage uploads with a transcoding worker | video uploads through your server will fall over on the first long one | | Provenance | Original text and media preserved, immutable, alongside any edit | the original is the evidence and must survive every subsequent change | | Embedding | A small script rendering from a signed public endpoint | widgets are the delivery mechanism and must not slow down the host page | | Disclosure | Incentive and relationship recorded at collection and rendered with the testimonial | a disclosure added later is a disclosure nobody made | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server, the widget build and the transcoding worker. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Immutable originals, append-only publication history and consent records want real constraints. - Get it: Docker or your package manager. - Verify: psql connects and an insert-only table can be enforced - [ ] **Object storage with direct upload** · pennies per month at small volume - Why: Video testimonials are large and uploading them through your application server is how the first long submission takes the site down. - Get it: Any provider supporting presigned direct uploads. Configure a lifecycle policy before storing anything. - Verify: A large file uploads directly from a browser without passing through your server - [ ] **Which rules apply to you, written down** · free to read; an hour of advice if this is commercial - Why: This build encodes the United States position, which took effect on 21 October 2024 with civil penalties currently $51,744 per violation. Your jurisdiction may differ or be stricter, and the tool's behaviour should match the stricter one. - Get it: Identify where your customers are, read the applicable consumer protection guidance, and write down what you concluded. - Verify: A written note names the jurisdictions and what each requires of testimonials - [ ] **Ten real testimonials including an awkward one** · free - Why: The interesting cases are the ones the tool is meant to handle: a review from an employee, one where the person got a discount, and one that is lukewarm rather than glowing. - Get it: Collect real ones with permission, and deliberately include an insider and an incentivised submission to test the disclosure path. - Verify: Ten exist, including at least one insider and one incentivised ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Legal advice about your jurisdiction. - Automated video editing and captioning. - Review platform integrations. - Sentiment ranking of testimonials. - 150+ review sources with 120+ on auto-sync, on an extraction engine Feedspace owns and runs in-house - the upkeep · sources change and keeping the fetching working is someone else's job - rule-based curation · matching new reviews join a widget or wall page by themselves, so the embed never goes stale - in-browser video and audio recording that works on mobile without an app, and the media pipeline behind it - aggregate rating badges that stay accurate because the auto-syncs keep feeding them If one of those is essential to you, that is the reason to keep paying for Feedspace, and the README should say so rather than pretend.
# Build brief · Feedspace The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a testimonial collection and display tool that cannot be used to mislead to replace Feedspace. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Testimonial software is a compliance product wearing a marketing product's clothes. In the United States the Federal Trade Commission's rule on consumer reviews and testimonials took effect on 21 October 2024, with civil penalties currently set at $51,744 per violation. It prohibits buying or selling fake reviews, incentivising reviews that express a particular sentiment, publishing reviews by company insiders without disclosing the relationship, and suppressing negative reviews while presenting the remainder as representative. Every one of those is a feature a testimonial tool could easily provide by accident. A publish toggle is review suppression if it is used to hide the negative ones. An edit field is fabrication if it changes what someone said. A reward for leaving a review is an incentive that has to be disclosed. So the build records, for every testimonial: whether anything was given in exchange, what relationship the person has to the company, and the unedited original. It shows suppression as a number the operator has to look at. It is designed so that using it honestly is easier than using it deceptively, because the alternative is a tool whose main function is to produce evidence against its owner. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · the work is collection, provenance and embedding - Media: Direct-to-storage uploads with a transcoding worker · video uploads through your server will fall over on the first long one - Provenance: Original text and media preserved, immutable, alongside any edit · the original is the evidence and must survive every subsequent change - Embedding: A small script rendering from a signed public endpoint · widgets are the delivery mechanism and must not slow down the host page - Disclosure: Incentive and relationship recorded at collection and rendered with the testimonial · a disclosure added later is a disclosure nobody made ### Data model (create this before Phase 1) - testimonials(id, kind, author_name, author_email, submitted_at, source, status) · source is direct, imported or manual - originals(testimonial_id, text, media_path, captured_at) · immutable, written once, never updated - revisions(testimonial_id, ordinal, text, edited_by, reason, at) · every change to displayed text, with the original always available - disclosures(testimonial_id, incentive_kind, incentive_value, relationship) · relationship is customer, employee, investor, family or other - consents(testimonial_id, scope, granted_at, evidence, withdrawn_at) · scope is what they agreed you could do with it - publications(testimonial_id, published, changed_by, reason, at) · append only, so unpublishing is a recorded act - collection_stats(period, received, published, unpublished) · the suppression figure, computed and shown rather than available on request ### Phase 1 · Collection that captures the disclosures with the words Build: A collection form taking text, audio or video, which also asks whether anything was received in exchange and what the person's relationship to the company is. Store the original immutably. Capture explicit consent for the uses you intend. Trap: Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. Done when: A submission without an incentive answer and a relationship answer is refused, tested directly. The original text or media is written once and cannot be updated, enforced by the schema. Consent scope is recorded with evidence of when and how it was given. A video submission of reasonable length uploads without passing through your application server. Do not build yet: No editing, display or embedding. ### Phase 2 · Editing that stays honest Build: Allow light editing with every revision recorded and the original always retrievable. Detect an edit that changes meaning rather than only length or punctuation, and require a confirmation naming the reason. Show edited testimonials as edited. Trap: An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. Done when: Every edit produces a revision and the original remains retrievable, verified after five edits. A change that alters meaning rather than length requires a confirmation with a reason. The displayed testimonial indicates it was edited. Deleting a revision is impossible, verified by attempting it. Do not build yet: No display, embedding or import yet. ### Phase 3 · Publishing, and the number nobody wants to see Build: Publish and unpublish testimonials, recording every change with who and why. Compute and display the ratio of received to published for each period. Show unpublished counts beside any aggregate rating you display. Trap: A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. Done when: Publishing changes are append only with a reason recorded. The received-to-published ratio is displayed in the main dashboard rather than hidden in a report. An aggregate rating states whether it includes unpublished testimonials. Unpublishing a testimonial does not delete it or its disclosures. Do not build yet: No embedding or import yet. ### Phase 4 · Showing them, with the disclosure attached Build: An embeddable widget rendering testimonials with the relationship and incentive disclosed in the same view, not behind a link. Keep the script small and asynchronous. Handle the widget being embedded on a page you do not control. Trap: Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. Done when: An embedded testimonial from an employee or an incentivised reviewer shows that fact adjacent to the text, verified on a test page. The widget script is small and loads asynchronously without blocking. The widget renders correctly inside a host page with conflicting styles. A withdrawn consent removes the testimonial from every embed within minutes. Do not build yet: No import or export yet. ### Phase 5 · Imports, which arrive without their provenance Build: Import existing testimonials from a file or a platform, marking each as imported with an unknown consent and disclosure status until reviewed. Refuse to publish an imported testimonial until its provenance questions are answered. Trap: An import that lands testimonials in the same state as directly collected ones erases the distinction between a testimonial you have consent evidence for and one you copied from somewhere, and once they are mixed you cannot tell them apart again. Mark on import, block publication until reviewed, and keep the original source recorded. Done when: Imported testimonials cannot be published until consent and disclosure questions are answered, tested by attempting it. The original source is recorded per import. Bulk review is possible without answering identically for every row. Export contains testimonials, originals, revisions, disclosures and consent evidence. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about your jurisdiction. This build encodes one regulator's rule and yours may differ or be stricter. - Automated video editing and captioning, which is most of what the hosted products actually charge for. - Review platform integrations, where each is a separate API and terms agreement. - Sentiment analysis that ranks testimonials, which is a short step from selecting only the flattering ones. ### README must contain - That incentive and relationship are captured at collection because they cannot be reconstructed later - That the original is immutable and every edit is a retained revision - That the received-to-published ratio is displayed rather than available on request - That disclosures render adjacent to the testimonial in every embed - That imported testimonials cannot be published until their provenance is answered
# Agent instructions · Feedspace indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Direct-to-storage uploads with a transcoding worker, Original text and media preserved, immutable, alongside any edit, A small script rendering from a signed public endpoint, Incentive and relationship recorded at collection and rendered with the testimonial. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name.
# Build plan · Feedspace A tool for collecting written, audio and video testimonials and embedding them, where the entire design constraint is that the record must stay honest: originals preserved, incentives and relationships recorded, edits visible, and suppression measured rather than hidden. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Collection that captures the disclosures with the words A testimonial cannot be submitted without the facts you will later need to disclose. ### Steps 1. Require the disclosure fields at submission Incentive and relationship. Both required, answered by the person themselves. Files: `src/collect.ts` 2. Write the original immutably No update path in the schema. It is the evidence. Files: `src/schema.sql` 3. Upload media direct to storage Through your server is how the first long video takes the site down. Files: `src/uploads.ts` ### Done when - [ ] A submission without disclosures is refused - [ ] The original cannot be updated - [ ] Consent scope and evidence are recorded - [ ] A long video uploads without touching the app server ### Watch out - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. ## Phase 2 · Editing that stays honest You can tidy a testimonial without being able to change what it says. ### Steps 1. Record every revision with a reason Append only. The original is always one click away. Files: `src/revisions.ts` 2. Distinguish trimming from rewriting Length changes and meaning changes are different acts and deserve different friction. Files: `src/diff.ts`, `src/diff.test.ts` 3. Mark edited testimonials as edited On display, not in an admin panel nobody reads. Files: `src/render.ts` ### Done when - [ ] Every edit produces a retained revision - [ ] The original is always retrievable - [ ] A meaning change requires a reason - [ ] Revisions cannot be deleted ### Watch out - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. ## Phase 3 · Publishing, and the number nobody wants to see You can choose what to show, and you can see what that choice looks like. ### Steps 1. Record publication changes append only Who, when and why. Unpublishing is an act, not a state. Files: `src/publications.ts` 2. Compute and show the suppression ratio In the dashboard. A number in a report nobody opens is not a control. Files: `src/stats.ts` 3. Qualify every aggregate An average excluding unpublished testimonials must say so beside the number. Files: `src/aggregate.ts` ### Done when - [ ] Publication changes are append only with reasons - [ ] The suppression ratio appears in the dashboard - [ ] Aggregates state what they exclude - [ ] Unpublishing preserves the testimonial and disclosures ### Watch out - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. ## Phase 4 · Showing them, with the disclosure attached An embedded testimonial carries its disclosure wherever it appears. ### Steps 1. Render disclosures adjacent, not behind a link Same view, comparable visual weight to the author's name. Files: `widget/render.ts` 2. Keep the widget small and asynchronous It is on someone else's page. It must not slow it down. Files: `widget/embed.ts` 3. Isolate from host page styles A widget that inherits the host's styling breaks unpredictably. Files: `widget/isolate.ts` ### Done when - [ ] Disclosures render adjacent to the testimonial - [ ] The widget loads asynchronously and small - [ ] It renders correctly against conflicting styles - [ ] Withdrawn consent removes it from embeds quickly ### Watch out - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. ## Not in this build - Legal advice about your jurisdiction. - Automated video editing and captioning. - Review platform integrations. - Sentiment ranking of testimonials. ## After v1, if you want it - A request flow that asks past customers at the right moment, which is where collection volume comes from - Response to a testimonial, since replying publicly is often better than hiding a lukewarm one - Per-page widget variants, once the disclosure rendering is proven
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Testimonials, originals and consent evidence. Personal data about identified people. DATABASE_URL=postgres://localhost:5432/testimonials # Required. Audio and video. Direct uploads, with a lifecycle policy set before anything is stored. MEDIA_BUCKET=s3://testimonials-media # Required. Where the widget is served from. Separate origin, so a host page cannot reach into it. WIDGET_ORIGIN=https://embed.example.com # Required. Present as a constant so the answer is visible in configuration. There is no other value. SHOW_SUPPRESSION_RATIO=always # Required. Consent evidence outlives the testimonial, because it is what proves you were allowed to publish it. CONSENT_RETENTION_YEARS=7
# Feedspace · product brief ## Problem The form, the queue, the wall and the embed widget are one sitting, as the two tools here show. What is not: 150+ sources with 120+ on auto-sync, on an extraction engine Feedspace owns in-house, and widgets that fill themselves from curation rules. Only collecting written reviews through your own form? Build it. Want the ones you have elsewhere kept current? Don't. ## Product outcome A testimonial tool where the honest path is the easy one: originals preserved, incentives and relationships disclosed wherever the words appear, and the number of testimonials you chose not to publish visible on your own dashboard. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - a small always-on host - somewhere to keep uploaded video and audio - ffmpeg to convert recordings into a web-playable file ## Explicit non-goals for v1 - Legal advice about your jurisdiction. - Automated video editing and captioning. - Review platform integrations. - Sentiment ranking of testimonials. - 150+ review sources with 120+ on auto-sync, on an extraction engine Feedspace owns and runs in-house - the upkeep · sources change and keeping the fetching working is someone else's job - rule-based curation · matching new reviews join a widget or wall page by themselves, so the embed never goes stale - in-browser video and audio recording that works on mobile without an app, and the media pipeline behind it - aggregate rating badges that stay accurate because the auto-syncs keep feeding them ## Success criteria - A submission without disclosures is confirmed refused - The original is confirmed to have no update path - The suppression ratio is confirmed visible in the dashboard - Disclosures are confirmed rendered adjacent in the widget - An unreviewed import is confirmed unpublishable
# Build brief · Feedspace The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a testimonial collection and display tool that cannot be used to mislead to replace Feedspace. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Testimonial software is a compliance product wearing a marketing product's clothes. In the United States the Federal Trade Commission's rule on consumer reviews and testimonials took effect on 21 October 2024, with civil penalties currently set at $51,744 per violation. It prohibits buying or selling fake reviews, incentivising reviews that express a particular sentiment, publishing reviews by company insiders without disclosing the relationship, and suppressing negative reviews while presenting the remainder as representative. Every one of those is a feature a testimonial tool could easily provide by accident. A publish toggle is review suppression if it is used to hide the negative ones. An edit field is fabrication if it changes what someone said. A reward for leaving a review is an incentive that has to be disclosed. So the build records, for every testimonial: whether anything was given in exchange, what relationship the person has to the company, and the unedited original. It shows suppression as a number the operator has to look at. It is designed so that using it honestly is easier than using it deceptively, because the alternative is a tool whose main function is to produce evidence against its owner. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · the work is collection, provenance and embedding - Media: Direct-to-storage uploads with a transcoding worker · video uploads through your server will fall over on the first long one - Provenance: Original text and media preserved, immutable, alongside any edit · the original is the evidence and must survive every subsequent change - Embedding: A small script rendering from a signed public endpoint · widgets are the delivery mechanism and must not slow down the host page - Disclosure: Incentive and relationship recorded at collection and rendered with the testimonial · a disclosure added later is a disclosure nobody made ### Data model (create this before Phase 1) - testimonials(id, kind, author_name, author_email, submitted_at, source, status) · source is direct, imported or manual - originals(testimonial_id, text, media_path, captured_at) · immutable, written once, never updated - revisions(testimonial_id, ordinal, text, edited_by, reason, at) · every change to displayed text, with the original always available - disclosures(testimonial_id, incentive_kind, incentive_value, relationship) · relationship is customer, employee, investor, family or other - consents(testimonial_id, scope, granted_at, evidence, withdrawn_at) · scope is what they agreed you could do with it - publications(testimonial_id, published, changed_by, reason, at) · append only, so unpublishing is a recorded act - collection_stats(period, received, published, unpublished) · the suppression figure, computed and shown rather than available on request ### Phase 1 · Collection that captures the disclosures with the words Build: A collection form taking text, audio or video, which also asks whether anything was received in exchange and what the person's relationship to the company is. Store the original immutably. Capture explicit consent for the uses you intend. Trap: Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. Done when: A submission without an incentive answer and a relationship answer is refused, tested directly. The original text or media is written once and cannot be updated, enforced by the schema. Consent scope is recorded with evidence of when and how it was given. A video submission of reasonable length uploads without passing through your application server. Do not build yet: No editing, display or embedding. ### Phase 2 · Editing that stays honest Build: Allow light editing with every revision recorded and the original always retrievable. Detect an edit that changes meaning rather than only length or punctuation, and require a confirmation naming the reason. Show edited testimonials as edited. Trap: An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. Done when: Every edit produces a revision and the original remains retrievable, verified after five edits. A change that alters meaning rather than length requires a confirmation with a reason. The displayed testimonial indicates it was edited. Deleting a revision is impossible, verified by attempting it. Do not build yet: No display, embedding or import yet. ### Phase 3 · Publishing, and the number nobody wants to see Build: Publish and unpublish testimonials, recording every change with who and why. Compute and display the ratio of received to published for each period. Show unpublished counts beside any aggregate rating you display. Trap: A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. Done when: Publishing changes are append only with a reason recorded. The received-to-published ratio is displayed in the main dashboard rather than hidden in a report. An aggregate rating states whether it includes unpublished testimonials. Unpublishing a testimonial does not delete it or its disclosures. Do not build yet: No embedding or import yet. ### Phase 4 · Showing them, with the disclosure attached Build: An embeddable widget rendering testimonials with the relationship and incentive disclosed in the same view, not behind a link. Keep the script small and asynchronous. Handle the widget being embedded on a page you do not control. Trap: Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. Done when: An embedded testimonial from an employee or an incentivised reviewer shows that fact adjacent to the text, verified on a test page. The widget script is small and loads asynchronously without blocking. The widget renders correctly inside a host page with conflicting styles. A withdrawn consent removes the testimonial from every embed within minutes. Do not build yet: No import or export yet. ### Phase 5 · Imports, which arrive without their provenance Build: Import existing testimonials from a file or a platform, marking each as imported with an unknown consent and disclosure status until reviewed. Refuse to publish an imported testimonial until its provenance questions are answered. Trap: An import that lands testimonials in the same state as directly collected ones erases the distinction between a testimonial you have consent evidence for and one you copied from somewhere, and once they are mixed you cannot tell them apart again. Mark on import, block publication until reviewed, and keep the original source recorded. Done when: Imported testimonials cannot be published until consent and disclosure questions are answered, tested by attempting it. The original source is recorded per import. Bulk review is possible without answering identically for every row. Export contains testimonials, originals, revisions, disclosures and consent evidence. Do not build yet: Nothing after this. ### Out of scope (and why) - Legal advice about your jurisdiction. This build encodes one regulator's rule and yours may differ or be stricter. - Automated video editing and captioning, which is most of what the hosted products actually charge for. - Review platform integrations, where each is a separate API and terms agreement. - Sentiment analysis that ranks testimonials, which is a short step from selecting only the flattering ones. ### README must contain - That incentive and relationship are captured at collection because they cannot be reconstructed later - That the original is immutable and every edit is a retained revision - That the received-to-published ratio is displayed rather than available on request - That disclosures render adjacent to the testimonial in every embed - That imported testimonials cannot be published until their provenance is answered
# Architecture · Feedspace ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | the work is collection, provenance and embedding | | Media | Direct-to-storage uploads with a transcoding worker | video uploads through your server will fall over on the first long one | | Provenance | Original text and media preserved, immutable, alongside any edit | the original is the evidence and must survive every subsequent change | | Embedding | A small script rendering from a signed public endpoint | widgets are the delivery mechanism and must not slow down the host page | | Disclosure | Incentive and relationship recorded at collection and rendered with the testimonial | a disclosure added later is a disclosure nobody made | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Collection | Capturing words, media and disclosures together. | Any form, provided incentive and relationship are required at submission. | | Provenance | Immutable originals and revision history. | Not swappable. The original is the evidence and an update path destroys it. | | Publication | Append-only publish state and the suppression ratio. | Any implementation that makes the ratio visible rather than available. | | Widget | Rendering with disclosures adjacent. | Any renderer, provided the disclosure is not behind a link. | | Import | Marking and blocking unreviewed testimonials. | Any importer, provided imported and collected never become indistinguishable. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Testimonials, originals and consent evidence. Personal data about identified people. - `MEDIA_BUCKET` · required · Audio and video. Direct uploads, with a lifecycle policy set before anything is stored. - `WIDGET_ORIGIN` · required · Where the widget is served from. Separate origin, so a host page cannot reach into it. - `SHOW_SUPPRESSION_RATIO` · required · Present as a constant so the answer is visible in configuration. There is no other value. - `CONSENT_RETENTION_YEARS` · required · Consent evidence outlives the testimonial, because it is what proves you were allowed to publish it. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Feedspace product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Direct-to-storage uploads with a transcoding worker, Original text and media preserved, immutable, alongside any edit, A small script rendering from a signed public endpoint, Incentive and relationship recorded at collection and rendered with the testimonial. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. - An import that lands testimonials in the same state as directly collected ones erases the distinction between a testimonial you have consent evidence for and one you copied from somewhere, and once they are mixed you cannot tell them apart again. Mark on import, block publication until reviewed, and keep the original source recorded.
# Delivery milestones · Feedspace Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Collection that captures the disclosures with the words A testimonial cannot be submitted without the facts you will later need to disclose. ### Steps 1. Require the disclosure fields at submission Incentive and relationship. Both required, answered by the person themselves. Files: `src/collect.ts` 2. Write the original immutably No update path in the schema. It is the evidence. Files: `src/schema.sql` 3. Upload media direct to storage Through your server is how the first long video takes the site down. Files: `src/uploads.ts` ### Done when - [ ] A submission without disclosures is refused - [ ] The original cannot be updated - [ ] Consent scope and evidence are recorded - [ ] A long video uploads without touching the app server ### Watch out - Asking about incentives and relationships later, when preparing to publish, produces answers reconstructed from memory or simply guessed, which is exactly the record that will not hold up. Ask at collection, when the person themselves can answer, and make both fields required rather than optional. ## M2 · Editing that stays honest You can tidy a testimonial without being able to change what it says. ### Steps 1. Record every revision with a reason Append only. The original is always one click away. Files: `src/revisions.ts` 2. Distinguish trimming from rewriting Length changes and meaning changes are different acts and deserve different friction. Files: `src/diff.ts`, `src/diff.test.ts` 3. Mark edited testimonials as edited On display, not in an admin panel nobody reads. Files: `src/render.ts` ### Done when - [ ] Every edit produces a retained revision - [ ] The original is always retrievable - [ ] A meaning change requires a reason - [ ] Revisions cannot be deleted ### Watch out - An unlogged edit field turns a testimonial into a quotation you wrote yourself, and the person who said it has no idea. Every displayed version is a revision with the original beside it, and trimming for length is a different act from changing a word, so make the tool distinguish them. ## M3 · Publishing, and the number nobody wants to see You can choose what to show, and you can see what that choice looks like. ### Steps 1. Record publication changes append only Who, when and why. Unpublishing is an act, not a state. Files: `src/publications.ts` 2. Compute and show the suppression ratio In the dashboard. A number in a report nobody opens is not a control. Files: `src/stats.ts` 3. Qualify every aggregate An average excluding unpublished testimonials must say so beside the number. Files: `src/aggregate.ts` ### Done when - [ ] Publication changes are append only with reasons - [ ] The suppression ratio appears in the dashboard - [ ] Aggregates state what they exclude - [ ] Unpublishing preserves the testimonial and disclosures ### Watch out - A publish toggle with no visible consequence is a review suppression mechanism, and the operator using it will not think of it that way until a regulator does. Compute the suppression ratio, display it in the dashboard rather than in a report, and make an aggregate rating that excludes unpublished testimonials say so. ## M4 · Showing them, with the disclosure attached An embedded testimonial carries its disclosure wherever it appears. ### Steps 1. Render disclosures adjacent, not behind a link Same view, comparable visual weight to the author's name. Files: `widget/render.ts` 2. Keep the widget small and asynchronous It is on someone else's page. It must not slow it down. Files: `widget/embed.ts` 3. Isolate from host page styles A widget that inherits the host's styling breaks unpredictably. Files: `widget/isolate.ts` ### Done when - [ ] Disclosures render adjacent to the testimonial - [ ] The widget loads asynchronously and small - [ ] It renders correctly against conflicting styles - [ ] Withdrawn consent removes it from embeds quickly ### Watch out - Putting the disclosure behind a link, in a tooltip, or in small print below the fold makes it a disclosure that was technically present and practically absent, which is the arrangement the rule was written about. Render it adjacent to the testimonial in the same visual weight as the author's name. ## M5 · Imports, which arrive without their provenance (production only) Testimonials brought in from elsewhere are marked as what they are. ### Steps 1. Mark imports and block publication Until provenance is answered. Mixing them with collected ones is irreversible. Files: `src/import.ts` 2. Record the source per import Where it came from, when, and who imported it. Files: `src/import.ts` 3. Export the complete record Originals, revisions, disclosures and consent. It is the evidence. Files: `src/export.ts` ### Done when - [ ] An unreviewed import cannot be published - [ ] The import source is recorded - [ ] Bulk review does not force identical answers - [ ] Export contains the complete provenance record ### Watch out - An import that lands testimonials in the same state as directly collected ones erases the distinction between a testimonial you have consent evidence for and one you copied from somewhere, and once they are mixed you cannot tell them apart again. Mark on import, block publication until reviewed, and keep the original source recorded.
# Operations · Feedspace ## Backup Consent evidence and originals are the records you may need to produce years later. Back them up with the retention that outlives the testimonials themselves, and test that a restore returns the originals rather than only the current revisions. ## Restore Restore and confirm originals and consent evidence survived, since a restore that returns only current revisions has lost exactly the material that demonstrates the testimonials are genuine. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The suppression ratio is the metric to watch, and it is for the operator rather than for you. A ratio drifting upward over a quarter is the pattern the rule was written about, whatever the individual reasons were. ## Incident checklist A testimonial published without its disclosure is the failure that matters. The widget renders disclosures from the same record, so if one is missing the disclosure was never captured, which points at the collection form rather than the display. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A submission without disclosures is confirmed refused - [ ] The original is confirmed to have no update path - [ ] The suppression ratio is confirmed visible in the dashboard - [ ] Disclosures are confirmed rendered adjacent in the widget - [ ] An unreviewed import is confirmed unpublishable ## Launch constraint Do not market omitted Feedspace capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Testimonials, originals and consent evidence. Personal data about identified people. DATABASE_URL=postgres://localhost:5432/testimonials # Required. Audio and video. Direct uploads, with a lifecycle policy set before anything is stored. MEDIA_BUCKET=s3://testimonials-media # Required. Where the widget is served from. Separate origin, so a host page cannot reach into it. WIDGET_ORIGIN=https://embed.example.com # Required. Present as a constant so the answer is visible in configuration. There is no other value. SHOW_SUPPRESSION_RATIO=always # Required. Consent evidence outlives the testimonial, because it is what proves you were allowed to publish it. CONSENT_RETENTION_YEARS=7
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because the hand-built version decays. A wall you assemble yourself is accurate the week you build it and stale three months later, the newest review sitting in an inbox nobody pasted from. Here neither end stays manual: reviews arrive on their own, rules decide what reaches the widget. Rebuilding it means owning the fetching, not just the form.
x150+ review sources with 120+ on auto-sync, on an extraction engine Feedspace owns and runs in-house
xthe upkeep · sources change and keeping the fetching working is someone else's job
xrule-based curation · matching new reviews join a widget or wall page by themselves, so the embed never goes stale
xin-browser video and audio recording that works on mobile without an app, and the media pipeline behind it
xaggregate rating badges that stay accurate because the auto-syncs keep feeding them
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Feedspace pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| starter | $0/workspace | $0/workspace | 1 workspace; 1 collection form; 20 text + 20 video reviews total; 1-minute video limit; 1 GB storage; 1 scheduler; unlimited Walls/widgets; 1 member. |
| professional | $29/workspace | $24.17/workspace | 1 workspace; unlimited forms, text and video reviews; 3-minute video limit; 50 GB storage; 5 schedulers; 5 members. |
| business | $249/workspace | $207.50/workspace | Unlimited workspaces, forms and reviews; 5-minute HD video; 250 GB storage; 25 members; 10 custom reels/month. |
free tier1 workspace; 1 collection form; 20 text + 20 video reviews total; 1-minute video limit; 1 GB storage; 1 scheduler; unlimited Walls/widgets; 1 member.
billingMonthly + annual; annual billing gives 2 months free.
hidden costsStorage/workspace limits force an upgrade when reached; no public overage rate is listed. Paid plans include a 7-day trial and a money-back window.
verified 2026-08-12 · source ↗
Vibecode Feedspace
Kinda. The core of Feedspace is buildable in a weekend with the prompt on this page, but there are real gaps: 150+ review sources with 120+ on auto-sync, on an extraction engine Feedspace owns and runs in-house, the upkeep · sources change and keeping the fetching working is someone else's job. Read the honest list above before committing.
How much does Feedspace cost?
Feedspace costs about $29/month (Professional, checked 2026-08-03), which is $348 per year.
What do I lose by replacing Feedspace?
Honestly: 150+ review sources with 120+ on auto-sync, on an extraction engine Feedspace owns and runs in-house; the upkeep · sources change and keeping the fetching working is someone else's job; rule-based curation · matching new reviews join a widget or wall page by themselves, so the embed never goes stale; in-browser video and audio recording that works on mobile without an app, and the media pipeline behind it; aggregate rating badges that stay accurate because the auto-syncs keep feeding them. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Feedspace?
Yes: Shosay (Unlimited text, video and audio proof, imports, walls and 25-plus widgets; the bill is zero, the tiny Powered by pill is not.) The prompt is for when you want it exactly your way.