Docs

KVKK consent records

@vamos-ui/react/compliance builds an auditable record from a submitted form: the notice version and the hash of its text, each consent granted or refused, the time, and the IP address or user agent only when you pass them. You implement the store.

Scope
This page describes what the code does and lists the KVKK provisions it relates to. It does not say whether a given form needs consent, what a notice must contain for your processing, or how long to keep records.

What a record holds

buildConsentRecord(input) returns a plain JSON object; recordConsent(store, input) also chains it to the store's last record and appends it. Web Crypto only, so it runs in Node 20+, edge runtimes, Deno and Bun.

FieldContentNotes
noticeid, version, SHA-256 of the exact text, url, locale, acknowledgedThe text is hashed, never stored. acknowledged is the "I have read the notice" box, when the form has one.
consents[]purpose id, granted, SHA-256 of the statement next to the box, versionA refused consent is recorded too (granted: false).
event"submission" or "withdrawal"A withdrawal is a new record, not an edit.
recordedAtISO 8601, UTCServer time of the call.
subjectidentifiers you choose (an e-mail, a customer id)Nothing by default except what you pass; contactConsentInput keeps the e-mail unless told otherwise.
contextform, page, ip, userAgentip and userAgent appear only if you pass them; the kit never reads request headers.
prevHash / hashSHA-256 of the canonical JSON; the previous record's hashEditing or deleting a record breaks the chain.
import { recordConsent } from "@vamos-ui/react/compliance";

await recordConsent(store, {
  notice: { id: "kvkk-aydinlatma", version: "2026-11-01", text: NOTICE_TEXT, url: "https://ornek.com.tr/kvkk", locale: "tr-TR" },
  noticeAcknowledged: true,
  consents: [{ purpose: { id: "marketing", statement: MARKETING_TEXT, version: "1" }, granted: false }],
  subject: { email: "deniz@ornek.com.tr" },
  form: "contact",
});

With the contact form

ContactForm already keeps the notice and the marketing consent apart: a linked notice, an "I have read it" box that is never pre-checked, and a separate optional marketing box. Give the notice a version and the form posts it in a hidden _notice field, so a visitor who opened the page before you published a new notice is recorded against the version they saw. resolveNotice accepts only versions you list.

"use server";
import { headers } from "next/headers";
import { validateContactForm, site2LabelsTr, type SiteFormState } from "@vamos-ui/react/blocks";
import { contactConsentInput, recordConsent } from "@vamos-ui/react/compliance";
import { contactFields } from "../contact-fields";
import { consentStore, NOTICES } from "@/lib/consent"; // NOTICES: current version first

export async function sendContact(prev: SiteFormState, data: FormData): Promise<SiteFormState> {
  const v = validateContactForm(contactFields, data, { privacyCheckbox: true });
  if (v.spam) return { status: "success" };
  if (!v.ok) return v.state;
  const h = await headers();
  await recordConsent(consentStore, contactConsentInput(v, {
    notices: NOTICES,
    marketing: { id: "marketing", statement: site2LabelsTr.marketingConsent },
    ip: h.get("x-forwarded-for")?.split(",")[0]?.trim(), // only if you decided to keep it
  }));
  // … send the message
  return { status: "success" };
}

// page: <ContactForm action={sendContact} privacyLink={{ href: "/kvkk", version: NOTICES[0].version }} privacyCheckbox marketingConsent />

The store

A store is { append(record) }, plus an optional last() that returns the most recent record so new ones chain to it. Keep it append-only (no update or delete permission for the application user). Under concurrent writes, take last() and append() in one transaction, or two records can share a prevHash.

import type { ConsentRecordStore } from "@vamos-ui/react/compliance";

export const consentStore: ConsentRecordStore = {
  append: (r) => db.insert(consentRecords).values({ id: r.id, recordedAt: r.recordedAt, hash: r.hash, body: r }),
  last: async () => (await db.select().from(consentRecords).orderBy(desc(consentRecords.recordedAt)).limit(1))[0]?.body ?? null,
};

memoryConsentStore() exists for tests and previews; it forgets everything on restart.

Verifying records

  • verifyConsentRecord(record) recomputes the hash.
  • verifyConsentRecord(record, { notice }) also checks that the archived notice text hashes to what the record says was shown — keep every published version of the notice text.
  • verifyConsentChain(records) checks every hash and every prevHash link, oldest first, and returns the index of the first break.

A hash chain shows that records were not changed after the fact by someone who could not rewrite the whole chain. It is not a timestamp from a third party.

Kit vs. KVKK

Law No. 6698 on the Protection of Personal Data (KVKK), and the rules made under it, as they relate to this helper:

ProvisionWhat it is aboutWhat the kit doesLeft to you
Art. 10, Communiqué on the obligation to inform (2018)Informing people at collection: controller, purposes, recipients, method and legal basis, rights. Informing is a separate act from obtaining consent.Links the notice from the form; records its version and text hash, and whether the box was ticked.Writing the notice; publishing every version; one notice per processing activity.
Art. 3 and 5 (explicit consent)Explicit consent: specific, informed, freely given. It is one processing condition among several (contract, legal obligation, legitimate interest…).Separate, unticked boxes per purpose; records granted and refused, with the statement hash.Deciding which processing relies on consent at all and which statement covers which purpose.
Withdrawal of consentConsent can be withdrawn; processing based on it stops from then on.event: "withdrawal" records.A way to withdraw, and acting on it.
Art. 7, Regulation on erasure, destruction or anonymisation (2017)Data is erased or anonymised when the purpose ends; a retention and destruction policy applies to many controllers.Nothing. Records are personal data too (e-mail, IP).Retention periods, deletion of records and of the data they refer to.
Art. 9 (amended by Law No. 7499, 2024)Transfers abroad: adequacy decisions, appropriate safeguards such as standard contracts, or limited incidental cases.Nothing. The store is yours, wherever it runs.Where records and form data are stored and processed.
Art. 11 and 13Rights of the data subject (access, correction, erasure…); controllers answer applications within 30 days.Records can be found by the subject identifiers you chose.The application channel and the answers.
Art. 12Technical and administrative security measures; breaches are notified to the Board and the people concerned.Hashes notice texts instead of storing them; IP and user agent are opt-in.Access control, encryption at rest, breach procedures.
Art. 16 (VERBİS)Registration in the data controllers' registry, with exemptions set by Board decisions.Nothing.Registration, where it applies.
Law No. 6563 and its regulation (İYS)Commercial electronic messages (SMS, calls, e-mail) need prior approval, and approvals are kept in the Message Management System (İYS).Records the marketing box as a consent; it does not register anything in İYS.Registering approvals and opt-outs in İYS when you send such messages.

The kit's Turkish strings for the notice and marketing boxes are neutral templates; see Localization.