Privacy lifecycle

Privacy is an application lifecycle concern, not a single Beignet primitive. Beignet gives you ports, use cases, hooks, audit logs, storage, uploads, and redaction helpers so each app can make sensitive data ownership explicit.

Design the lifecycle before production data arrives:

Data classification

Classify app data by risk before choosing what to store or emit:

ClassExamplesDefault handling
Stable identifiersuserId, tenantId, requestId, resourceIdSafe for logs, audit records, and error context when access is controlled
Business metadatastatus, counts, feature names, timestampsUsually safe when it does not reveal private content
User contentmessages, bios, comments, uploads, form answersStore in app repositories or storage only; avoid diagnostic sinks
Secretspasswords, tokens, cookies, provider credentials, API keysNever log, audit, report, or expose
Regulated dataPHI, payment details, government IDs, legal recordsStore only behind explicit product, retention, and vendor controls

Prefer stable IDs over raw content in every operational system. Logs, audit records, devtools, and error reporters should help an operator find the app record that owns the data, not duplicate the data.

Retention

Retention should be owned by app policy and implemented through use cases, repositories, storage adapters, and scheduled tasks.

Define retention windows for each durable surface:

SurfaceTypical ownerRetention question
Primary tablesFeature repositoriesHow long does the product need this record?
Audit logsAudit adapter and compliance policyHow long must activity be explainable?
Object storageUpload definitions and attachment repositoriesWhen should files be deleted, archived, or quarantined?
Outbox and job stateBackground workflow adaptersWhen can completed or failed work be pruned?
Devtools persistenceDevelopment tooling configIs persistence disabled or short-lived outside local development?
Logs and error reportingProvider settingsDoes the vendor retention match your data classification?

Use scheduled tasks for routine cleanup:

export const pruneExpiredSessions = schedules.defineSchedule(
  "privacy.prune-expired-sessions",
  {
    every: "1 day",
    async handle({ ctx }) {
      await ctx.ports.sessions.deleteExpired({
        before: ctx.ports.clock.now(),
      });
    },
  },
);

Keep cleanup idempotent. Deleting the same record twice should be safe, because scheduled work can retry.

Export

Exports should be explicit use cases, not ad hoc database reads. That keeps authorization, tenancy, audit logging, and redaction in one workflow.

export async function exportUserData(ctx: AppContext, input: { userId: string }) {
  await assertCanExportUserData(ctx, input.userId);

  const profile = await ctx.ports.users.findExportProfile(input.userId);
  const uploads = await ctx.ports.attachments.listExportableForUser(input.userId);

  await ctx.ports.audit.record(
    auditEntry(ctx, {
      action: "privacy.user-data.exported",
      resource: { type: "user", id: input.userId },
      metadata: { uploadCount: uploads.length },
    }),
  );

  return {
    profile,
    uploads: uploads.map((upload) => ({
      id: upload.id,
      fileName: upload.fileName,
      contentType: upload.contentType,
    })),
  };
}

Do not export provider credentials, internal IDs that are not meaningful to the user, audit records about other actors, or records outside the tenant boundary.

Deletion and anonymization

Use deletion when the product and legal model allow the record to disappear. Use anonymization when the app must preserve aggregate history while removing personal identity.

Good deletion workflows:

export async function deleteUserAccount(ctx: AppContext, input: { userId: string }) {
  await assertCanDeleteUser(ctx, input.userId);

  await ctx.ports.uow.transaction(async (tx) => {
    await tx.attachments.markDeletedForUser(input.userId);
    await tx.users.anonymize(input.userId, {
      displayName: "Deleted user",
      email: null,
      bio: null,
    });

    await tx.audit.record(
      auditEntry(ctx, {
        action: "privacy.user.anonymized",
        resource: { type: "user", id: input.userId },
      }),
    );
  });
}

Deletion does not automatically remove data already copied into logs, error reporters, vendor dashboards, backups, analytics, or support exports. That is why diagnostic surfaces should avoid raw private content in the first place.

Redaction

Use Beignet's redaction helpers for generic sensitive structures:

import { redactHeaders, redactValue } from "@beignet/core/ports";

logger.info("Provider callback received", {
  headers: redactHeaders(request.headers),
  payload: redactValue(providerPayload),
});

Generic redaction is not enough for domain privacy. Add app-owned redaction for fields such as patient notes, private messages, payment descriptions, legal records, uploaded document names, and free-form text.

function redactProfileForDiagnostics(profile: UserProfile) {
  return {
    id: profile.id,
    tenantId: profile.tenantId,
    hasBio: Boolean(profile.bio),
    createdAt: profile.createdAt,
  };
}

Keep redaction allow-list based when possible. It is safer to choose the fields that may leave the application than to strip a few known-sensitive fields from a large object.

What not to log

Do not send these values to logs, devtools, audit metadata, error reporters, job metadata, provider instrumentation, or alert payloads:

Log stable references instead:

logger.warn("Upload scan failed", {
  requestId: ctx.requestId,
  tenantId: ctx.tenant?.id,
  actorId: ctx.actor.id,
  attachmentId,
  objectKeyHash,
});

Testing privacy behavior

Privacy behavior should be testable like any other application workflow.

Add focused tests for:

Memory ports make these tests cheap. Capture logs, audit entries, jobs, mail, notifications, and error reports in memory, then assert on the emitted metadata.

Related pages