Build Survicate
KINDAback to the verdict
A survey tool whose hard problems are versioning questions so historical answers stay meaningful, keeping partial responses rather than losing them at the close of a tab, and embedding on someone else's site without breaking it.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the application and the collection endpoint.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - have readya few pounds a month, or free on a small tier
Why A survey nobody can reach is a form, and the embed must be loaded from a real origin over HTTPS to test the policy behaviour.
Get it Any small host or tunnel that gives you an HTTPS origin.
Verify
The widget script loads on an external page over HTTPS - have readyfree
Why Embed isolation is only provable against a hostile page, and every real site is one.
Get it A static page with global element selectors and a restrictive policy header, served locally.
Verify
The widget renders correctly and reports clearly if blocked - decidefree
Why Naming it after the registered mark in anything commercial needs written consent, and the decision is easier before the labels are in the code.
Get it Read the current licensing terms, then write down the name you will use in the interface and the exports. open ↗
Verify
The chosen name appears in the schema, interface and README - decidefree
Why Both are enforced by code and both are tempting to set after seeing the data, which is the wrong order.
Get it Write down how long responses are kept and the smallest sample that may be shown as a percentage.
Verify
Both are in configuration and named in tests
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- surveys(id, name, status, created_at) - survey_versions(id, survey_id, version, schema_json, published_at) · immutable once published - responses(id, survey_id, version_id, token_hash, started_at, last_seen_at, completed_at, source, locale) - answers(response_id, question_key, value_json, answered_at) · written as each question is answered - invites(id, survey_id, token_hash, label, expires_at, used_at) · the raw token is never stored - events(id, response_id, kind, at) · started, resumed, submitted, abandoned, with nothing else recorded - question_keys are stable identifiers whose meaning never changes; changing the wording means a new version, not a new meaning for an old key.
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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/surveys.db | Surveys, versions, responses and answers. |
PUBLIC_BASE_URL | required | https://feedback.example.com | The origin the embed script and share links are built from. |
ADMIN_PASSWORD_HASHsecret | required | argon2id$...redacted | The admin side. The collection route needs no login and must not share the session. |
TOKEN_PEPPERsecret | required | a long random string, redacted | Mixed in before hashing invite tokens so the table is not a lookup list. |
RETENTION_DAYS | required | 365 | Responses older than this are deleted on a schedule. |
MIN_SAMPLE_FOR_PERCENT | required | 20 | Below this a report shows counts rather than a percentage. |
The build, in order
Questions that stay the same question
Publishing freezes a version and every response names the version it answered.
An immutable row is the only version control that survives a busy afternoon.
Files
src/lib/versions.tstests/immutable-version.test.tsWithout this the data is uninterpretable within a month.
Files
src/db/schema.sqlThe one mistake that quietly corrupts a year of answers.
Files
src/lib/publish.ts
done when · tick each as it passeswatch out- Editing a live question in place because it was only a typo means the fifty answers collected yesterday and the fifty collected tomorrow were given to different questions, and the chart that averages them is wrong in a way no test and no reviewer will notice. Freeze on publish and version the schema, even for a typo.
Answers saved the moment they are given
A response abandoned at question nine still contains nine answers.
The abandoned responses are data, and often the interesting kind.
Files
src/app/api/respond/route.tsMobile networks retry, and people tap twice.
Files
tests/idempotent-submit.test.tsThe table should not be readable as a roster of who answered what.
Files
src/lib/tokens.ts
done when · tick each as it passeswatch out- Collecting the whole survey in the browser and posting it at the end loses every long response to a closed tab, a flat battery or a lost signal, and long responses come disproportionately from the people who had the most to say. Persist per answer, and treat submit as a state change rather than as the moment the data arrives.
Logic evaluated where it cannot be edited
Branching and required questions are decided by the server.
The client asks what comes next; it does not decide.
Files
src/lib/logic.tstests/logic.test.tsThis is the check that makes the rest of the logic mean anything.
Files
src/app/api/respond/route.tsBetter than discovering it when the data has a hole in it.
Files
src/lib/publish.ts
done when · tick each as it passeswatch out- Enforcing required questions and branch conditions in the browser means anyone with the developer tools open can submit whatever they like, and more commonly a stale client from a previous version submits answers to questions the current branch never showed, producing rows nobody can explain. Decide the next question server side and reject an answer to a question the server did not ask.
An embed that cannot break someone's site
The widget renders inside a shadow root, styles nothing outside it, and fails quietly.
The only reliable boundary between your CSS and theirs.
Files
src/embed/widget.tsInline code is what a strict policy blocks first.
Files
src/embed/loader.tsA demo page with no CSS proves nothing.
Files
tests/embed-isolation.test.ts
done when · tick each as it passeswatch out- Injecting the widget into the host page's DOM inherits their styles, so a survey that looks correct on your demo page renders as unstyled text on a real site, and any inline script you rely on is blocked outright by a content security policy with no error the site owner will ever see. Render into a shadow root, keep the script external, and log a clear failure to the console when the policy blocks you.
Numbers that do not flatter youproduct builder
Every reported figure states its denominator, and a respondent can be removed.
A percentage without a count is a decoration.
Files
src/app/reports/page.tsxOtherwise the export is only readable by this app, this month.
Files
src/lib/export.tsWithout keeping the content you were asked to remove.
Files
src/lib/deletion.tstests/deletion.test.ts
done when · tick each as it passeswatch out- Computing a completion rate over completed responses only makes it ninety nine per cent forever, and quoting an average score without the number of responses behind it turns four answers into a company metric. Show the denominator next to every rate, and refuse to draw a chart below a stated minimum.
That is the whole plan for Survicate. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- The Net Promoter name and marks.
- Visitor identification and cross-site tracking.
- Email and mobile survey channels.
- A template and integration catalogue.
- spam and abuse defense at scale
- large integration and template catalog
- compliance controls and guaranteed delivery
- resilient polling of malformed and rate-limited feeds
- enterprise workflow logic
- Translations per version, which the immutable schema makes safe to add
- A sampling report showing who was eligible and who was shown the survey
- Scheduled exports to a folder, so the data outlives the app without anyone remembering to click
Need the files? The project pack on the verdict page hands your agent the whole brief · more forms.