How it is built
Architecture overview
An experience-management platform lets an organisation ask people questions at scale and turn the answers into decisions. It builds a questionnaire (customer survey, employee engagement pulse, product feedback widget), delivers it by email, link or in-app prompt to a defined audience, collects responses, and then reports: counts and averages by segment, trends over time, which topics drive the score, and which individual responses need action. Qualtrics, Medallia and Culture Amp are the known commercial products; LimeSurvey, Formbricks, KoboToolbox and ODK are open-source relatives.
Three ideas make it work. First, the questionnaire is data, not code: a declarative definition (questions, branching, validation, randomisation) is stored, versioned and interpreted by one engine on the server and on the client, so the same survey runs on web, mobile and in an email. Second, response data is append-only and analysed by scanning, so the store is columnar and pre-aggregated by time rather than a transactional row store. Third, the statistics are part of the product: a raw mean of whoever answered is misleading, so weighting, confidence intervals and significance tests sit between the data and the dashboard. A fourth, quieter concern is trust: respondents are promised anonymity, so reports must not allow a small group to be singled out.
Parts:
- Questionnaire engine — definition schema, branching expressions, validation, randomisation and sampling — JSON Schema validation, Pratt parsing, Recursive-descent parsing, Deterministic bucketing,
randomization-blocked-stratified(no page yet),hash-based-sampling(no page yet),conjoint-choice-design(no page yet),csprng(no page yet) - Distribution — email and link delivery, bounces, tracking, rate control —
email-delivery-smtp(no page yet),smtp-protocol(no page yet), SPF, DKIM and DMARC sender authentication, DNS message format and client lookups, Durable job queue, Exponential backoff with jitter, Token bucket,bounce-processing(no page yet),mime-parsing(no page yet),open-and-click-tracking(no page yet), Linear-time regular expression matching - Identity, tenancy and access — single sign-on, user provisioning, who sees which results —
saml(no page yet),scim-provisioning(no page yet), XML Streaming Parser, Replay protection window, Authorization models (RBAC, ABAC, ReBAC), Row-level security (multi-tenant isolation in the data layer),closure-table(no page yet), TLS 1.3, Mutual TLS,x509-certificates(no page yet),rsa-cryptosystem(no page yet) - Response store and aggregation — append-only facts, fast scans, time buckets — Columnar storage, Vectorized execution, Rollup aggregation, Time partitioning, Delta encoding, Bit packing and PFOR, Varint (LEB128), B+ tree,
dictionary-encoding(no page yet),zone-maps(no page yet),hash-aggregation(no page yet),connection-pooling(no page yet) - Statistics — weighting, intervals, tests, models of drivers —
survey-weighting-raking(no page yet), Hypothesis testing,bootstrap-confidence-intervals(no page yet),wilson-score-interval(no page yet), Generalized linear models (GLMs), Linear and logistic regression, Robust statistics, Newton root finding, LU and Cholesky factorization, L-BFGS, Monte Carlo integration,markov-chain-monte-carlo(no page yet) - Text and driver analytics — classify comments, rank drivers, explain scores — Text classification, Transformer block, Byte-pair encoding (BPE) tokenization, Calibration,
embedding-inference(no page yet), Text normalization, Aho-Corasick multi-pattern matching, Gradient-boosted decision trees (GBDT),shapley-values-treeshap(no page yet), AdamW, Reverse-mode automatic differentiation,rejection-sampling(no page yet) - Privacy — thresholds before showing a slice —
k-anonymity(no page yet), Row-level security (multi-tenant isolation in the data layer) - Durability and recovery — keep survey definitions and responses — Point-in-time recovery, ARIES recovery, Primary-backup replication, Write-ahead log, Object storage
- Operation and test — Process supervision, Graceful draining, Deterministic simulation testing, Coverage-guided fuzzing,
hdr-histogram(no page yet)
admin ─► survey designer ─► definition (JSON, versioned) ─► validation + publish
│
audience (CRM, HRIS via SCIM) ─► sampler (hash / blocked) ─► invitations ─► durable queue ─► SMTP workers
▲ bounces, complaints, unsubscribes
respondent ◄── link / email / in-app ── survey runtime (branching, quotas, randomisation)
│ answers (append-only events, keyed by response id)
▼
ingest ─► row store (responses, definitions) ──► columnar facts ──► rollups by time / segment
│ │
weights (raking) + intervals ◄┘ ▼
text model (themes, sentiment) ──► dashboards, alerts, case routing
access control + minimum group size ──► what a viewer may see
How the parts fit together
Questionnaire engine: a definition and an interpreter
A survey definition lists pages, questions with types and constraints, and rules for what appears next. SurveyJS describes its library as rendering JSON-driven forms and surveys (source: SurveyJS repository), which is the common shape: a document that a runtime interprets. The document has a schema, and a JSON Schema validation step at publish time rejects malformed definitions before a respondent finds them. Branching and piping use small expressions ("Q3 > 7 and region = 'EU'"); a Pratt parsing or Recursive-descent parsing parser compiles them once at publish time, so the runtime evaluates a tree, not a string. Parsers are the one place where a survey author's input becomes code paths, so they are fuzzed (Coverage-guided fuzzing).
Assignment of respondents to variants (randomised question order, A/B wording, holdout groups) must be reproducible: the same person opening the link twice sees the same version. Deterministic bucketing does this by hashing the respondent id with a per-experiment salt. Experimental designs that need balance across strata (blocked or stratified randomisation) are randomization-blocked-stratified (no page yet), and conjoint studies need balanced sets of profiles (conjoint-choice-design (no page yet)). hash-based-sampling (no page yet) picks a fixed share of an audience so that all events of one person fall in or out together.
Distribution: email is the hard part
Most responses arrive from an email link, so deliverability is an engineering problem. Mail is sent by queue workers (email-delivery-smtp (no page yet) over the SMTP session of RFC 5321, smtp-protocol (no page yet)) pulling from a Durable job queue with Exponential backoff with jitter for temporary failures and a Token bucket per sending domain, because receiving servers throttle senders. The sender proves it is authorised with SPF, DKIM and DMARC sender authentication (RFC 7208, RFC 6376, RFC 7489), all of which are DNS lookups (DNS message format and client lookups).
Failures come back as delivery status notifications (RFC 3464) or feedback-loop complaints; bounce-processing (no page yet) classifies them (permanent, temporary, spam complaint) with pattern matching (Linear-time regular expression matching) over messages parsed by mime-parsing (no page yet), and suppresses the address after a hard bounce. List-unsubscribe handling follows RFC 8058. open-and-click-tracking (no page yet) records engagement through signed redirect links; an image fetch is not proof that a person read the message, so analyses should prefer clicks and completions as the engagement signal.
Identity, tenancy and access: one platform, many companies
Enterprise customers connect their own identity provider. saml (no page yet) (the OASIS SAML 2.0 specifications) delivers a signed XML assertion; the platform must parse it safely with a streaming parser (XML Streaming Parser), verify the signature with the provider's certificate (x509-certificates (no page yet), rsa-cryptosystem (no page yet)), and reject replayed assertions (Replay protection window). scim-provisioning (no page yet) (RFC 7643 and RFC 7644) keeps users and groups in sync: create, patch and deprovision calls from the customer's directory, validated against schemas (JSON Schema validation).
Data from different customers sits in shared tables, so isolation is enforced in the data layer: Row-level security (multi-tenant isolation in the data layer) appends a policy predicate to every query. Within a customer, Authorization models (RBAC, ABAC, ReBAC) decide who sees which results; organisational hierarchies ("a manager sees their team and below") are trees stored with a closure-table (no page yet). Traffic between services is protected by TLS 1.3 and, internally, Mutual TLS.
Response store and aggregation: scan, do not look up
Responses are written once and read in aggregate: "average score by region by week". Columnar storage stores each column contiguously so a query reads only the columns it touches. Low-cardinality columns (region, question id) compress with dictionary-encoding (no page yet), timestamps and counters with Delta encoding, and small integers with Bit packing and PFOR or Varint (LEB128). zone-maps (no page yet) keep per-block minimum and maximum so a time filter skips most blocks. Vectorized execution processes batches of values per operator call. Group-by uses hash-aggregation (no page yet).
Dashboards ask for the same aggregates repeatedly, so Rollup aggregation folds raw events into one mergeable partial per time bucket and segment, and Time partitioning keeps the buckets in time-ordered blocks so old data is cold and cheap. The row-oriented side (survey definitions, invitations, users) uses a B+ tree database behind connection-pooling (no page yet), because it is small, transactional and accessed by key. Two stores means a flow between them: the ingest path writes the response durably in the row store, and a batch or stream job appends it to the columnar store with an idempotent key, so a replay cannot double count.
Statistics: the part customers pay for
A sample of respondents is not the population. If younger employees answer less, a raw mean is biased. survey-weighting-raking (no page yet) adjusts weights so that weighted sample margins match known population margins (iterative proportional fitting); the R survey package documents post-stratification, calibration and raking as supported operations (source: CRAN survey package), and its variance estimation by Taylor series or replicate weights is what turns a weighted mean into a mean with an interval. Without weights, intervals for proportions use wilson-score-interval (no page yet), and for arbitrary statistics bootstrap-confidence-intervals (no page yet) resample the data. Hypothesis testing answers "did the score change after the programme?", with the caveat that dashboards that test dozens of segments will find false positives unless the number of comparisons is controlled.
Driver analysis ("what moves satisfaction?") fits Generalized linear models (GLMs) or Linear and logistic regression with the numeric tools that implement them: Newton root finding for the iteratively reweighted fit, LU and Cholesky factorization for the normal equations, and L-BFGS when regularised. Robust statistics (median, MAD) handle outliers in free-entry numeric answers. For small groups, a Bayesian hierarchical model fitted by markov-chain-monte-carlo (no page yet) borrows strength across groups; that costs minutes, not milliseconds, so it runs in a batch job. Monte Carlo integration is the simulation tool behind intervals and forecasts. Models that rank predictors use Gradient-boosted decision trees (GBDT), and shapley-values-treeshap (no page yet) turns a tree model's prediction into per-feature contributions that a business user can read.
Text analytics: themes and sentiment from comments
Open-text answers are often more valuable than scores. Text normalization cleans casing and spelling variants; Text classification assigns themes and sentiment, from keyword rules (Aho-Corasick multi-pattern matching) through linear models on vectors from a pretrained embedding model (embedding-inference (no page yet), run at serving time, no training of its own) to a fine-tuned Transformer block network, with Byte-pair encoding (BPE) tokenization handling many languages. Training uses AdamW and Reverse-mode automatic differentiation. Scores from the classifier are only useful as numbers if they are calibrated (Calibration), so "80 % negative" means what it says; this is the check to run before a model's output reaches an alert rule. rejection-sampling (no page yet) is the filter step when a generative model proposes summaries and a checker keeps the faithful ones.
Privacy: a minimum group size
Anonymous surveys promise that no one can be identified. Showing a team of two people's average reveals each person's answer to the other. k-anonymity (no page yet) sets a threshold: slices with fewer than k respondents are suppressed or merged, and two overlapping slices are also checked so that a difference of two reports cannot expose a small group. The check lives in the query layer next to row-level security, because dashboards and exports share it.
Durability and operation
Survey definitions and responses are customer data of long retention. They use Write-ahead log durability and ARIES recovery, Primary-backup replication for failover, and Point-in-time recovery with archives on Object storage. Workers are supervised (Process supervision) and drain before restart (Graceful draining). Invitation and ingest logic is tested under crashes and duplicates with Deterministic simulation testing.
hdr-histogram (no page yet) records the latency of the paths where respondents wait, loading the next question, saving an answer and the dashboard query, at fixed relative error. The 99th and 99.9th percentiles per tenant show whether a large survey launch is crowding out small customers, and they are the numbers the ingest and aggregation workers are scaled against.
Data and control flow
A survey goes out and results appear
- An administrator builds the survey. The definition is validated against the schema, its expressions are compiled, and a version is published; responses always reference the version they were collected under.
- The audience is loaded from the customer's directory (SCIM) or a file; a sampler picks recipients by hash, and the platform creates one invitation row per person with a signed, unguessable link.
- Invitations are queued. Workers send email at the allowed rate per domain, retrying temporary failures with jitter. Bounces and complaints update suppression lists.
- A respondent opens the link; the runtime serves the first page, evaluates branching as answers arrive, and saves partial progress. Each answer is an event with the response id, so saving twice is harmless.
- On completion the response is committed durably, and an idempotent job appends it to the columnar store and updates the time rollups.
- A dashboard request applies the viewer's permissions and row-level policies, reads rollups (and fresh data from the last partial bucket), applies weights and intervals, and suppresses slices below the minimum group size.
- A text job classifies new comments and routes strongly negative ones to a case queue, which sends an alert to the account owner.
An employee leaves the company
- The customer's directory sends a deprovisioning call (SCIM patch or delete).
- The platform deactivates the user, revokes sessions and removes their access to results; their historical responses stay, de-identified if the programme promised that.
Scaling
- Mail throughput and reputation. Sending is limited by what receivers accept; spread load across IPs and domains, authenticate, and ramp volume slowly.
- Response spikes. A launch email causes a burst of page loads and writes within minutes; serve the runtime statically, queue the writes, and keep the response write path to one durable insert.
- Aggregation cost. Move from scanning raw responses to rollups by time bucket and segment; add partitions and zone maps before adding hardware.
- Tenant skew. One large customer with millions of responses must not slow the others; partition by tenant, limit per-tenant query concurrency.
- Analytic models. Text models and Bayesian fits are batch work; precompute per survey and refresh on schedule.
Failure modes
| Failure | Symptom | Cause | Fix |
|---|---|---|---|
| Invitations land in spam | Response rate drops suddenly | Missing or failing SPF, DKIM or DMARC, high complaint rate | Authenticate every sending domain, honour unsubscribes, suppress bounces |
| Bias in results | Dashboard score differs from reality | Low response from some groups, no weighting | Weight to known margins, show response rates and intervals |
| Double counted responses | Totals exceed audience | Retry of the ingest job without an idempotency key | Key by response id; replay-safe jobs |
| Small-group disclosure | A manager can infer an individual's answer | No minimum group size, overlapping slices | Suppress below k, check overlapping reports, restrict exports |
| Cross-tenant data leak | A user sees another customer's data | A query path without a tenant predicate | Row-level security in the database, tests that query as each tenant |
| SSO assertion abuse | Account takeover through forged or replayed assertions | Weak XML signature handling, no replay check | Strict signature validation, reject replays, limit assertion lifetime |
| Expression bug | Respondents see wrong or no questions | Branching error or edge case in expression evaluation | Test definitions with generated paths, version definitions |
| Misleading significance | A "significant" change that vanishes | Many segments tested, small samples | Control for multiple comparisons, show intervals not only p-values |
| Miscalibrated text model | Sentiment alerts flood or stay silent | Model trained on other domains, drift in vocabulary | Calibrate on recent labelled data, monitor alert rates |
| Slow dashboards | Seconds per chart for large tenants | Scans over raw responses | Rollups, partitions, zone maps; precompute common cuts |
Real implementations
| name | what it is | repo | licence | notes |
|---|---|---|---|---|
| LimeSurvey | Open-source survey platform | https://github.com/LimeSurvey/LimeSurvey | GPL-2.0-or-later (LICENSE) | https://www.limesurvey.org/ ; docs at https://docs.limesurvey.org/ |
| Formbricks | Open-source experience-management platform ("Open Source Qualtrics Alternative") | https://github.com/formbricks/formbricks | AGPL-3.0 for most code; MIT for some packages; separate licence for ee directory (LICENSE) | https://formbricks.com/docs |
| KoboToolbox (kpi) | Server for KoboToolbox: forms, question library, sharing, exports | https://github.com/kobotoolbox/kpi | AGPL-3.0 | Forms, question library, sharing and exports via API |
| ODK Central | Server for Open Data Kit data collection | https://github.com/getodk/central | Apache-2.0 | https://docs.getodk.org/central-intro/ |
| SurveyJS Form Library | JSON-driven survey and form renderer for web frameworks | https://github.com/surveyjs/survey-library | MIT | The library, not the hosted service |
R survey package | Analysis of complex survey samples: weights, calibration, raking, variance | https://cran.r-project.org/package=survey | — | See the CRAN page for the licence |
Qualtrics, Medallia and Culture Amp are proprietary and not listed in the table.
Sources
- SurveyJS Form Library repository (JSON-driven forms and surveys): https://github.com/surveyjs/survey-library
- CRAN,
survey: Analysis of Complex Survey Samples: https://cran.r-project.org/package=survey - RFC 5321, SMTP: https://www.rfc-editor.org/rfc/rfc5321 ; RFC 3464, delivery status notifications: https://www.rfc-editor.org/rfc/rfc3464 ; RFC 5322, message format: https://www.rfc-editor.org/rfc/rfc5322
- RFC 7208 (SPF): https://www.rfc-editor.org/rfc/rfc7208 ; RFC 6376 (DKIM): https://www.rfc-editor.org/rfc/rfc6376 ; RFC 7489 (DMARC): https://www.rfc-editor.org/rfc/rfc7489 ; RFC 8058 (one-click unsubscribe): https://www.rfc-editor.org/rfc/rfc8058
- RFC 7643 and RFC 7644 (SCIM): https://www.rfc-editor.org/rfc/rfc7644
- OASIS, SAML 2.0 core: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf