← Blog

Legacy sites

SharePoint 2016 naar Supabase: ISO 13485 QMS-draaiboek

Een Notified Body-audit over negen weken, een SharePoint 2016-farm op de laatste benen, en een ISO 13485-spoor dat intact moet blijven. Zo verhuisden we het.

Jacob Molkenboer· Oprichter · A Brand New Company· 13 mrt 2025· 10 min
Gesloten leren logboek, koperen sleutel op kaart met groen lint, rood lakzegel op ivoorpapier, zijlicht.

Het is dinsdag in februari. De quality manager bij een medische hulpmiddelenfabrikant van 28 mensen in Eindhoven heeft nog negen weken voordat haar Notified Body langskomt voor een tussentijdse audit. Haar QMS draait op een SharePoint 2016-farm op twee Windows Server 2016-VMs in een kast achter de receptie. Microsoft beëindigde de mainstream support voor SharePoint Server 2016 in juli 2021 en de extended support in juli 2026. De IT-leverancier die de farm bouwde reageert al maanden niet meer op e-mail. De document approval-workflow werkte voor het laatst betrouwbaar in 2022.

Dit is het verhaal van hoe we haar van SharePoint kregen op een stack van Next.js, Supabase en ondertekende PDFs, zonder ook maar één ISO 13485-auditattribuut te verliezen. Het is ook een draaiboek dat je kunt overnemen.

Het audit trail-probleem waar niemand je voor waarschuwt

ISO 13485:2016 clausule 4.2.5 zegt dat records leesbaar, identificeerbaar en terugvindbaar moeten blijven. In de praktijk pakt de Notified Body een Risk Management File van drie jaar terug en stelt vier vragen op rij. Wie keurde versie 4 goed. Wanneer. Waarom werd versie 3 obsoleted. Waar is het trainingsrecord dat aantoont dat de approver gekwalificeerd was om die goedkeuring te geven.

SharePoint slaat dit allemaal op. Het slaat het alleen op in een hooiberg. Sommige attributen leven in de metadata van het document library-item. Andere in custom workflow-tabellen die de oorspronkelijke consultant heeft toegevoegd. Weer andere in Word-documenteigenschappen. En sommige in de comments-kolom van een SharePoint Designer-workflow die niemand heeft aangeraakt sinds die consultant vertrok.

De migratieklus is niet verplaats de bestanden. Het is de chain of custody verplaatsen voor elk attribuut waar de auditor naar kán vragen, en aantonen dat de verplaatsing niets heeft veranderd.

Twee weken inventarisatie voor de eerste regel code

Voor we één regel TypeScript schreven bouwden we een spreadsheet. Eén rij per attribuut. Eén kolom per ISO 13485-clausule die het raakt. Waar elk attribuut vandaag in SharePoint leeft, waar het morgen gaat leven, en hoe we gaan aantonen dat de waarde onderweg niet veranderd is.

Voor een controlled document zagen de attributen er zo uit:

  • document_id, title, doc_type (SOP, WI, FRM, ...)
  • current_version, previous_version, supersedes_id
  • effective_date, review_due_date, obsoletion_date
  • author_id, reviewer_ids, approver_id
  • approval_date, approval_reason, change_request_id
  • training_required, training_role_ids
  • linked_records (audit findings, CAPAs, change requests, deviations)

De clausules zijn de bril van de auditor. Heeft een clausule geen mapping, dan zit er een gat. Wij vonden er drie. Twee zaten verstopt in het description-veld van een SharePoint-workflow. Eén was een Word-macro die niemand sinds 2020 had gedraaid.

Waarschuwing

Kun je geen kolom aanwijzen voor clausule 4.2.4 c (approval reason) en clausule 7.3.9 (design change reasons), dan faalt de migratie de audit, ook al gaat elk bestand byte-voor-byte over.

Het schema waar een auditor blij van wordt

We hebben het datamodel in drie zorgen gesplitst. Een documents-tabel houdt de huidige status bij. Een document_versions-tabel is append-only en bewaart elke gepubliceerde versie met de SHA-256 van de bijbehorende ondertekende PDF. Een audit-schema staat in zijn eigen namespace zonder UPDATE- of DELETE-rechten, zelfs niet voor de Supabase service role.

create schema audit;

create table audit.events (
  id            bigserial primary key,
  occurred_at   timestamptz not null default now(),
  actor_id      uuid not null,
  document_id   uuid,
  version_id    uuid,
  event_type    text not null,
  before        jsonb,
  after         jsonb,
  signed_sha256 text,
  reason        text
);

revoke update, delete on audit.events
  from public, authenticated, service_role;

create or replace function audit.capture() returns trigger
language plpgsql security definer as $
begin
  insert into audit.events (
    actor_id, document_id, event_type, before, after
  ) values (
    coalesce(current_setting('app.actor', true)::uuid,
             '00000000-0000-0000-0000-000000000000'),
    coalesce(new.id, old.id),
    tg_op,
    case when tg_op in ('UPDATE','DELETE') then to_jsonb(old) end,
    case when tg_op in ('INSERT','UPDATE') then to_jsonb(new) end
  );
  return coalesce(new, old);
end $;

create trigger documents_audit
  after insert or update or delete on public.documents
  for each row execute function audit.capture();

Twee details verdienen hun plek. De revoke op UPDATE en DELETE betekent dat de audit-tabel écht append-only is op database-niveau, niet alleen per conventie in applicatiecode. En elke write moet aan het begin van de transactie set_config('app.actor', user_uuid, true) draaien, zodat de trigger in productie nooit terugvalt op de nul-UUID. We hebben in CI een Postgres-assertion gezet die elke test laat falen die naar documents schrijft zonder eerst de actor te zetten.

Ondertekende PDFs als de bron van waarheid

De ondertekende PDF, niet de database-rij, is het officiële artefact. Een auditor moet één bestand kunnen openen en daarin de inhoud zien, de handtekeningen, de approval reasons en de keten terug naar de vorige versie. De database is een snelle index op die PDFs. Brandt de database morgen af, dan vormen de PDFs in object storage nog steeds een geldig QMS.

We gebruikten PAdES B-LTA-handtekeningen (Long Term with Archive timestamp), ondersteund door een eIDAS-gekwalificeerde Time Stamping Authority. De handtekening blijft tientallen jaren verifieerbaar, ook nadat het ondertekencertificaat is verlopen. De ondertekencode is rechttoe rechtaan Node.js:

import { SignPdf } from '@signpdf/signpdf';
import { P12Signer } from '@signpdf/signer-p12';
import { plainAddPlaceholder } from '@signpdf/placeholder-plain';
import { readFileSync } from 'node:fs';

const p12 = readFileSync(process.env.QMS_SIGNER_P12!);
const signer = new P12Signer(p12, {
  passphrase: process.env.QMS_SIGNER_PASS!,
});

export async function signApproval(opts: {
  pdf: Buffer;
  reason: string;
  approver: string;
}) {
  const placeheld = plainAddPlaceholder({
    pdfBuffer: opts.pdf,
    reason: opts.reason,
    contactInfo: opts.approver,
    name: opts.approver,
    location: 'Eindhoven, NL',
  });
  return new SignPdf().sign(placeheld, signer);
}

Het reason-veld is geen decoratie. Daar schrijft de approver approved for release per CR-2026-014. Elke PAdES-viewer toont het. Auditors vinden het geweldig, omdat het waarom op dezelfde plek beantwoordt als wie en wanneer.

Cutover zonder ook maar één handtekening te verliezen

De cutover liep op een zaterdagochtend. Het plan had zeven stappen en we hebben het twee keer geoefend op een staging-kopie.

  1. Zet de SharePoint-site op read-only door bij elke groep de Contribute-rechten weg te halen. Geen back-up, geen export in dit stadium. Read-only is de migration freeze.
  2. Draai de extractor. Voor elk document trek je elke historische versie op, met volledige metadata, comments en eventueel bijgevoegde approval-mails.
  3. Voor elke historische versie genereer je een nieuwe PDF met de oorspronkelijke inhoud plus een ondertekende migratiebijlage. Die bijlage legt het SharePoint item-ID vast, de oorspronkelijke modified date, de migrerende actor en een SHA-256 van de oorspronkelijke blob.
  4. Insert in document_versions. Schrijf nooit rechtstreeks naar documents. De huidige rij wordt berekend uit de nieuwste versie waar state = 'effective'.
  5. Reconcilieer. Tel items per doc_type in SharePoint versus Supabase. Tel versies per item. Hash elke blob aan beide kanten. Elke mismatch blokkeert de cutover.
  6. Houd SharePoint nog 90 dagen live in read-only modus. Het team heeft op elke documentpagina een one-click toon dit in SharePoint-link gedurende dat venster.
  7. Doe een mock-audit voordat je de echte boekt. We nodigden een externe ISO 13485-consultant uit om een dag lang te proberen het spoor te breken. Hij vond twee dingen. Die hebben we gefixt.

Stap drie verdient een opmerking. Toen we PDFs voor historische versies opnieuw genereerden, hebben we de oorspronkelijke documentinhoud niet aangeraakt. De migratiebijlage wordt aan de PDF toegevoegd als incremental update, niet samengevoegd in het oorspronkelijke signature dictionary. Elke originele handtekening blijft zelfstandig verifieerbaar. De nieuwe countersignature bewijst de migratie zelf.

De rondleiding voor de auditor

De Notified Body-auditor logt niet in op Supabase. Hij gaat naast de quality manager zitten en zegt laat me SOP-014 versie 3 zien, wie heeft die goedgekeurd, wanneer is de training gegeven, en waar is de change request die versie 4 in gang zette.

De Next.js-UI toont één scherm per document met drie tabs.

  1. Current. De ondertekende PDF inline, signature panel standaard open, training-status badge in de header.
  2. History. Elke eerdere versie downloadbaar, per rij approver en reason direct in beeld.
  3. Trail. Het onveranderlijke event log gefilterd op dit document, met een deep link naar dezelfde view gefilterd op actor.

Dat is de hele UI. Elke read op de Trail-tab wordt zelf gelogd, voor het geval de auditor vraagt wie er naar dit record heeft gekeken. De auditor in ons geval bracht elf minuten door op dit scherm gedurende de volledige auditdag. De rest van de tijd was gesprek.

Drie lessen waarvoor we betaald hebben

Eén, laat niemand de oude data fixen tijdens de migratie. Een engineer wilde een typo in een SOP-titel uit 2021 corrigeren. Het juiste antwoord is nee. Migratie bewaart de historie. Correcties zijn change controls in het nieuwe systeem, met hun eigen goedkeuring en handtekening.

Twee, het ondertekencertificaat is het meest waardevolle asset in de hele stack. Behandel het als een root credential van een productiedatabase. De p12 staat in een hardware-backed secret store, de unlock-passphrase in Supabase Vault, en rotatie vereist goedkeuring van twee personen met een vastgelegd change record.

Drie, schrijf het auditor-walkthroughscript voor je de Next.js-UI bouwt. Leest het script natuurlijk en beantwoordt het elke clausule die de auditor kan opwerpen, dan schrijft de UI zichzelf bijna. Begin je met de UI, dan bouw je een prachtige document library en mis je het stuk waarin de auditor het event log binnen drie kliks op actor moet kunnen filteren.

Wat deze stack daadwerkelijk kost om te draaien

De hardware-rekening ging van twee Windows Server-VMs plus SQL Server-CALs plus een SharePoint farm-licentie naar een Supabase Pro-project van €25 per maand, een Vercel team-seat en een paar euro per jaar aan TSA-timestamp credits. Het eIDAS-gekwalificeerde certificaat is de enige post die echt geld kost: ongeveer €600 per jaar voor een ondertekencertificaat van een qualified trust service provider. Dat is allemaal niet het punt. Het punt is dat de quality manager op de nieuwe stack een vraag over clausule 4.2.5 in negen seconden beantwoordt. Op de oude stack kostte het haar veertien minuten en een telefoontje.

Toen we deze migratie voor het Eindhovense team draaiden, brak onze eerste countersignature de PAdES Long Term-validatie omdat we binnen het oorspronkelijke signature dictionary opnieuw tekenden in plaats van een incremental update toe te voegen, en het opnieuw stempelen van elke historische PDF kostte ons een dag. Die valkuil en twintig andere leggen we nu vast in hoe we legacy-migratie aanpakken.

Staar je naar je eigen SharePoint 2016-farm met een audit in de agenda, dan is het kleinste wat je vandaag kunt doen die attribuut-spreadsheet. Eén rij per attribuut, één kolom per ISO-clausule. Twee uur en een koffie. Het vertelt je of je naar een klus van vier weken of veertien weken kijkt.

Kern

Je auditor maakt het niet uit welke stack het QMS vasthoudt. Het maakt uit dat elke handtekening, versie en approval reason koud reproduceerbaar is.

FAQ

Is een stack van Next.js plus Supabase verdedigbaar tegenover een Notified Body voor ISO 13485?

Ja, mits de records ondertekende PDFs zijn en de audit trail op database-niveau écht append-only is. De auditor geeft om de chain of custody, niet om het merk van de database.

Hebben we eIDAS-gekwalificeerde handtekeningen nodig voor een intern QMS?

ISO 13485 verplicht geen qualified signatures, maar PAdES B-LTA met een qualified TSA geeft je handtekeningen die verifieerbaar blijven nadat het ondertekencertificaat is verlopen. Die circa 600 euro per jaar is dat waard.

Hoe lang moeten we de oude SharePoint-farm na cutover laten draaien?

Negentig dagen in read-only modus is genoeg. Lang genoeg om een gemist attribuut op te vangen, kort genoeg dat niemand het weer als live systeem gaat behandelen.

Kunnen we migreren zonder de SharePoint-writes te bevriezen?

Niet veilig. Een live bron betekent dat de reconciliatie nooit convergeert en de auditor de hash-telling niet kan vertrouwen. Een freeze van twee uur op zaterdagochtend is goedkoper dan een gefaalde audit.

legacy sitesmigrationarchitecturecase studyoperationssecurity

Iets bouwen?

Start een project