Skip to content

feat(legal): sub-processor register + 30-day operator notification - #220

Merged
AutomatosAI merged 5 commits into
mainfrom
feat/subprocessor-register
Jul 27, 2026
Merged

feat(legal): sub-processor register + 30-day operator notification#220
AutomatosAI merged 5 commits into
mainfrom
feat/subprocessor-register

Conversation

@AutomatosAI

Copy link
Copy Markdown
Owner

What

Answers the operators' data protection reviewer question (e): how will subscribers be notified of changes to the sub-processor list?

Until now the honest answer was "they mostly won't." The list was a hardcoded array in a page component, so it could only change with a deploy. The DPA §6 promised 30 days' notice and a 14-day objection window; the page told operators to subscribe by email if they wanted to hear about changes. Notice you have to opt into is not notice.

How

  • subprocessors table, seeded with the nine vendors already published. announcedAt is set to the existing page's publication date so objection windows are computed from when operators could actually have seen an entry
  • subprocessor_objections, recorded against the specific vendor
  • lib/legal/subprocessor-notice.ts — the DPA windows as pure rules with an injected clock, so 30/14-day behaviour is tested exhaustively rather than by waiting a month
  • Announcement emails every active tenant. No subscriber list, deliberately
  • Super-admin CRUD; the public page renders from the database and shows pending entries during their notice period

Decisions worth reviewing

Creating does not announce. Separate calls, so an entry can be drafted and checked before an email reaches the whole customer base and opens a window that cannot be un-opened.

The 30-day floor is enforced server-side. Going sooner needs an explicit override and a reason, both audit-logged. Taking away notice operators were already promised should leave a trace with a name against it.

Announcing refuses if the notice is short. Sending a "30 days' notice" email 5 days out is worse than sending nothing — it manufactures a record of having complied.

announcedAt is stamped even on partial send failure. The window has started for everyone who received it; re-announcing would restart their clock. Failures are counted and logged to be chased individually.

Retire, never delete. The register is the evidence a vendor once processed operator data, and for how long.

Late objections are accepted and flagged, not rejected. Refusing to record a controller's objection because they were slow would leave us processing over a live, unanswered concern.

Dr Green is seeded as-is. The agreed position is that it's an independent controller rather than our sub-processor, but that's pending written confirmation — and retiring an entry is exactly what this register is for, with a changelog entry and operator notice, rather than a silent migration asserting a legal position. Retire it through the UI once Dr Green confirms.

Not included

No admin UI page yet — the register is driven through the API. The public page, the notifier and the objection endpoint are complete; a super-admin screen is the remaining piece.

Refs docs/PRDS/prd-data-protection-remediation.md (WS3, US-011 to US-014)

🤖 Generated with Claude Code

The list was a hardcoded array in the page component, so it could only
change with a deploy, and nothing could start the 30-day notice clock the
DPA promises operators. Art. 28(2)/(4) obligations that live only in
prose are obligations nobody can evidence.

- subprocessors table, seeded with the nine vendors already published.
  announcedAt is set to the existing page's publication date so objection
  windows are computed from when operators could actually have seen the
  entry, not from today
- subprocessor_objections, recorded against the specific vendor rather
  than left in an inbox — an objection that cannot be evidenced is one
  the operator cannot rely on
- lib/legal/subprocessor-notice.ts: the DPA §6 windows as pure rules with
  an injected clock, so 30-day and 14-day behaviour is tested exhaustively
  rather than by waiting a month
- the page renders from the database and shows PENDING entries during
  their notice period. Advance notice is the point: an operator cannot
  object to a change they only see once it is already in force

Also corrects the page's own contradiction. It told operators to
subscribe by email to hear about changes, while the DPA promised they
would be notified. Notice you have to opt into is not notice; the page
now states that every active operator is emailed.

Dr Green is seeded AS-IS. The agreed position is that it is an
independent controller rather than our sub-processor, but that is pending
written confirmation — and retiring an entry is exactly what this
register is for, with a changelog entry and operator notice, rather than
a silent migration asserting a legal position.

Refs docs/PRDS/prd-data-protection-remediation.md (WS3, US-011)
The mechanism behind DPA §6. Before this the promise was prose: the page
told operators to subscribe by email to hear about changes, while the DPA
said we would notify them. A controller who never heard about a change
cannot exercise the objection right they were granted.

Every active tenant is emailed. There is no subscriber list, deliberately.

- refuses to announce a change that does not carry the promised notice.
  Sending a "30 days' notice" email 5 days out is worse than sending
  nothing, because it manufactures a record of having complied
- one operator's bad address does not stop the rest being told; failures
  are counted, logged and returned rather than aborting the run
- announcedAt is stamped even on partial failure. The objection window
  has started for everyone who did receive it, and re-announcing would
  restart their clock; failures are surfaced to be chased individually
- activateDueSubprocessors flips pending to active on the effective date,
  and never activates an unannounced entry whatever the date says —
  processing must not begin on a vendor nobody was told about
- the email states the objection deadline as a DATE. A reader should not
  have to do arithmetic to find out how long they have

Refs docs/PRDS/prd-data-protection-remediation.md (WS3, US-013)
Completes the DPA §6 loop: a vendor can be added, announced, objected to
and retired, with every step evidenced.

Creating does NOT announce. They are separate calls so an entry can be
drafted and checked before an email goes to the entire customer base and
an objection window opens that cannot be un-opened.

- the 30-day floor is enforced server-side, not trusted to the caller.
  Going sooner is possible but needs an explicit override AND a reason,
  both written to the audit log — taking away notice operators were
  already promised should leave a trace with a name against it
- amending an already-announced entry logs a warning: operators were told
  something that is no longer true
- retiring sets retiredAt and keeps the row. The register is the evidence
  that a vendor once processed operator data and for how long; deleting
  destroys the only record the relationship existed
- objections are recorded against the specific vendor, never an inbox.
  "We never received it" should not be a position we can take
- late objections are ACCEPTED and flagged, not rejected. Refusing to
  record a controller's objection because they were slow would leave us
  processing over a live, unanswered concern

Refs docs/PRDS/prd-data-protection-remediation.md (WS3, US-012/014)
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AutomatosAI, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 20facd39-bac3-4f00-b7f4-1a6d7964dcf2

📥 Commits

Reviewing files that changed from the base of the PR and between 26fe7b6 and 094dd32.

📒 Files selected for processing (11)
  • nextjs_space/app/api/super-admin/subprocessors/[id]/route.ts
  • nextjs_space/app/api/super-admin/subprocessors/route.ts
  • nextjs_space/app/api/tenant-admin/subprocessor-objections/route.ts
  • nextjs_space/app/legal/subprocessors/page.tsx
  • nextjs_space/lib/legal/subprocessor-announce.ts
  • nextjs_space/lib/legal/subprocessor-notice.ts
  • nextjs_space/lib/legal/subprocessor-schema.ts
  • nextjs_space/prisma/migrations/20260727020000_add_subprocessor_register/migration.sql
  • nextjs_space/prisma/schema.prisma
  • nextjs_space/tests/unit/subprocessor-announce.test.ts
  • nextjs_space/tests/unit/subprocessor-notice.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/subprocessor-register

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Gerard161-Site and others added 2 commits July 27, 2026 20:43
`prisma` is exported as `any` (lib/db.ts), so query results carry no type
and every callback parameter over them landed as an implicit `any` under
noImplicitAny — TS7006 across the page and the announcer.

Adds a hand-declared SubprocessorRecord and annotates the query results
rather than sprinkling `: any` at each call site, so the callbacks infer
and the register keeps type checking at the boundary where it matters.

Refs docs/PRDS/prd-data-protection-remediation.md (WS3)
@AutomatosAI
AutomatosAI merged commit 5cdc4e0 into main Jul 27, 2026
4 of 5 checks passed
AutomatosAI added a commit that referenced this pull request Jul 28, 2026
main is red and cannot deploy. The Prisma schema is invalid:

  Error validating field `tenants` in model `tenant_legal_profiles`:
  The relation field `tenants` is missing an opposite relation field
  on the model `tenants`.

prisma generate runs as a postinstall hook, so this fails `pnpm install`
before any build step — the Typecheck/Lint/Build job dies at "Install
dependencies" with everything after it skipped, and a Railway deploy
would fail the same way.

Cause: #219 and #220 each added one relation line to the `tenants` model
immediately after `email_event_mappings`. Neither conflicted on its own
and both were green in isolation. Merging them in sequence kept #220's
line and dropped #219's, leaving the tenant_legal_profiles MODEL present
with no opposite relation. No file was lost — every other WS2 artefact is
intact; it is exactly one line.

Restores that line. Also worth noting for future stacked work: two
branches appending to the same relation block is a silent-loss shape that
per-branch CI cannot catch, because each side is valid alone.

Refs docs/PRDS/prd-data-protection-remediation.md (WS2/WS3)

Co-authored-by: Gerard Kavanagh <gerard161@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants