Workflow

Legal Acceptance

How Terms of Service, Privacy Policy, and Data Processing Agreement acceptance flow through signup, re-acceptance on version bumps, and the append-only audit trail that satisfies GDPR Art. 15 and ISO 27001 A.5.28.

Owner: Security TeamLast reviewed: 2026-04-18

Three documents, one mechanism

Every OneComply user accepts three legal documents on first signup:

  • Terms of Service (/terms) — the contract for using the product.
  • Privacy Policy (/privacy) — how we handle personal data.
  • Data Processing Agreement (/dpa) — GDPR Art. 28 processor-controller agreement.

Mechanically, the three are treated as a single bundle. The signup checkbox covers all three at once; the TermsAcceptance audit row snapshots all three version strings in the same record. If any document changes, every user must re-accept the bundle — we never expose "accept Privacy only" UI because the compliance evidence for regulated customers is cleaner when acceptance is atomic.

Version strings and how they work

Each document has a plain-string version in src/lib/terms-versions.ts:

export const TERMS_VERSION = "2026-03-25";
export const DPA_VERSION = "2026-03-25";
export const PRIVACY_VERSION = "2026-03-25";

The string is the "Last updated"date on the public legal page. When the page changes, bump the matching constant. On every subsequent dashboard request the user's most-recent TermsAcceptance row is compared to these constants; any mismatch triggers the blocking re-acceptance modal.

Version comparison is plain equality — not date-ordering. A rolled- back legal change bumps the version forward (to a new date), never to an older one. We never want the re-acceptance gate to race against clock skew or DB write ordering.

Legal text is pending lawyer review

The default text on /terms, /privacy, and /dpa is placeholder suitable for development and design- partner testing. Before public launch, these documents need review and rewrite by an EU SaaS lawyer familiar with CSSF / DORA contexts. The mechanism — versioned, click-through, append-only, re-prompts on drift — is production-grade regardless of what text fills the pages.

The four flows that record acceptance

1. Signup

Required checkbox on /auth/signup. On first dashboard request after email confirmation, requireOrgContext creates the User row and immediately writes a TermsAcceptance snapshot against the current versions. IP and User-Agent are not populated at this step (they would require threading the NextRequest through to the helper); subsequent re-acceptances capture them.

2. Re-acceptance on version drift

Dashboard layout calls getTermsAcceptanceStatus(userId) on every request. If any document version on the latest acceptance row is out of date, it renders the blocking TermsReacceptanceModal — fixed inset, z-70, backdrop-blur, no escape except "Sign out" or "Accept".

Acceptance POSTs to /api/auth/accept-terms, which writes a fresh TermsAcceptance row with the current versions, IP, and UA. Never an UPDATE — always anINSERT, so the full history is queryable.

3. Vendor questionnaire response

External vendors filling out a questionnaire on /respond/[secureLink]have no OneComply account. We can't record a TermsAcceptancerow for them. Instead, the response page renders a footer notice covering the informed half of GDPR's informed-consent standard — they see who processes their data, under what agreement, in which EU region, before submitting.

4. Auditor Terms of Engagement

External auditors don't sign the main DPA (they're not controller or processor of the customer's data — they're an independent reviewer). Instead, they accept a Terms of Engagement document on their first portal visit, governed by AuditorAccess.auditorTermsAcceptedAt. See Auditor Access for the full flow.

User-facing acceptance history

Users can view their acceptance history at /dashboard/settings/legal. The page shows:

  • The current version of each legal document with a read/archive link.
  • Whether the user's latest acceptance matches — green checkmark when in sync.
  • A full append-only history table — every acceptance row with timestamp, version strings, and IP address.

This satisfies GDPR Art. 15 (right of access) for the acceptance records. Users never have to file a support ticket asking "what did I agree to?" — they can produce the evidence themselves.

How to update a legal document

  1. Edit the relevant page — src/app/terms/page.tsx, /privacy/page.tsx, or /dpa/page.tsx.
  2. Update the "Last updated" date on the page itself.
  3. Bump the matching constant in src/lib/terms-versions.ts to the same date.
  4. Commit + deploy. On the next deploy, every existing user hits the re-acceptance modal on their next dashboard request. New signups get the fresh version via the signup checkbox. The history table automatically records old and new acceptance rows side-by-side.

No migration needed. No data loss. No email blast. The modal is the email blast.

Regulatory mapping

  • GDPR Art. 7 (conditions for consent) — click-through + logged acceptance + version-specific re-acceptance.
  • GDPR Art. 15 (right of access) — users can view their full acceptance history on the Settings page.
  • GDPR Art. 28 (DPA) — processor-controller agreement documented per-user with timestamp, IP, and version.
  • ISO 27001 A.5.28 (supplier & external-party access) — every acceptance event is a row auditors can query.
  • DORA Art. 5 (governance) — same append-only trail, joinable to the central AuditLog by user and timestamp.