The form was not merely a UI problem
The visible symptom was an event registration form that felt crowded, inconsistent, and difficult to trust. Required fields were not obvious. Explanations competed with inputs. Sensitive and ordinary questions looked identical. The page used a visual language that did not belong to the rest of the product.
It would have been easy to treat this as a CSS task. That would have improved the screenshot while leaving the dangerous part untouched.
An event form is a boundary where a person gives information to an organization. The interface decides what the person understands, the API decides what is accepted, the database decides how long the answer survives, and operational systems decide who can later read it. A trustworthy design therefore has to connect UX, validation, authorization, encryption, retention, and deletion.
The real goal was not to make the form look cleaner. It was to make every collected value necessary, understandable, bounded, and disposable.
This first article explains how I turned that goal into an engineering plan. The remaining articles follow the same data into the session boundary, encrypted storage, and a controlled production rollout.
Concepts in plain language
- Dynamic form: a form whose questions come from the backend instead of being permanently coded into the page.
- Data minimization: asking only for information that the event genuinely needs.
- Authorization: checking not only that someone is logged in, but also that they may act on this specific event or registration.
- Idempotency: making a repeated submission return the original result instead of creating a duplicate registration.
- Retention: the period for which an answer is allowed to remain stored before cleanup removes it.
Step 1: map the complete data journey
I started by drawing the journey of one answer rather than opening the component file.
Person
-> event page
-> registration form
-> same-origin application endpoint
-> authenticated API operation
-> validation and authorization
-> encrypted persistence
-> authorized operational view
-> retention cleanupEach arrow creates a different failure mode:
- The page can ask for more information than the event needs.
- The browser can retain credentials or submitted values longer than expected.
- A request can be forged, replayed, oversized, or structurally abusive.
- A valid user can submit an answer for the wrong event or participation.
- A database reader can see plaintext answers.
- A cleanup job can remove the visible value but leave encrypted metadata or revision history behind.
This map changed the definition of “done.” A polished component was only one part of the result.
Step 2: write the data contract before redesigning the card
Dynamic forms are especially risky because the frontend does not know the questions at build time. A form author can add a field later, so privacy cannot depend on a developer remembering to update a hard-coded screen.
I made the public form projection explicit. Every field needed a bounded contract such as:
fieldKey
label
fieldType
isRequired
dataClassification
purposeText
consentRequired
retentionDays
validationRule
allowedOptions
sortOrderThe important fields are not only technical. purposeText tells the person why the answer is requested. retentionDays makes deletion enforceable. dataClassification lets the interface and operational tools distinguish ordinary coordination data from higher-risk data. consentRequired prevents a sensitive answer from being interpreted as consent merely because it was submitted.
This follows the principle of data minimization: collect only what is adequate, relevant, and necessary for the stated purpose.
Questions I used for every field
- Does the event genuinely need this answer?
- Can the purpose be explained in one direct sentence?
- Is a less identifying answer sufficient?
- Who must read it, and at which point in the workflow?
- What is the shortest useful retention period?
- What should happen if the person refuses?
- Would free text create avoidable risk compared with a bounded selection?
If those questions did not have good answers, the field did not belong in the form.
Step 3: make the privacy hierarchy visible
The new design used the same spacing, typography, surface colors, border rhythm, and interaction states as the rest of the site. Consistency matters because an unfamiliar visual system on a data-entry screen can make a legitimate page feel untrustworthy.
The content hierarchy became:
- Event identity and participation mode.
- A short explanation of why registration is needed.
- The minimum set of questions.
- Purpose and retention details next to the relevant question.
- Consent control where consent is actually required.
- A final submission summary and one clear action.
Required fields received a visible asterisk and a text legend. The asterisk was not the only signal: the input also used the native required contract where appropriate, accessible description relationships, and a field-level error message. Optional fields were labelled “optional” instead of leaving the person to infer the rule.
Why the asterisk still matters
An asterisk is compact and familiar, but by itself it is ambiguous and inaccessible. The complete pattern was:
*next to the visible label;- a nearby “Fields marked * are required” explanation;
- semantic required state for assistive technology;
- error text connected to the input;
- focus moved to the first invalid field after submission;
- no reliance on color alone.
The design also stopped treating every explanation as a warning. Normal purpose information used quiet supporting text. Sensitive classification, consent, or an unusually long retention period received stronger emphasis. This reduced noise while preserving the signals that matter.
Step 4: use progressive disclosure, not hidden policy
The original form tried to display every rule at once. The result was technically complete but practically unreadable.
I separated information into three layers:
- Always visible: label, required state, short purpose, and the control.
- Visible when relevant: consent language, retention period, or sensitive-data notice.
- Expandable detail: longer event policy and operational explanations.
Progressive disclosure must not hide information required for meaningful consent. It should reduce repetition, not conceal consequences. A person should understand what will be submitted before pressing the action button.
Step 5: keep client validation helpful and server validation authoritative
Client validation exists for fast feedback. It is not a security boundary. Requests can be created without the UI, browser code can be modified, and old clients can outlive a form change.
The server therefore enforced limits independently:
- maximum number of responses;
- maximum total JSON body size;
- maximum nesting depth;
- maximum text, email, and phone lengths;
- finite number ranges;
- exact option membership for selection fields;
- field applicability for the selected participation path;
- required answer presence after normalization;
- explicit consent for consent-gated fields;
- current form version and event lifecycle state.
The service canonicalized values before hashing, comparison, or persistence. Whitespace-only required answers became empty. Selection values had to match a configured option rather than merely resemble one. Unexpected keys were rejected or discarded at a clearly defined boundary.
Browser validation: improve the correction experience
Server validation: protect the domain and storage boundary
Database constraints: prevent impossible persistence statesAll three layers are useful because they defend different assumptions.
Step 6: bind every answer to an authorized owner
An authenticated request is not automatically an authorized request. A user may possess a valid session and still attempt to submit against another event, another ticket tier, or another participation identifier.
The write flow therefore resolved and locked the complete scope:
- Authenticate the session.
- Load the event through its public identifier.
- Verify that the event is open for the requested participation mode.
- Resolve the current form version and applicable fields.
- Verify ownership of any hold, reservation, or participation.
- Validate answers against that resolved form.
- Write the participation and answers in one transaction.
- Emit only non-sensitive operational events after commit.
This is where privacy and consistency meet. An answer written to the wrong owner is both an authorization failure and a confidentiality failure.
Step 7: make retries safe without duplicating registrations
Mobile networks retry. People double-click. Proxies time out after the server commits. A registration endpoint must expect the same logical command more than once.
The request carried an idempotency key bound to the authenticated actor and normalized payload. Replaying the same command returned the existing result. Reusing the key with a different payload produced a conflict instead of silently changing meaning.
The key itself was not permission. Authorization and lifecycle checks still ran at the correct point. Idempotency prevented duplicate side effects; it did not bypass policy.
Step 8: design deletion at the same time as collection
Retention was not left as a future cleanup ticket. Each answer had a policy target, and cleanup handled all representations of that answer:
- legacy plaintext columns;
- encrypted ciphertext;
- initialization vector and authentication tag;
- encrypted data key metadata;
- amendment and revision history;
- derived notification or audit payloads where applicable.
Audit records kept the fact that an administrative action happened, but not the answer content. Operational logs used identifiers, counts, classifications, and stable error codes rather than payloads.
Deleting only the primary value while retaining copies in revisions, logs, or dead-letter messages would have created privacy theatre rather than privacy engineering.
Step 9: test the negative paths
The most valuable tests were not the happy-path screenshot tests. They proved that the boundary rejected dangerous states:
- missing applicable required field;
- empty required selection;
- selection outside the configured options;
- response count above the limit;
- oversized or deeply nested JSON;
- excessive email, phone, text, or number values;
- consent-required answer without explicit consent;
- form field from another participation context;
- stale form version;
- repeated request with conflicting idempotency payload;
- unauthorized owner or event scope;
- cleanup rerun after data was already anonymized.
The UI was tested too: keyboard order, visible focus, screen-reader labels, mobile layout, error summary, and consistency with the surrounding event page.
What the redesign actually achieved
The final form looked calmer, but the larger improvement was structural:
- people could see why each answer was requested;
- required state and errors were unambiguous;
- dynamic fields remained bounded by a server-owned contract;
- authorization connected answers to the correct event participation;
- retries did not create duplicate registrations;
- retention and cleanup were part of the original design;
- the next security layers could operate on a precise data model.
The next article follows the request before it reaches this validation boundary: Removing Access Tokens from Browser JavaScript with an HttpOnly BFF.
This article intentionally omits organization names, domains, credentials, infrastructure addresses, and identifying production details. The architecture and lessons remain real.