DEV Community

YatesHolloway6872
YatesHolloway6872

Posted on

Node.js AI Tutor Logs: Delete User Data Under GDPR (2026 Retention Practice)

TL;DR: For a Node.js AI tutor, keep identity out of application logs and traces. Put a short-lived random subject_ref on each agent run, keep the user-to-reference mapping in the application database, and record usage, duration, and workflow outcomes against that reference. When an erasure request arrives, delete the mapping and user-scoped operational records through one tested path. This measures latency and cost without turning every log replica into another customer database.

Choice Cost attribution Erasure work Debug context Decision
Raw user data in logs Easy join Find every copy Rich, but risky Reject
Stable hash of a user ID Easy join Still linkable Usually adequate Runner-up
Random expiring run reference plus separate mapping Controlled join Bounded stores Adequate with structured events Choose
No correlation field Weak Low Poor across a loop Aggregates only

Recommendation: choose the random reference and separate mapping. It is the least complex design that preserves run-level cost accounting while giving deletion a finite surface. For a solo SaaS, that matters more than making raw prompts convenient to search. Every hour spent chasing duplicated identity is an hour not spent shipping the weekly release.

This is an engineering decision note, not legal advice. GDPR duties depend on context and purpose. The narrower implementation goal is to make data paths legible enough that retention and erasure decisions can actually be enforced.

Keep it boring.

How should GDPR log retention work when you delete user data?

A deterministic email hash feels like a clean compromise. It is compact, searchable, and stable across services. The catch is that stability. The same input produces the same output, so the value remains a durable correlation key; deleting the source row does not remove copies already written to indexes, archives, exports, or incident notes. Predictable inputs can also be tested against candidate hashes.

A random reference changes the operational problem. Generate it per user over a defined window, or per agent run when cross-run attribution is unnecessary. Keep the mapping in a controlled application table with an expiry and a deletion path. Logs receive the reference, not an email, account name, prompt, lesson response, access token, or IP address.

This is data minimization expressed as a schema. GDPR Article 5 describes data minimization and storage limitation. Article 17 describes a right to erasure and its limits. Neither is implemented by an index-retention setting alone. A seven-day index plus a one-year object archive is a one-year system unless that archive participates in deletion or has a justified, documented lifecycle.

Short retention still helps. It limits exposure and reduces the active partitions a deletion worker may need to touch. But the unit to reason about is every copy: primary index, replay queue, dead-letter record, cold archive, local diagnostic file, analytics export, and backup policy. A dashboard setting is only one row in that inventory.

For an EU startup, these logging practices reduce risks around the right to be forgotten by making the actual deletion boundary visible. They do not replace a legal assessment. They do stop a familiar engineering mistake: treating application logs as disposable while quietly retaining the same user data in secondary storage.

The two criteria that decide the design

The first criterion is attribution without content. An edtech agent loop may plan a lesson, retrieve context, call a model several times, and grade an answer. To understand gross margin and performance, the application needs run-level facts: stage name, start time, duration, usage counters returned by the model interface, outcome, retry count, and the pricing-policy version used by the business ledger. It does not need the learner's prose in a log event.

Keep financial calculation separate from telemetry. Usage is an observed quantity. Cost is a business interpretation that can change with contracts, model choices, or accounting rules. Store raw usage counters and the calculation-policy identifier; calculate attributed cost in a controlled ledger. That preserves auditability without freezing a mutable unit price into thousands of events.

Consider one tutoring run with four stages. The run reference ties planning, retrieval, generation, and grading together; each stage emits its own duration and usage counters, while the ledger totals the four records under one pricing-policy version. Support can see that generation failed after a retry without seeing what the learner typed. Finance can attribute the run's usage without joining on an email. When the mapping expires or is deleted, those operational measurements remain useful for aggregate latency analysis but no longer provide the telemetry system with a direct route back to the learner. This is the whole bargain: preserve mechanics, remove content, and centralize the relationship that makes the reference meaningful.

That's the boundary.

The second criterion is provable lifecycle coverage. Write down each destination, its purpose, owner, retention rule, deletion capability, and restoration behavior. Then test the list. A deletion job that clears the searchable index but ignores an export bucket is incomplete. A restore procedure that silently reintroduces an erased mapping is incomplete too.

For a one-person company, keep this inventory brutally small. One structured telemetry path, one restricted mapping table, one usage ledger, and documented backup behavior are enough to ship. Adding another processor or export must buy more revenue per engineering hour than the deletion surface it creates. Generic log fan-out rarely passes that test. Outsource undifferentiated storage if it helps, but keep the identity boundary and deletion contract under application control.

A small TypeScript boundary

The useful abstraction is a narrow event type, not a clever redaction regex. Regex runs after developers have decided to serialize arbitrary objects; nested request bodies and exception metadata can slip through. An allowlist makes safe fields the easy fields.

type AgentStage = "plan" | "retrieve" | "generate" | "grade";
type AgentOutcome = "ok" | "error" | "cancelled";

type AgentRunEvent = {
  occurredAt: string;
  runRef: string;
  stage: AgentStage;
  durationMs: number;
  inputUnits: number;
  outputUnits: number;
  retryCount: number;
  outcome: AgentOutcome;
  errorCode?: string;
  pricingPolicyVersion: string;
};

type EventSink = {
  write(event: AgentRunEvent): Promise<void>;
};

export async function recordAgentStage(
  sink: EventSink,
  event: AgentRunEvent,
): Promise<void> {
  if (!event.runRef || event.durationMs < 0) {
    throw new Error("invalid_agent_run_event");
  }
  await sink.write(event);
}
Enter fullscreen mode Exit fullscreen mode

The type has no escape hatch such as metadata: Record<string, unknown>. That omission is deliberate. An error code belongs here; an exception message may contain lesson text, a URL query, or response content, so it belongs in a separately controlled diagnostic path only when the documented purpose requires it.

Create the correlation reference with the platform random UUID function. The telemetry writer never receives a user object.

import { randomUUID } from "node:crypto";

type RunIdentityStore = {
  save(input: {
    runRef: string;
    userId: string;
    expiresAt: Date;
  }): Promise<void>;
  deleteForUser(userId: string): Promise<void>;
};

export async function beginAgentRun(
  identities: RunIdentityStore,
  userId: string,
  expiresAt: Date,
): Promise<string> {
  const runRef = randomUUID();
  await identities.save({ runRef, userId, expiresAt });
  return runRef;
}
Enter fullscreen mode Exit fullscreen mode

This does not make the reference anonymous by declaration. While the mapping exists, it can be related to a user and should be treated accordingly. The gain is control: access to resolution is narrow, expiry is explicit, and deletion no longer depends on parsing human-written messages.

Sampling needs the same discipline. OpenTelemetry distinguishes head sampling, decided near trace creation, from tail sampling, decided after more of a trace is available. Tail sampling can preserve unusual latency or errors, but a sampler is not a privacy filter. Sensitive content must be excluded before an event or span reaches either path. Keep the usage ledger independent of trace sampling as well; otherwise a sampled-out successful run becomes missing cost and attribution totals drift.

Test deletion like a product feature

A retention policy that exists only in a document will decay. Put a synthetic user through the real agent loop, request deletion, advance or simulate the relevant lifecycle, and query each declared destination by the synthetic identifiers. The expected result should be explicit for every store: deleted, aggregated beyond identification, retained under a documented exception, or pending until a defined backup lifecycle completes.

Test failure paths too. Let archive deletion fail while primary deletion succeeds. The job should remain retryable and observable without copying the user identifier into its own error log. A deletion operation ID can correlate attempts; the protected deletion record can hold the subject relationship. Record completion per destination so retries are idempotent.

There is an awkward operational trade-off. Removing the mapping quickly makes later user-specific debugging harder. Accept it. Keep non-identifying run mechanics long enough to answer system questions such as p95 stage latency, retry rates, and usage by model class. If support truly needs a user-linked diagnostic session, create a time-boxed flow with an explicit purpose instead of retaining every learner's history just in case.

Three checks catch most regressions before deployment:

  • A schema test rejects unknown event fields and representative prompt-shaped keys.
  • An integration test verifies deletion across the mapping, ledger, active telemetry store, and archive lifecycle.
  • A restore drill verifies that erased mappings do not return when a backup is restored.

The third check is easy to postpone because it does not help this week's feature. Schedule it anyway. A deletion design is only as credible as its recovery path.

When is the stable-hash runner-up better?

A stable pseudonymous key can be reasonable when authorized analysts must join long-running aggregates across many runs and a per-window mapping service would add disproportionate operational complexity. It can also fit when the joining purpose, access boundary, retention period, and erasure method are explicit and tested. Use a keyed construction with managed rotation rather than a bare hash of a predictable email, and treat the result as linkable data.

The cost is harder erasure. Every destination holding that key must support deletion or expiry, key rotation becomes part of lifecycle planning, and old aggregates may remain linkable until their window closes. This option earns its place only when cross-run analysis has a concrete product or financial use. Curiosity is not a retention purpose.

No correlation field is the other useful edge case. Fleet-level counters for latency buckets, total usage, and outcome counts may need no user or run identifier. Prefer that shape when the question is aggregate. Do not add a join key by habit.

The decision is plain: measure the AI loop with structured, content-free events; attribute cost through a controlled reference; and make deletion span the same destinations as ingestion. The architecture stays small, weekly shipping stays realistic, and an erasure request becomes a routine operation instead of a search through prose.

Further reading

Top comments (0)