Build Feedspace

KINDAreplaces $29/mosaves $348/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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-daythe files for this build are in the project pack

RuntimeTypeScript with Fastify and PostgreSQLMediaDirect-to-storage uploads with a transcoding workerProvenanceOriginal text and media preserved, immutable, alongside any editEmbeddingA small script rendering from a signed public endpointDisclosureIncentive and relationship recorded at collection and rendered with the testimonial

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server, the widget build and the transcoding worker.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Immutable originals, append-only publication history and consent records want real constraints.

    Get it Docker or your package manager. open ↗

    Verify psql connects and an insert-only table can be enforced

  3. 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

  4. decidefree 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. open ↗

    Verify A written note names the jurisdictions and what each requires of testimonials

  5. have readyfree

    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

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- 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

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/testimonialsTestimonials, originals and consent evidence. Personal data about identified people.
MEDIA_BUCKETrequireds3://testimonials-mediaAudio and video. Direct uploads, with a lifecycle policy set before anything is stored.
WIDGET_ORIGINrequiredhttps://embed.example.comWhere the widget is served from. Separate origin, so a host page cannot reach into it.
SHOW_SUPPRESSION_RATIOrequiredalwaysPresent as a constant so the answer is visible in configuration. There is no other value.
CONSENT_RETENTION_YEARSrequired7Consent evidence outlives the testimonial, because it is what proves you were allowed to publish it.

The build, in order

  1. Collection that captures the disclosures with the words

    A testimonial cannot be submitted without the facts you will later need to disclose.

    1. Incentive and relationship. Both required, answered by the person themselves.

      Files src/collect.ts

    2. No update path in the schema. It is the evidence.

      Files src/schema.sql

    3. Through your server is how the first long video takes the site down.

      Files src/uploads.ts

    done when · tick each as it passes
    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.
  2. Editing that stays honest

    You can tidy a testimonial without being able to change what it says.

    1. Append only. The original is always one click away.

      Files src/revisions.ts

    2. Length changes and meaning changes are different acts and deserve different friction.

      Files src/diff.tssrc/diff.test.ts

    3. On display, not in an admin panel nobody reads.

      Files src/render.ts

    done when · tick each as it passes
    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.
  3. Publishing, and the number nobody wants to see

    You can choose what to show, and you can see what that choice looks like.

    1. Who, when and why. Unpublishing is an act, not a state.

      Files src/publications.ts

    2. In the dashboard. A number in a report nobody opens is not a control.

      Files src/stats.ts

    3. An average excluding unpublished testimonials must say so beside the number.

      Files src/aggregate.ts

    done when · tick each as it passes
    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.
  4. Showing them, with the disclosure attached

    An embedded testimonial carries its disclosure wherever it appears.

    1. Same view, comparable visual weight to the author's name.

      Files widget/render.ts

    2. It is on someone else's page. It must not slow it down.

      Files widget/embed.ts

    3. A widget that inherits the host's styling breaks unpredictably.

      Files widget/isolate.ts

    done when · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more testimonials.