Build LimeSurvey Cloud
KINDAback to the verdict
A self-hosted survey tool whose hard problems are making anonymity structural rather than promised, keeping branching logic honest when a question is edited after responses exist, and closing a quota at the right moment when several people are answering at once.
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 migrations.
Get it nodejs.org or your version manager. open ↗
Verify
node --version prints v22 or higher - decidefree
Why It is the only irreversible decision in the build and it determines the schema you generate.
Get it For each survey you plan to run, write down which mode it is and what you will tell respondents on the first page.
Verify
Every planned survey has a mode and a sentence of respondent-facing wording - have readya few pounds a month
Why Respondents are typing answers about themselves into a page you sent them, and a certificate warning ends that immediately.
Get it Any small instance with an automatic certificate, or a platform that provides one.
Verify
The public survey URL loads with a valid certificate on a phone - decidefree
Why Deciding how long you keep other people's answers after you have collected them means deciding it when deleting is inconvenient.
Get it Choose a number of days per survey and put it in the configuration before the survey opens.
Verify
Every survey has a retention period set before it accepts a response - have readypennies per month
Why Responses cannot be recollected, and a survey that lost half its answers is not a survey you can salvage.
Get it Any object storage or a second machine with a scheduled copy.
Verify
A restore from the copy reproduces the response count
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- surveys(id, title, mode, state, opens_at, closes_at, created_at) · mode is anonymous or identified and is immutable after the first response - questions(id, survey_id, code, kind, required, position, version, retired_at) · a meaningful edit makes a new version rather than mutating the row - options(id, question_id, question_version, value, label, position) - responses(id, survey_id, state, started_at, submitted_at_day, completed) · identified surveys add participant_id; anonymous surveys do not have the column - answers(response_id, question_id, question_version, value_json) · the version is what makes an old answer still readable - participants(id, survey_id, token_hash, invited_at, completed_at) · exists only for identified surveys - quotas(id, survey_id, name, limit_count, action, current_count) · counted in the same transaction that writes the response - rules(id, survey_id, question_id, question_version, condition_json, effect) · the branching tree, evaluated server side
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, questions, responses and quotas. Back this up on a schedule. |
UPLOAD_DIR | required | ./data/uploads | Respondent file uploads. Deleted with the responses at retention. |
ADMIN_PASSWORD_HASHsecret | required | argon2id$...redacted | The single administrator login. Generated by the included command. |
PUBLIC_BASE_URL | required | https://surveys.example.org | Used in invitation links and in the codebook header. |
DEFAULT_RETENTION_DAYS | required | 365 | Applied to a survey that does not set its own. Shorter is a reasonable default. |
MAX_UPLOAD_BYTES | required | 5242880 | Per file. Uploads are the fastest way to fill a small disk. |
The build, in order
Two kinds of survey, decided once
Creating a survey chooses anonymous or identified, and the storage differs accordingly.
A missing column cannot be joined by accident.
Files
db/schema.sqlsrc/lib/surveys.tsThis is the one irreversible decision in the product.
Files
src/lib/surveys.tstests/anonymity.test.tsInvitation time plus submission time is an identifier.
Files
src/lib/responses.ts
done when · tick each as it passeswatch out- Storing a nullable participant column and leaving it empty for anonymous surveys means the anonymity depends on every future query remembering to ignore it, and one CSV export written in a hurry six months later is enough to break the promise. Make the column absent, so a report that tries to join cannot even be written.
Questions that can change without rewriting history
Editing a live survey never alters the meaning of answers already given.
Otherwise the editor becomes unusable and people stop editing.
Files
src/lib/questions.tsIt is the only thing that makes an old answer readable.
Files
src/lib/answers.tstests/versioning.test.tsThe person editing is the only one who knows if the meaning changed.
Files
src/app/admin/questions/page.tsx
done when · tick each as it passeswatch out- Editing an option label in place from 'Weekly' to 'Weekly or more often' silently reinterprets every answer already collected under the old wording, and no one will ever know because the report shows a single tidy bar. Version the question, keep both labels, and make the report show them as separate series until a human merges them deliberately.
Branching that the browser cannot argue with
Skip logic is evaluated on the server and a skipped question cannot be answered.
The client is a display, not an authority.
Files
src/lib/logic.tstests/logic.test.tsA respondent should never be the one who finds the loop.
Files
src/lib/rules.tsStale answers to unasked questions are the same corruption, arriving politely.
Files
src/lib/responses.ts
done when · tick each as it passeswatch out- Evaluating skip logic in the browser and trusting the posted answers means anyone can submit answers to questions they were never shown, which quietly corrupts every cross-tabulation in the report and is undetectable afterwards. Evaluate on the server, discard out of scope answers with a recorded reason, and count the discards.
Quotas that close at the right moment
A quota of one hundred ends with one hundred responses, not one hundred and four.
Everything else about quotas is presentation.
Files
src/lib/quotas.tstests/quota-race.test.tsDiscarding a partial answer is a choice, so it should be one.
Files
src/lib/quotas.tsCollapsing them is how people over-collect.
Files
src/app/admin/responses/page.tsx
done when · tick each as it passeswatch out- Checking the quota before writing the response and then writing it leaves a window where four people submitting in the same second all pass a check that says ninety nine, and the overshoot is only visible weeks later in the data. Do the count and the insert in one transaction, and test it by firing concurrent submissions at a quota with one place left.
Getting the data out and deleting itproduct builder
Exports carry their own meaning, and a retention date actually removes data.
A CSV of option indexes with no labels is not data.
Files
src/lib/export.tstests/export.test.tsThe file somebody attached is the most identifying thing in the survey.
Files
src/lib/retention.tsYou will be asked to show that you did it.
Files
src/lib/requests.ts
done when · tick each as it passeswatch out- An export that flattens two versions of a question into one column and omits the labels produces a file that looks fine and is wrong, and it will be the file somebody quotes in a report. Ship a codebook beside every export naming every question version and option label, and refuse to export without it.
That is the whole plan for LimeSurvey Cloud. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Bulk invitation sending.
- Panel and respondent marketplace integrations.
- Weighting and significance testing.
- Multi-administrator roles and approvals.
- spam and abuse defense at scale
- large integration and template catalog
- compliance controls and guaranteed delivery
- enterprise workflow logic
- Randomised question and option order, recorded per response so the analysis can account for it
- A read-only results link that shows aggregate counts and never a single response
- Resumable responses by a link the respondent keeps, which is a different thing from an invitation token
Need the files? The project pack on the verdict page hands your agent the whole brief · more forms.