| name | documenso-data-handling |
| description | Handle document data, signatures, and PII in Documenso integrations.
Use when managing document lifecycle, handling signed PDFs,
or implementing data retention policies.
Trigger with phrases like "documenso data", "signed document",
"document retention", "documenso PII", "download signed pdf".
|
| allowed-tools | Read, Write, Edit |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Documenso Data Handling
Overview
Best practices for handling documents, signatures, and personally identifiable information (PII) in Documenso integrations.
Prerequisites
- Understanding of data protection regulations (GDPR, CCPA)
- Secure storage infrastructure
- Encryption capabilities
Document Lifecycle
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ DRAFT │────▶│ PENDING │────▶│ COMPLETED │
│ │ │ (Signing) │ │ │
└─────────────┘ └──────┬──────┘ └──────┬──────┘
│ │
▼ │
┌─────────────┐ │
│ REJECTED/ │ │
│ CANCELLED │ │
└─────────────┘ │
▼
┌─────────────┐
│ ARCHIVED │
│ (Storage) │
└─────────────┘
Downloading Signed Documents
Step 1: Download Completed Document
import { getDocumensoClient } from "./documenso/client";
import fs from "fs/promises";
async function downloadSignedDocument(
documentId: string,
outputPath: string
): Promise<void> {
const client = getDocumensoClient();
const doc = await client.documents.getV0({ documentId });
if (doc.status !== "COMPLETED") {
throw new Error(`Document not completed. Status: ${doc.status}`);
}
const pdfData = await client.documents.downloadV0({ documentId });
await fs.writeFile(outputPath, Buffer.from(pdfData as ArrayBuffer));
console.log(`Signed document saved to: ${outputPath}`);
}
Step 2: Secure Storage
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import crypto from "crypto";
const s3 = new S3Client({ region: process.env.AWS_REGION });
interface StorageResult {
key: string;
bucket: string;
checksum: string;
}
async function storeSignedDocument(
documentId: string,
pdfData: Buffer,
metadata: Record<string, string>
): Promise<StorageResult> {
const date = new Date();
const key = `signed-documents/${date.getFullYear()}/${
String(date.getMonth() + 1).padStart(2, "0")
}/${documentId}.pdf`;
const checksum = crypto.createHash("sha256").(pdfData).();
s3.(
({
: process..!,
: key,
: pdfData,
: ,
: ,
: process..,
: {
...metadata,
checksum,
: ().(),
},
})
);
{
key,
: process..!,
checksum,
};
}
PII Handling
Step 3: Recipient Data Management
interface RecipientPII {
email: string;
name: string;
documentIds: string[];
}
interface PIIReference {
recipientHash: string;
documentIds: string[];
createdAt: Date;
lastAccessedAt: Date;
}
function hashRecipientEmail(email: string): string {
const salt = process.env.PII_SALT!;
return crypto
.createHmac("sha256", salt)
.update(email.toLowerCase().trim())
.digest("hex");
}
async function trackRecipientDocument(
email: string,
documentId: string
): Promise<void> {
const recipientHash = hashRecipientEmail(email);
await db..({
: { recipientHash },
: {
recipientHash,
: [documentId],
: (),
: (),
},
: {
: { : documentId },
: (),
},
});
}
Step 4: Data Minimization
async function addRecipientMinimal(
documentId: string,
email: string,
name: string
): Promise<string> {
const client = getDocumensoClient();
const recipient = await client.documentsRecipients.createV0({
documentId,
email,
name,
role: "SIGNER",
});
return recipient.recipientId!;
}
function sanitizeForLogging(doc: any): any {
return {
id: doc.id,
title: doc.title,
status: doc.status,
recipientCount: doc.recipients?.length ?? 0,
};
}
Data Retention
Step 5: Retention Policy Implementation
interface RetentionPolicy {
completedDocuments: number;
draftDocuments: number;
cancelledDocuments: number;
}
const RETENTION_POLICY: RetentionPolicy = {
completedDocuments: 2555,
draftDocuments: 30,
cancelledDocuments: 90,
};
async function enforceRetentionPolicy(): Promise<RetentionReport> {
const report = {
draftsDeleted: 0,
cancelledDeleted: 0,
archivedCompleted: 0,
};
const client = getDocumensoClient();
const now = new Date();
const drafts = await findDocumentsByStatus("DRAFT");
for (const draft of drafts) {
const age = ( (draft.!));
(age > .) {
client..({ : draft.! });
report.++;
}
}
cancelled = ();
( doc cancelled) {
age = ( (doc.!));
(age > .) {
(doc.!);
report.++;
}
}
report;
}
(): {
now = ();
.((now.() - date.()) / ( * * * ));
}
Step 6: Archive Before Delete
interface ArchiveRecord {
documentId: string;
title: string;
status: string;
completedAt?: string;
recipientEmails: string[];
storageKey: string;
archivedAt: string;
}
async function archiveAndDelete(documentId: string): Promise<void> {
const client = getDocumensoClient();
const doc = await client.documents.getV0({ documentId });
let storageKey = "";
if (doc.status === "COMPLETED") {
const pdfData = await client.documents.downloadV0({ documentId });
const result = await storeSignedDocument(
documentId,
Buffer.from(pdfData as ArrayBuffer),
{ title: doc.title!, : doc. }
);
storageKey = result.;
}
: = {
documentId,
: doc.!,
: doc.!,
: doc.,
: doc.?.(
(r.!)
) ?? [],
storageKey,
: ().(),
};
db..({ : archiveRecord });
client..({ documentId });
.();
}
GDPR Compliance
Step 7: Data Subject Access Request (DSAR)
interface DSARResponse {
recipientHash: string;
documents: Array<{
documentId: string;
title: string;
status: string;
signedAt?: string;
}>;
exportedAt: string;
}
async function handleDSAR(email: string): Promise<DSARResponse> {
const recipientHash = hashRecipientEmail(email);
const client = getDocumensoClient();
const documents: DSARResponse["documents"] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const result = await client.documents.findV0({ page, perPage: 100 });
for (const doc of result.documents ?? []) {
const hasRecipient = doc.?.(
r.?.() === email.()
);
(hasRecipient) {
recipient = doc.?.(
r.?.() === email.()
);
documents.({
: doc.!,
: doc.!,
: doc.!,
: recipient?.,
});
}
}
hasMore = (result.?. ?? ) === ;
page++;
}
{
recipientHash,
documents,
: ().(),
};
}
Step 8: Right to Erasure
async function handleErasureRequest(email: string): Promise<ErasureReport> {
const report = {
documentsAffected: 0,
cannotDelete: [] as string[],
deleted: [] as string[],
};
const dsar = await handleDSAR(email);
for (const doc of dsar.documents) {
if (doc.status === "COMPLETED") {
report.cannotDelete.push(doc.documentId);
console.log(
`Cannot delete ${doc.documentId}: Legal retention required`
);
continue;
}
if (doc.status === "DRAFT" || doc.status === "CANCELLED") {
const client = getDocumensoClient();
await client.documents.deleteV0({ documentId: doc.documentId });
report..(doc.);
}
report.++;
}
report;
}
Encryption at Rest
import crypto from "crypto";
const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY!, "hex");
function encryptPII(plaintext: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-gcm", ENCRYPTION_KEY, iv);
let encrypted = cipher.update(plaintext, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag().toString("hex");
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
}
function decryptPII(ciphertext: string): string {
const [ivHex, authTagHex, encrypted] = ciphertext.split(":");
const iv = Buffer.from(ivHex, );
authTag = .(authTagHex, );
decipher = crypto.(, , iv);
decipher.(authTag);
decrypted = decipher.(encrypted, , );
decrypted += decipher.();
decrypted;
}
Output
- Signed documents securely stored
- PII properly protected
- Retention policies enforced
- GDPR compliance implemented
Error Handling
| Data Issue | Cause | Solution |
|---|
| Download failed | Document not complete | Check status first |
| Storage failed | Permissions | Check bucket policy |
| Decryption failed | Wrong key | Verify encryption key |
| DSAR incomplete | Pagination | Handle all pages |
Resources
Next Steps
For enterprise RBAC, see documenso-enterprise-rbac.