Q2F8
A collection policy and receipt system that turns privacy promises into server-enforced, testable product behavior.
Project brief
- Period
- 2018
- Status
- Archived
- Focus
- Privacy as an enforced constraint
- Constraint
- Client consent controls could not be trusted to enforce server behavior.
- Result
- Collection, retention, and deletion became versioned executable policy.
Q2F8 was a 2018 personal project built to test whether a privacy promise could become enforceable system behavior. The timing mattered: GDPR became applicable in May 2018, and product interfaces were filling with consent banners whose visual choices often received more attention than the data lifecycle behind them.
Decision record
Where did the privacy promise have to hold?
- Decision
- Enforce versioned collection policy at server admission and carry it through retention and deletion.
- Trade-off
- Some desirable analytics never entered the system, which was an intended outcome.
A preference control was not a policy
I connected the experiment to several synthetic versions of my earlier applications. They emitted representative analytics and operational events against test accounts. No real personal data or production service was involved. The purpose was not to claim legal compliance; it was to design a system in which collection purpose, consent, schema, retention, and deletion were explicit enough to inspect and test.
The project began with a proposed event called form_hesitation. It would have recorded focus changes and timing around a sensitive form. The event promised insight and retained intimate behavioral detail without a named decision proportionate to that detail.
I chose not to implement it. That absence clarified the project more than another settings screen would have. Q2F8 should not make all collection permissible after a click. It should require every retained event to justify its purpose and lifecycle before the application could send it.
J05N had centralized intended interface behavior. Q2F8 raised the challenge: the server, storage, and background jobs had to uphold a policy even if an old browser bundle or careless integration did not.
I wrote a narrow contract:
- Every admitted event had a registered purpose, category, closed property schema, owner label, retention period, and deletion behavior.
- Grant, withdrawal, and policy acknowledgement created immutable versioned receipts.
- Client code avoided disallowed collection, while the server remained authoritative.
- Unknown events and extra properties were rejected rather than stored optimistically.
- Retention and account deletion produced operational evidence.
- The audit view could explain what was collected, why, under which receipt and policy, and until when.
- Choosing not to collect remained a valid and visible design outcome.
The system could enforce declared rules. It could not decide whether every purpose was ethical, whether a legal basis was correct, or whether an aggregate was harmless. Those judgments remained outside the software and were represented as required review inputs rather than automated conclusions.
That limitation kept the project from using architecture as legal theatre.
An event catalogue instead of folklore
Every allowed event received a versioned catalogue definition:
name
version
purpose
category
propertySchema
sensitivityNotes
retentionPolicy
deletionPolicy
introducedUnderPolicy
ownerLabelThe purpose field required a specific decision or operational need. “Improve the experience,” “analytics,” and “might be useful” were rejected as descriptions. A valid purpose named what would be decided and why a less revealing signal was insufficient.
Property schemas were closed by default. An early definition accepted an open metadata object for experimentation. Synthetic email addresses, free-form notes, and whole error payloads immediately appeared under inconsistent keys. The flexibility had turned every integration into an undocumented schema extension.
After closing the schemas, a new property received the same review as a new event. One revealing field can change the privacy character of otherwise ordinary telemetry.
Definitions were immutable once used. A changed meaning created a new version and migration decision. Reusing one event name for a new purpose would have made old consent and retention evidence ambiguous.
The catalogue generated TypeScript types for test integrations and runtime validators for the collection endpoint. Generated client helpers improved ergonomics without becoming the policy authority.
The event I did not collect
The hesitation proposal was broken down into the questions it hoped to answer. Was the form confusing? Did a validation rule block progress? Did people abandon one step more often than another?
I replaced the detailed behavior stream with narrower events:
- Step displayed.
- Validation rule triggered, without entered value.
- Continue or save action.
- Aggregate time between named steps.
- Explicit request for help.
Qualitative testing supplied explanation that telemetry could not. The replacement reduced analytical flexibility and reduced retained intimacy. Q2F8 recorded that tradeoff in the catalogue decision note.
I added a “rejected or avoided event” log to the planning interface. It captured the proposed signal, decision it was meant to support, rejected risks, and chosen alternative. This prevented non-collection from disappearing as if no design work had happened.
The log also kept later developers—still only me in this personal project—from reintroducing the same invasive idea under another name. An absence can be a maintained architectural decision.
Consent as immutable receipts
Grant, withdrawal, and policy acknowledgement were stored as append-only receipts. A receipt identified the test subject, categories, policy version, source interface, effective time, and the version of the preference text shown.
Corrections created a new receipt rather than editing history. A derived current-consent projection supported fast checks, while the immutable sequence remained the explanation source.
The settings interface used plain product language and symmetrical paths. Withdrawing a category did not require more navigation or persuasion than granting it. The current state named what would stop, what operational data remained necessary, and what deletion process would follow for already retained records.
The receipt did not contain a copy of every rendered page. It referenced versioned policy and interface text stored separately. A digest connected the exact text version to the receipt so later edits could not rewrite what had been presented.
Receipt retention followed its own declared policy. An append-only ledger was not permission to retain identity forever. Where a receipt needed to outlive collected events, that duration and reason were explicit.
I treated the receipt as evidence of an interaction, not proof that the person fully understood every consequence. The interface still carried a responsibility to communicate clearly.
The server-side collection gate
Client libraries checked the current preference before making a request. That reduced unnecessary transmission and supported a responsive interface. It was not sufficient enforcement.
The collection endpoint evaluated each event against:
- A known catalogue name and version.
- The submitted property schema.
- The current effective receipt for the event category.
- The originating application and allowed environment.
- The event time relative to withdrawal and policy change.
- Request authenticity and replay limits.
Unknown events were rejected. Extra properties were not silently retained in a raw envelope. The response returned a stable rejection reason that appeared in the integration dashboard.
Accepted records received an admission receipt containing the catalogue version, consent receipt reference, admitting policy version, retention deadline, and collection time. This made later explanation possible without rerunning present-day policy against historical data.
The gate was the project’s main breakthrough. A stale bundle, missed conditional, or integration regression could no longer turn a client convention into silent collection. The system’s authoritative boundary enforced the promise.
Essential operational events followed a separate, narrow catalogue. Calling an event essential did not exempt it from purpose, schema, sensitivity, retention, and deletion review. The category controlled consent behavior, not accountability.
Withdrawal exposed temporal edge cases. A browser could queue an event under a granted state, lose its connection, and deliver it after withdrawal. Another tab could retain an old projection. Server and client clocks could differ.
The server used receipt order and its own admission time rather than trusting a client’s statement that consent had existed. Events received after withdrawal were rejected for consent-dependent categories, even if their client timestamp was earlier.
This sacrificed some possibly legitimate offline analytics and upheld the clearer promise: once withdrawal became effective, the collection gate stopped admitting new dependent data.
The client cleared queued consent-dependent events when it learned of withdrawal. It kept only a local count for the user-facing explanation that pending analytics had been discarded. It did not transmit the discarded contents for diagnosis.
Preference changes broadcast across open tabs, but every tab still treated the server receipt as authoritative. If a tab could not confirm the new receipt, it defaulted to non-collection for optional categories.
Policy-version changes followed a similar conservative path. A new purpose or materially changed policy did not inherit old consent automatically. The catalogue could declare when a fresh acknowledgement was required.
Retention as executable behavior
Every admitted record carried a retention class and computed deadline. Scheduled jobs selected expired records by policy and deleted them in bounded batches. The job emitted counts, age of the oldest remaining expired record, failures, and a completion receipt.
The first design stored retention as documentation beside the event. Nothing connected the number to deletion. Moving the deadline into admitted record metadata and building the job made the promise operational.
Where irreversible aggregation was justified, raw records were removed after aggregation. The aggregate schema received its own review for re-identification risk. “Aggregate” was not treated as a magic privacy label.
Test accounts exercised the full path with shortened retention windows. A canary created a known record, verified admission, waited for its deadline, and confirmed that both query and storage paths no longer returned it. Failure surfaced before a production-like deadline accumulated months of debt.
Deletion receipts did not store the deleted payload. They recorded policy, affected partition, count, execution identity, and outcome. Evidence that deletion ran should not recreate the data it removed.
The job paused when its selection query and the catalogue disagreed about policy version. Erasure was incorrect if it stranded eligible data or crossed beyond the declared account scope. An unknown policy state required intervention.
Account erasure as a workflow
The first withdrawal implementation stopped future events and did nothing to retained account-linked data. That gap forced me to separate preference withdrawal, retention expiry, and account erasure.
An erasure request created a durable workflow with discovered data domains, deletion steps, exceptions, and receipts. Each registered store implemented a discovery and deletion contract. The workflow could report complete, partially complete with a named exception, or failed and retryable.
I built synthetic stores for event data, saved publication drafts, preview accounts, and audit records. The purpose was to test orchestration across different deletion semantics, not to claim a complete real system.
The workflow used stable step identities. Repeating a timed-out deletion call returned the earlier receipt where the store supported idempotency. A store with an unknown outcome paused for verification rather than issuing a potentially destructive second request blindly.
This extended X8B6’s safe-retry pattern into policy work. Privacy promises frequently depend on background jobs; those jobs need the same recovery design as visible product actions.
The user-facing status avoided saying “deleted” after only queuing work. It named the request time, domains completed, any retained exception and reason, and the next retry or review state.
The audit surface
The first audit page used legal and database vocabulary. It was accurate enough for me and difficult to use as a product-design tool. I reorganized it around questions:
- What event exists?
- What specific decision is it meant to support?
- Which properties can it contain?
- What admits or rejects it?
- How long does it remain?
- How is it deleted?
- Which policy and tests provide evidence?
The view linked a catalogue definition to recent synthetic admission and rejection counts, retention canary status, schema violations, and the latest deletion receipt. It did not expose real payloads by default.
A search before adding a new event helped prevent duplicates. Similar purposes appeared together even when names differed. A proposed definition could show that an existing aggregate already answered the stated question.
The audit surface also displayed events with no recent consumers and decisions with no event use. Removal became a first-class maintenance path. A data pipeline should be able to shrink when a signal no longer supports an active decision.
Tests generated from policy
The catalogue generated positive and negative contract tests. Each event had a valid minimal payload, invalid extra properties, incorrect types, absent consent, withdrawn consent, wrong origin, unknown version, and expired policy cases.
Property-based tests produced payload variations near schema boundaries. Integration tests ran generated clients against the real collection gate. A change to the catalogue could not pass by updating TypeScript declarations while leaving server validation stale.
Retention tests used controllable time and isolated partitions. Erasure tests interrupted each step and retried. Receipt tests verified that accepted records referenced the policy and consent versions used at admission.
The system also tested non-collection. With a category disabled, the browser emitted no request and the endpoint rejected a manually crafted one. This dual assertion protected both data minimization and authority.
I added a policy-diff report. A changed definition showed new properties, broader purpose, longer retention, category movement, and deletion changes. Review could focus on meaning rather than reading two JSON documents.
The first prototype sent accepted events directly into one broad table. Access control then had to distinguish every purpose at query time, and a debugging export could accidentally combine categories that the collection interface had presented separately.
I partitioned storage by policy class and kept a small admission envelope common across them. The event payload lived only in the store appropriate to its declared use. Query tools requested a named purpose and received only fields registered for that purpose. Unbounded raw export stayed outside the project’s routine capability set.
The collection service and audit interface used separate credentials. The collector could append admitted events and could not browse historical payloads. Retention workers could select and delete by policy metadata without receiving unrestricted application access. Erasure orchestration invoked narrow store adapters.
Logs recorded event name, catalogue version, admission result, and reason code while excluding payload values by default. A schema rejection should not leak the exact disallowed field into a broadly retained log. Controlled debugging could inspect a synthetic reproduction rather than preserving an unexpected real value.
Backup policy followed retention. A deletion promise that ignored backups would be incomplete, but immediately rewriting every archive was not always practical. The experiment used encrypted, expiring backups with a restoration procedure that reapplied deletion tombstones before a restored service could become active. The audit surface showed the maximum backup persistence window.
This did not create perfect deletion from every hypothetical medium. It made the recovery path part of the lifecycle and prevented backup restoration from silently resurrecting erased test accounts.
I also modeled a policy-gate outage. Optional collection failed closed: applications continued their primary work without telemetry. Essential operational events used a narrowly cached current catalogue and a short queue with an expiry. The project would not make its actual features depend on the availability of analytics admission.
These boundaries connected privacy to reliability and security. Minimization reduced what could leak. Narrow credentials limited what one compromised component could reach. Fail-closed behavior kept an enforcement outage from becoming permission to collect first and decide later.
Reviewing a catalogue change
Even as a one-person project, I separated proposing a definition from activating it. The review screen asked for the supported decision, less revealing alternatives considered, category, exact properties, retention, deletion, and a test plan.
A proposal built a preview catalogue and generated contract tests without making the event admissible. Activation created a new immutable catalogue version. Existing clients referencing an older compatible version continued under its rules; incompatible or retired definitions received an explicit rejection.
Broadening purpose, adding a property, extending retention, or moving into a less restrictive category could not masquerade as a patch release. The diff labeled each change by privacy consequence and required a fresh decision note.
Retirement occurred in stages. New admission stopped first. Existing retained data followed its original or a shorter retention rule. Generated clients marked the event deprecated. The audit surface kept the historical definition as long as admission and deletion receipts referenced it.
This process was intentionally slower than adding an arbitrary tracking call. Friction was the mechanism by which the project forced data appetite into an explicit product decision. The challenge was to make that friction specific and informative rather than bureaucratic.
The same pattern later reappeared in V0M3’s tool permissions and N17Q’s agent policies. Consequential capability should move through a versioned proposal, review, activation, and retirement path. A configuration file can be product state when it defines what the system is allowed to do.
Policies that were only labels
The open metadata object was the first mistake. It turned a typed event name into a container for unreviewed data. Closed schemas became the default.
The first withdrawal path stopped future requests and ignored retained data. Linking every event to retention and erasure behavior corrected the incomplete lifecycle.
I originally trusted client-side gating as the implementation of consent. The server policy gate made enforcement resilient to old bundles and mistakes.
The first audit interface optimized for legal terminology and storage structure. Organizing it around event, purpose, schema, admission, retention, and deletion made it useful before collection, when decisions could still change.
I also treated aggregation as automatically safe. Reviewing aggregate shape and minimum group size exposed where a supposedly anonymous output still described a very small population.
The database became accountable to an absence
Q2F8 took the contract discipline from J05N and extended it through server admission, storage, scheduled work, deletion, and evidence. The interface was only one point in a policy lifecycle.
It added a new kind of difficulty to the personal-project sequence: the system had to constrain itself. Success was not measured by how much data it could collect or how flexible the schema became. It was measured by whether declared limits survived stale clients, retries, time, and maintenance.
The strongest architectural idea was the server-side policy gate tied to immutable receipts. The strongest product idea was that non-collection could be an intentional, documented result.
W93H, the next project, would reuse the catalogue and receipt instincts for operational evidence. Instead of explaining why an event was allowed to exist, it would explain what a distributed system had done during a failure—while keeping observation, action, and hypothesis distinct.
What policy could not infer
Q2F8 was a personal technical exploration using synthetic accounts and events. It did not certify GDPR compliance or replace legal, security, accessibility, or ethical review. Declared purpose can still be vague, aggregates can still reveal people, and audit evidence can still be interpreted badly.
The system made rules executable and inspectable. It could prove that a named policy gate admitted or rejected a test event and that a deletion workflow ran. It could not automate whether the surrounding human policy was just.
Technology: TypeScript catalogue definitions, generated runtime schemas, append-only consent receipts, a server-side collection gate, admission receipts, scheduled retention jobs, idempotent erasure steps, policy-diff reports, and a synthetic audit environment.