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:
- classify the data you collect
- decide where it is allowed to live
- define retention and deletion behavior
- keep exports and access reviews reproducible
- redact data before it reaches logs, devtools, error reporters, or vendor metadata
Data classification
Classify app data by risk before choosing what to store or emit:
| Class | Examples | Default handling |
|---|---|---|
| Stable identifiers | userId, tenantId, requestId, resourceId | Safe for logs, audit records, and error context when access is controlled |
| Business metadata | status, counts, feature names, timestamps | Usually safe when it does not reveal private content |
| User content | messages, bios, comments, uploads, form answers | Store in app repositories or storage only; avoid diagnostic sinks |
| Secrets | passwords, tokens, cookies, provider credentials, API keys | Never log, audit, report, or expose |
| Regulated data | PHI, payment details, government IDs, legal records | Store 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:
| Surface | Typical owner | Retention question |
|---|---|---|
| Primary tables | Feature repositories | How long does the product need this record? |
| Audit logs | Audit adapter and compliance policy | How long must activity be explainable? |
| Object storage | Upload definitions and attachment repositories | When should files be deleted, archived, or quarantined? |
| Outbox and job state | Background workflow adapters | When can completed or failed work be pruned? |
| Devtools persistence | Development tooling config | Is persistence disabled or short-lived outside local development? |
| Logs and error reporting | Provider settings | Does 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:
- authorize the actor and tenant at the use-case boundary
- delete or anonymize feature-owned records through repositories
- remove or tombstone object-storage attachments
- revoke active sessions and tokens
- enqueue background cleanup when providers need asynchronous deletion
- audit that the deletion request was processed without storing the deleted private content
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:
- passwords, password reset tokens, magic links, session tokens, API keys, cookies, authorization headers, and provider credentials
- raw request bodies, multipart upload bodies, private messages, comments, notes, form answers, and user-authored files
- PHI, payment details, government IDs, financial account numbers, and legal documents
- full provider responses when the provider may echo sensitive request data
- presigned upload URLs, signed download URLs, or URLs containing access tokens
- unbounded objects whose shape may grow to include private fields later
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:
- export use cases only returning authorized tenant data
- deletion use cases removing or anonymizing every feature-owned record
- storage cleanup deleting or tombstoning object keys
- audit records using stable IDs instead of deleted content
- logs and error reporter metadata excluding raw secrets and private fields
- scheduled cleanup being idempotent
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
- Production security for the pre-deploy security checklist.
- Audit and activity logging for durable business activity records.
- Logging for structured diagnostic logs.
- Error reporting and alerting for production exception capture.
- Storage and Uploads for file ownership and cleanup.
- Scheduled tasks for retention and cleanup jobs.