A tiny self-hosted survey: one page, PHP + SQLite, no framework. Define questions in JSON, collect answers, glance at charts. Languages are whatever config/lang/ui.<code>.json files you ship.
https://survey.schick-software.de/demo
- PHP 8+ with PDO SQLite
- A web server that can write to
data/
Point the document root at this folder (or drop it into an existing PHP host).
- Deploy these files to your server.
- Ensure
data/is writable (SQLite lives there;.htaccessblocks direct access). - Open
index.phpto take the survey.
The charts page is not active by default and must not be left open on the internet.
- Restrict
analysis/(HTTP auth, IP allowlist, VPN, server ACL — your choice). - Rename
analysis/index.php.please-protect→analysis/index.php. - Open
/analysis/for the summary.
Until you rename the file, the analysis UI will not run.
UI strings live in config/lang/ui.<lang>.json (e.g. ui.de.json). Add or remove a language by adding or deleting that file — the switcher picks them up automatically (2-letter codes).
- Switcher in the header (hidden if only one file exists), or
?lang=de(cookie) - Falls back to
Accept-Language, thenconfig/brand.json→lang, thenenif present - App title and slogan are keys in those UI files
- Question text uses the same locale codes in
config/questions.jsonmaps
Edit config/questions.json, then reload.
Each survey needs a stable id (lowercase letters, digits, _, -) and a questions array:
{
"id": "demo",
"questions": [
{ "id": "q1", "type": "text", "label": { "en": "…" } }
]
}The survey id is stored on every row in SQLite (survey_id on questions, responses, answers), so one database can hold multiple surveys and files can be merged later. The browser stores the last response id per survey under plainsurvey.<id>.id in localStorage (theme stays global).
| Type | Notes |
|---|---|
text |
Free text |
choice |
Single choice — options need stable id + label map |
multi |
Multiple choice — same option shape |
rating |
scale: stars, emoji, or number; optional min / max (e.g. 0–10) |
Answers store option ids (not translated labels), so charts stay correct across languages.
Updating wording: change labels/descriptions in questions.json — synced on the next request, answers stay.
Adding, removing, or reshaping questions (ids, types, options): delete data/survey.sqlite so the DB re-seeds (wipes all surveys in that file). Alternatively, change the survey id in JSON to start a fresh namespace in the same database without touching older rows.
If you still have an older SQLite file from before
survey_id, deletedata/survey.sqliteonce and let it re-seed.
Edit config/brand.json (see brand.sample-*.json). Font, light/dark colors and logos, optional default lang. Title and slogan are in the language files.
A footer link (“Powered by PlainSurvey”) points at the project repo by default. Configure it with poweredBy in brand.json:
"poweredBy": true"poweredBy": false"poweredBy": {
"enabled": true,
"url": "https://github.com/fschick/plainsurvey"
}Label text comes from powered_by in config/lang/ui.*.json.
- Opening the survey creates a short personal id and link (
?id=…). - Share or bookmark that link to change answers later — open it, edit, submit again.
- The browser remembers the last id per survey (localStorage key
plainsurvey.<survey-id>.id). Use Start over /?new=1for a fresh response.
data/survey.sqlite holds everything. Filter by survey_id (see config/questions.json). Sample queries for counts, ratings, and free-text lists are in analysis/analysis.sample.sql:
sqlite3 data/survey.sqlite < analysis/analysis.sample.sqlOr open the DB in any SQLite client and run queries from that file.
index.php Survey form
assets/css/style.css Styles
assets/js/survey.js Front-end behaviour
analysis/index.php.please-protect Answer charts (enable after locking the folder)
analysis/analysis.sample.sql Sample SQL
config/ questions, brand, languages
src/ PHP helpers (db, i18n)
data/ SQLite (not public)
fonts/ Optional local webfonts
Almost all parts of this project were created with a human-controlled AI assistant.