A marketing_opt_in column stores the current answer and nothing else, and the question that gets asked is always “when, and to what exactly did they agree”.
CREATE TABLE consents (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
purpose VARCHAR(64) NOT NULL, -- 'marketing_email', 'analytics'
granted TINYINT(1) NOT NULL,
policy_version VARCHAR(16) NOT NULL,
source VARCHAR(64) NOT NULL, -- 'signup_form', 'preferences'
recorded_at DATETIME NOT NULL,
INDEX (user_id, purpose, recorded_at)
);
Append-only is the point: a withdrawal is a new row rather than an update, so the history survives. The policy_version column is what lets you answer whether a given consent still covers what you are doing now, which is the question that arrives after a wording change. Reading the current state is a query for the latest row per purpose, which is exactly what ROW_NUMBER is for.