Browser-Based Document Signing Implementation
Browser-based document signing is a process where user views document, confirms their agreement, and signs without installing additional software. Covers wide spectrum: from simple electronic signature (click "Agree"), drawn signature to simple QES via SMS.
Levels of Electronic Signature
Simple ES — identity confirmation via login/password or SMS code. Legally weak, suitable for internal documents and agreements.
Enhanced non-qualified ES — created using keys but without certified CA certificate. Used in b2b with mutual EDI agreement.
Enhanced qualified ES (QES) — full legal effect. Requires certified SKZI.
Most "browser signing" scenarios are simple or enhanced non-qualified ES.
Signing Workflow
User opens document
↓
View PDF/HTML document
↓
Enter signature (draw, text, or SMS)
↓
Document hash + metadata → server
↓
Server creates signed version
↓
Notification + storage
PDF Viewing in Browser
Before signing, user must read document. Embed PDF viewer:
import { Viewer, Worker } from '@react-pdf-viewer/core';
import { defaultLayoutPlugin } from '@react-pdf-viewer/default-layout';
import '@react-pdf-viewer/core/lib/styles/index.css';
function DocumentViewer({ pdfUrl, onDocumentRead }) {
const [pagesRead, setPagesRead] = useState(new Set<number>());
const totalPagesRef = useRef(0);
const handlePageChange = ({ currentPage }: { currentPage: number }) => {
setPagesRead(prev => {
const updated = new Set(prev).add(currentPage);
if (updated.size >= totalPagesRef.current) {
onDocumentRead(); // Unlock sign button
}
return updated;
});
};
return (
<Worker workerUrl="/pdf.worker.min.js">
<Viewer
fileUrl={pdfUrl}
plugins={[defaultLayoutPlugin()]}
onPageChange={handlePageChange}
onDocumentLoad={({ doc }) => { totalPagesRef.current = doc.numPages; }}
/>
</Worker>
);
}
Applying Signature to PDF
After receiving user signature, embed it in PDF and create signed version:
// Backend: process signed document
async function processDocumentSignature(documentId, userId, signatureDataUrl, metadata) {
const document = await db.documents.findByPk(documentId);
// 1. Load original PDF
const originalPdf = await s3.getObject({ Bucket: BUCKET, Key: document.s3Key }).promise();
// 2. Compute hash for audit log
const originalHash = crypto.createHash('sha256').update(originalPdf.Body).digest('hex');
// 3. Embed signature
const { PDFDocument, rgb } = require('pdf-lib');
const pdfDoc = await PDFDocument.load(originalPdf.Body);
const lastPage = pdfDoc.getPages().at(-1);
// Add signature image
const signatureImage = await pdfDoc.embedPng(
Buffer.from(signatureDataUrl.replace(/^data:image\/png;base64,/, ''), 'base64')
);
lastPage.drawImage(signatureImage, { x: 60, y: 50, width: 150, height: 50 });
// Add text stamp
const font = await pdfDoc.embedFont('Helvetica');
lastPage.drawText(
`Signed: ${metadata.signerName}\n${metadata.signedAt.toISOString()}\nIP: ${metadata.ip}`,
{ x: 60, y: 30, size: 8, font, color: rgb(0.4, 0.4, 0.4) }
);
const signedPdfBytes = await pdfDoc.save();
// 4. Save signed version
const signedKey = `signed/${documentId}/${userId}.pdf`;
await s3.putObject({ Bucket: BUCKET, Key: signedKey, Body: signedPdfBytes }).promise();
// 5. Record in database
await db.documentSignatures.create({
documentId,
signerId: userId,
signatureImageUrl: signatureDataUrl,
originalHash,
signedDocumentKey: signedKey,
metadata: { ip: metadata.ip, userAgent: metadata.userAgent, signedAt: new Date() },
});
return signedKey;
}
SMS Confirmation as Simple ES
For legally significant agreements — phone number confirmation:
// Generate and send code
async function initiateSmsSigning(documentId, userId, phone) {
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
const codeHash = bcrypt.hashSync(code, 10);
await redis.setex(
`sms_sign:${documentId}:${userId}`,
300, // 5 minutes
JSON.stringify({ codeHash, phone, attempts: 0 })
);
await smsService.send(phone, `Document signing code: ${code}. Valid for 5 minutes.`);
}
// Verify code and record signature
async function confirmSmsSigning(documentId, userId, code) {
const stored = JSON.parse(await redis.get(`sms_sign:${documentId}:${userId}`));
if (!stored || !bcrypt.compareSync(code, stored.codeHash)) {
throw new Error('Invalid code');
}
await redis.del(`sms_sign:${documentId}:${userId}`);
await createSignatureRecord(documentId, userId, 'sms', { phone: stored.phone });
}
Multi-Signing
Documents often require signatures from multiple parties (contract between client and provider). Workflow:
- Party A signs → document gets
partially_signedstatus - Notification to Party B + signing link
- Party B signs → status
fully_signed - Both parties get final document via email
Timeframe
PDF viewing + drawn signature + PDF embedding + audit log — 4–5 days. SMS confirmation — 1–2 days. Multi-signing with workflow — 3–4 days.







