Build LimeSurvey Cloud

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

estimated effort weekendthe files for this build are in the project pack

RuntimeNext.js with TypeScript, server rendered, one deployableStorageSQLite with responses in one table and answers keyed by question versionAnonymityA per-survey mode set at creation, enforced by which columns existLogicBranching evaluated server side from a stored rule tree, never from client stateQuotasCounted in a transaction at submission, with the close decided by the database

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 application and the migrations.

    Get it nodejs.org or your version manager. open ↗

    Verify node --version prints v22 or higher

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

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

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

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

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/surveys.dbSurveys, questions, responses and quotas. Back this up on a schedule.
UPLOAD_DIRrequired./data/uploadsRespondent file uploads. Deleted with the responses at retention.
ADMIN_PASSWORD_HASHsecretrequiredargon2id$...redactedThe single administrator login. Generated by the included command.
PUBLIC_BASE_URLrequiredhttps://surveys.example.orgUsed in invitation links and in the codebook header.
DEFAULT_RETENTION_DAYSrequired365Applied to a survey that does not set its own. Shorter is a reasonable default.
MAX_UPLOAD_BYTESrequired5242880Per file. Uploads are the fastest way to fill a small disk.

The build, in order

  1. Two kinds of survey, decided once

    Creating a survey chooses anonymous or identified, and the storage differs accordingly.

    1. A missing column cannot be joined by accident.

      Files db/schema.sqlsrc/lib/surveys.ts

    2. This is the one irreversible decision in the product.

      Files src/lib/surveys.tstests/anonymity.test.ts

    3. Invitation time plus submission time is an identifier.

      Files src/lib/responses.ts

    done when · tick each as it passes
    watch 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.
  2. Questions that can change without rewriting history

    Editing a live survey never alters the meaning of answers already given.

    1. Otherwise the editor becomes unusable and people stop editing.

      Files src/lib/questions.ts

    2. It is the only thing that makes an old answer readable.

      Files src/lib/answers.tstests/versioning.test.ts

    3. The 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 passes
    watch 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.
  3. Branching that the browser cannot argue with

    Skip logic is evaluated on the server and a skipped question cannot be answered.

    1. The client is a display, not an authority.

      Files src/lib/logic.tstests/logic.test.ts

    2. A respondent should never be the one who finds the loop.

      Files src/lib/rules.ts

    3. Stale answers to unasked questions are the same corruption, arriving politely.

      Files src/lib/responses.ts

    done when · tick each as it passes
    watch 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.
  4. Quotas that close at the right moment

    A quota of one hundred ends with one hundred responses, not one hundred and four.

    1. Everything else about quotas is presentation.

      Files src/lib/quotas.tstests/quota-race.test.ts

    2. Discarding a partial answer is a choice, so it should be one.

      Files src/lib/quotas.ts

    3. Collapsing them is how people over-collect.

      Files src/app/admin/responses/page.tsx

    done when · tick each as it passes
    watch 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.
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 forms.