Din lille webapp ved mere om folk end du tror
– Du logger hele requesten i console, “for en sikkerheds skyld”
– Du har smidt Google Analytics på, fordi “det har alle andre”
– Du har aldrig besluttet, hvornår dine logs skal slettes
– Du har ingen idé om, hvad din error-tracker gemmer om brugere
Hvis du nikkede til mindst et punkt, så er den her artikel til dig.
Lad mig starte med en disclaimer: Jeg er udvikler, ikke jurist. Det du får her, er et teknisk perspektiv på GDPR for udviklere til små webapps. Altså hvordan du designer kode, logs og dataflows, så du ikke laver helt skøre ting med persondata uden at opdage det.
Hvis du på et tidspunkt står med et rigtigt produkt, rigtige kunder og rigtige penge involveret, bør du tale med nogen der laver jura til daglig. Men du kan komme overraskende langt med lidt teknisk omtanke.
1. De stille persondata der sniger sig ind i din app
Mange tænker “persondata” og forestiller sig CPR-numre og pas. I virkeligheden er det langt mere lavpraktisk i små webapps: IP-adresser, e-mails i logs, user agents, request bodies.
Så lad os være konkrete. Her er nogle typiske steder, hvor dine hobbyprojekter allerede håndterer persondata uden at du har tænkt over det.
IP-adresser og user agent i server-logs
Standard webserver-logs indeholder ofte:
- IP-adresse
- Timestamp
- URL
- User agent (browser, OS osv.)
IP-adresse + timestamp + URL er nok til at blive set som persondata. Det er ikke specielt dramatisk, men det betyder, at du skal tænke over hvor længe du gemmer det, og om du kan nøjes med mindre.
Et simpelt mønster er at have to niveauer af logs:
- En kortlivet “access log” til driftsfejl (få dage eller uger)
- En mere anonym statistiklog uden fulde IP-adresser
Hvis du bruger en reverse proxy eller hosting der logger automatisk, kan du ofte skrue på formatet. Fx i Nginx kan du maskere IP-adresser delvist, så du kun beholder de første par oktetter.
E-mails og navne i fejlbeskeder
En klassiker: Du har en form der fejler, og i panik smider du hele payloaden i en log:
// Express eksempel
app.post("/signup", async (req, res) => {
try {
await createUser(req.body);
res.status(201).send("ok");
} catch (err) {
console.error("Signup failed", { body: req.body, error: err });
res.status(500).send("error");
}
});
Hvis req.body indeholder email, name, måske endda adresse, så gemmer du pludselig persondata i dine logs. Måske også i en tredjeparts error-tracker.
Et bedre mønster er at logge et “request id” eller en genereret reference, og så nøjes med at gemme de felter, du faktisk har brug for til debugging.
Stacktraces der indeholder tokens
En anden fæl ting er, når fejlbeskeder indeholder hele JWT tokens eller session IDs. Kombineret med IP og tid kan det være mere følsomt end du tror.
Hvis du logger headers, så sørg for aldrig at logge hele Authorization-headeren. Mask ét eller andet:
const safeHeadersForLog = (headers) => ({
"user-agent": headers["user-agent"],
"content-type": headers["content-type"],
// Authorization ryger helt ud eller maskeres
});
Det samme gælder cookies. Du behøver ikke logge folks sessions-cookie for at debugge din 500-fejl.
Form data i analytics og heatmaps
Hvis du kaster tredjeparts-scripts ind, som fx analytics eller “session replay”, så kan de nogle gange opsnappe:
- Indtastede e-mails i formularer
- Brugernavne
- Andre tekstfelter, som helt klart er persondata
Gode værktøjer giver dig mulighed for at “maskere” felter, men kun hvis du går ind og gør det. Hvis du bare smider scriptet ind og går videre, kan du ende med at sende alt muligt, du ikke havde forventet.
Hvis du er i den helt lette ende af full stack webapps, er den mest fredelige løsning ofte at vælge analytics der slet ikke bruger cookies eller persondata.
2. Dataflowet på 15 minutter – tegn det og find lækagerne
Inden vi går ned i logs vs analytics vs retention, er der én øvelse, der gør resten meget nemmere: Tegn dataflowet for din app. Ikke fancy arkitekturdiagram, bare en simpel tegning.
Tag et stykke papir (eller et simpelt diagramværktøj) og tegn:
- Klient (browser, mobil osv.)
- Server(e): API, backend, funktioner i skyen
- Database(r) og køer
- Tredjepartsservices: analytics, mail, error tracking, hosting
Så stiller du ét spørgsmål ved hver pil: Hvilke persondata kan bevæge sig her?
Klient til server
Her er det ret åbenlyst:
- Forms: email, password, navn, beskeder
- Upload: billeder, dokumenter
- Login: tokens, session-id
Skriv ned hvilke felter du sender. Ikke bare “brugerobjekt” men reelt: { email, name, avatarUrl }. Det gør det lettere senere at beslutte, hvad du vil kunne eksportere/slette.
Server til tredjepart
Her sker de mere skjulte ting:
- Sender du e-mails via et mail-API (Mailgun, SendGrid, etc.)?
- Sender du events til analytics fra backend?
- Bruger du en ekstern error-tracker (Sentry, etc.)?
Hvis ja, så flytter du persondata videre til et andet firma. Det er ikke ulovligt, men det betyder, at du skal vide nogenlunde, hvad du sender, og kunne forklare det i en simpel tekst til brugeren.
Server til database og backups
Her er de data, du normalt godt ved du har. Men to ting bliver ofte glemt:
- Automatiske backups fra din database-host
- Evt. egne dumps du tager manuelt og gemmer i et random cloud-drive
Hvis du vil kunne slette eller anonymisere data “rigtigt”, skal du have en idé om, hvor længe backups lever, og om de er krypteret. Mange managed databaser dokumenterer ret tydeligt deres retention. Læs den side. Gerne mens du ikke er alt for træt.
3. Logs og error tracking – det du faktisk bør ændre i koden
Logs er dér, hvor små apps ofte går helt amok med persondata. Mest fordi det er så fristende at logge alt, når man fejlsøger. Jeg kender det alt for godt, og ja, jeg har også haft “console.log(user)” liggende i produktion for længe.
Design et “safe log event” format
I stedet for at smide hele objekter ind i logs, kan du definere en lille funktion, der renser dem.
// pseudo-eksempel på et "safe log" helper
const redactUser = (user) => user && {
id: user.id,
role: user.role,
};
const safeLog = (msg, context = {}) => {
const cleaned = {
...context,
user: redactUser(context.user),
// Fjerner følsomme felter fra body
body: context.body && {
action: context.body.action,
},
};
console.log(msg, cleaned);
};
Pointen er: Du vælger aktivt, hvilke felter der må ende i logs, i stedet for at lade JavaScript lave et dump af hele verden.
Hvis du bruger en error-tracker, så giv den det samme “rensede” objekt, ikke rå request- eller user-objekter.
Maskér kendte følsomme felter
Passwords må aldrig i logs. Men også ting som:
- Reset tokens
- API-nøgler
- Adgangslinks der indeholder hemmelige tokens
Et mønster jeg godt kan lide, er at have en lille liste af “forbudte” feltnavne, og så lave en generel redaktør:
const SENSITIVE_KEYS = ["password", "token", "secret", "authorization"];
function redactObject(obj) {
if (!obj || typeof obj !== "object") return obj;
const result = Array.isArray(obj) ? [] : {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_KEYS.includes(key.toLowerCase())) {
result[key] = "[REDACTED]";
} else if (typeof value === "object") {
result[key] = redactObject(value);
} else {
result[key] = value;
}
}
return result;
}
Så kan du køre redactObject på dine bodies, før du logger dem. Det redder dig fra en del “hov, det var hele password-reset-linket” øjeblikke.
Begræns retention for logs
Selv hvis dine logs er rensede, behøver du ikke gemme dem for evigt. En simpel model er:
- Applikationslogs: 7-30 dage, afhængigt af behov
- Aggregater/metrics uden persondata: længere
På filbaserede logs kan du bruge log-rotation. På hosted løsninger kan du ofte vælge retention i UI. Vælg noget aktivt, i stedet for at lade standarden være “indtil disken er fuld”.
Hvis du er interesseret i debugging som vane, så er fejlfinding og debugging i øvrigt en kategori, der er værd at dykke ned i. Logs er kun én del af det.
4. Analytics uden at drukne i cookies og bannere
Analytics er næste hotspot. Mange smider Google Analytics på per refleks, og så har de pludselig brug for cookie-banner, databehandleraftaler og en del tekst.
Til små webapps kan du spørge dig selv: Hvad vil jeg faktisk vide?
Cookie-baseret analytics vs cookieless
Groft forsimplet har du to verdener:
- Cookie-baseret analytics: fx Google Analytics, der typisk tracker på individniveau og kræver samtykke.
- Cookieless / privacy-venlige løsninger: der måler pageviews og simple events uden at identificere brugere på samme måde.
Hvis du bare vil se “hvor mange besøg om dagen” og “hvilke sider bliver vist”, så er den anden kategori ofte nok. Og du slipper for at bygge halve cookie-bannere som i artiklen om rigtige cookie bannere.
Beslutningsmodel for små sites
Jeg plejer at tænke sådan her:
- Portfolio-side, blog, hobbyprojekt: vælg cookieless eller slet ingen analytics.
- Seriøst produkt med marketingfolk involveret: forvent cookie-baseret analytics og et rigtigt samtykke-setup.
Hvis du vælger noget i den “lette” ende, så læs deres privacy-dokumentation. Mange privacy-fokuserede værktøjer forklarer ret pænt, hvilke data de indsamler, og hvornår de mener, at det er uden for cookie-reglerne.
5. Retention der kan implementeres på en eftermiddag
“Retention” lyder tungt, men for en lille app handler det mest om: Hvornår sletter du ting igen, og gør du det automatisk, eller kun når du får dårlig samvittighed?
Start simpelt: Aldersbaseret sletning
Et mønster du kan implementere uden at skrive 300 linjer cron-job, er en simpel aldersregel på data. Eksempel: Brugere der har været inaktive i X måneder, markeres til sletning.
Hvis du bruger en database med timestamps, kan du lave et script, der fjerner eller anonymiserer gamle rækker:
-- pseudo-SQL til at anonymisere gamle brugere
UPDATE users
SET email = CONCAT("deleted-", id, "@example.com"),
name = NULL,
deleted_at = NOW()
WHERE last_login < NOW() - INTERVAL '12 months'
AND deleted_at IS NULL;
Du beholder måske nogle tekniske nøgler (id, created_at) til statistik, men fjerner det, der direkte siger “det her er Anders fra Århus”.
Backups – hvad gør du realistisk?
Backups er tricky, for du kan sjældent gå ind og slette én bruger i en gammel backup. Her er det mere realistisk at:
- Vide hvor længe din udbyder gemmer backups
- Undgå at tage ekstra ustrukturerede dumps og glemme dem
Hvis din database-host siger “vi gemmer backups i 30 dage”, så ved du, at en sletning i dag i praksis kan ligge i gamle backups i op til en måned. Det kan du beskrive ærligt i din privacy-tekst.
Hvis du manuelt tager exports til udvikling, så sørg for at:
- Bruge minimalt datasæt
- Anonymisere e-mails og navne
- Gem dem et sted med adgangskontrol, ikke i en tilfældig offentlig bucket
6. Export og sletning – en minimal API-kontrakt
GDPR taler om retten til indsigt (export) og retten til sletning. Det lyder voldsomt, men i en lille webapp kan du komme langt med en “minimal kontrakt” du bygger ind i din backend fra starten.
Definér din “brugerdata-model” ét sted
Før du kan eksportere eller slette, skal du vide, hvor du gemmer ting. Tegn relationerne:
users: id, email, name, created_atorders: id, user_id, amount, created_ataudit_logs: id, user_id, action, timestamp
Lav gerne en lille kommentar i koden eller i README, der siger: “Brugerdata er primært i disse tabeller”. Det gør sletning mindre gætteri senere.
Simpelt export-endpoint
Et minimal JSON-export kan se sådan ud:
// Pseudo-Express route med auth tjek
app.get("/me/export", requireAuth, async (req, res) => {
const userId = req.user.id;
const user = await db.user.findById(userId);
const orders = await db.order.findMany({ userId });
res.json({
user,
orders,
});
});
Du skal selvfølgelig sikre, at kun den autentificerede bruger kan hente sin egen data. Men pointen er: Du sampler de tabeller, der er relevante, og returnerer dem samlet.
Hvis du vil være ekstra flink mod fremtidige dig, kan du først lave en lille service-funktion:
async function exportUserData(userId) {
const user = await db.user.findById(userId);
const orders = await db.order.findMany({ userId });
return { user, orders };
}
Så kan du genbruge den både til API og fx en admin-funktion.
Simpelt delete-flow
For sletning er du nødt til at beslutte, om du vil:
- Slette rækker helt
- Eller anonymisere dem (fx for at beholde statistik)
En blanding er almindelig: Du fjerner kontaktdata, men beholder anonymiserede poster.
async function deleteUserAccount(userId) {
// Anonymiser ordredata
await db.order.updateMany({ userId }, {
userId: null,
});
// Anonymiser selve brugeren
await db.user.update(userId, {
email: `deleted-${userId}@example.com`,
name: null,
deletedAt: new Date(),
});
}
Det er ikke perfekt i juridisk forstand, men det er stærkt meget bedre end ingenting, og det er noget, du kan implementere i en lille app uden at miste overblikket.
7. Tredjeparts-SDKs – checklisten før du npm install
Hver gang du smider et tredjeparts-script eller SDK ind, inviterer du et nyt firma ind i dit dataflow. Det er ikke farligt i sig selv, men det er godt at gøre bevidst.
Tre spørgsmål du bør stille hver gang
Næste gang du overvejer et nyt SDK, så tjek:
- Sender det data videre til en vendor (analytics, logs, chat, video osv.)?
- Hvilke data samler det op som standard? Er der “masking” muligheder?
- Har de en nogenlunde forståelig privacy-side, så du kan forklare brugere hvad der sker?
Hvis meget af dokumentationen føles som at læse gammel græsk, er det ofte et tegn på, at værktøjet ikke er målrettet små hobbyprojekter men store virksomheder med jurister.
Frontend vs backend-SDKs
Det er også værd at skelne mellem:
- Frontend-SDKs som kører i brugerens browser og typisk kan se mere (DOM, inputfelter, kliks).
- Backend-SDKs som du selv kalder og har mere kontrol over, hvilke felter du sender.
Hvis du kan vælge en backend-integration frem for et stort frontend-script, har du ofte bedre styr på, hvad du sender, og hvornår.
8. Dit lille “privacy README” – så du ikke glemmer dine valg
Til sidst vil jeg anbefale noget, der ligner en blanding af dokumentation og huskeseddel: En lille “privacy README” i dit repo.
Den kan hedde PRIVACY.md eller være en sektion i din almindelige README som du måske allerede har styr på efter at have læst om gode READMEs.
Hvad den bør indeholde
Jeg ville mindst skrive:
- Hvilke data du gemmer om brugere (felter, tabeller, services)
- Hvorfor du gemmer dem (login, statistik, fejlretning)
- Hvornår du sletter eller anonymiserer dem (fx inaktivitet, manuelle sletninger)
- Hvordan en bruger i teorien kunne få sine data (export-endpoint) og blive slettet
- Liste over tredjepartsservices du sender data til
Det gør to ting:
- Hjælper fremtidige dig med at huske, hvad du har besluttet.
- Gør det muligt at skrive en simpel offentlig privacy-tekst til brugere, hvis appen vokser.
Og ja, det giver faktisk også et pluspunkt i din portfolio, fordi det viser, at du tænker sikkerhed og ansvarlighed ind i dine projekter, ikke bare features.
Eksempel på kort privacy README for en lille app
# Privacy-noter for "Habit Tracker" app
## Hvilke data gemmer vi
- Bruger: email, hashed password, timestamps
- Habits: titel, frekvens, tilknyttet bruger-id
- Logs: korte API-logs med delvist anonymiseret IP (xx.xx.*.*)
## Formål
- Email + password: login og adgangsstyring
- Habits: gemme brugerens data om vaner
- Logs: fejlsøgning og misbrugsdetektering
## Sletning og retention
- Bruger kan anmode om sletning (pt. manuel via admin-script)
- Ved sletning anonymiseres email og navn, habits bevares uden reference til bruger
- Logs roteres automatisk efter 14 dage
## Tredjepartsservices
- Error tracking: <navn> (sender anonymiseret fejl-info uden email)
- Hosting: <navn> (database-backups i 30 dage, krypteret)
Det er ikke jura-perfekt, men det er ærligt og teknisk sandt. Det er et godt sted at starte, og noget en jurist kan bygge videre på, hvis det en dag bliver nødvendigt.
9. Sådan kommer du i gang i dag uden at omskrive hele din app
Hvis du er lidt mentalt mæt nu, giver det mening. GDPR føles ofte som en mur af tekst. Jeg vil slutte med et helt lavpraktisk forslag til, hvad du kan gøre i din næste lille webapp, uden at det bliver et forskningsprojekt.
En realistisk to-do liste
Vælg 3 ting at gøre først:
- Stop med at logge hele request bodies og headers. Lav en lille
safeLog-helper. - Tegn dit dataflow på et stykke papir og skriv ned, hvilke tredjepartsservices får hvilke data.
- Lav en minimal
/me/export-route i din backend, så du kan hente en samlet JSON for en bruger.
Når det spiller, kan du senere:
- Tilføje et simpelt slette-flow
- Skifte til et mere privacy-venligt analytics-setup eller droppe det helt i små projekter
- Skrive et kort “privacy README” i dit repo
Det store skift er at gå fra “ingen idé om hvor data ender” til “jeg kan nogenlunde forklare, hvad der sker”. Når først du har den vane, føles det gradvist mindre skræmmende, også i større softwareprojekter i praksis.
Jeg er spændt på, hvornår vi som udviklere begynder at se på privacy på samme måde som automatiske tests: Ikke som pynt, men som en naturlig del af at bygge noget, man kan stå inde for om et par år.









Send kommentar
Du skal være logget ind for at skrive en kommentar.