Vibecode DocHub
track this build5 phases, 15 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For DocHub, fill, annotate, request low-stakes signatures, and track document versions. The hard boundary is google integrations, hosted collaboration, signature workflow, and document delivery, plus document fidelity, identity, and compliance.
You are building a lean indie version of DocHub. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # DocHub · indie build Upload a document, place fields for the signer to fill, send a link, capture the signature with a full record of what was shown and agreed, produce a sealed output whose hash proves it has not changed, and keep an audit trail you could hand to someone who disputes it. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with PostgreSQL | signature records are consequential and want transactions and a real audit table | | Documents | pdf.js for display, a PDF library for filling and flattening | the document a signer sees must be exactly the one they sign | | Hashing | The original and the sealed output both hashed and recorded | the hash is what makes tampering detectable, which is most of the evidentiary value | | Audit | Append-only records with no update path | an audit trail that can be edited is not evidence | | Delivery | Signed, expiring links per signer | each signer gets their own link so the record attributes actions correctly | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the sealing pipeline. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: Audit records are append-only and consequential, which wants a real database. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **A check on whether this is acceptable for your documents** · free to read; an hour of advice if the documents matter - Why: Electronic signature acceptance differs by jurisdiction and document type, and some documents cannot be signed this way at all. Finding out after signing is expensive. - Get it: Check the position for the specific documents you intend to use this for, and record what you found and when. If it matters commercially, ask someone qualified. - Verify: A written note records the position and the date checked - [ ] **A transactional email provider with a verified domain** · free tiers cover this volume - Why: Signing links and sealed documents both arrive by email, and one in a spam folder is an unsigned agreement. - Get it: Create an account, add the DNS records, take the credentials. - Verify: A signing link arrives at three providers and is not in spam - [ ] **Real documents of the kind you will use** · free - Why: Field placement, flattening and rendering all behave differently on a form generated by a word processor, a scanned document and one produced by a design tool. - Get it: Take three real examples including at least one that already contains form fields, which is the case that breaks flattening. - Verify: All three upload, place fields and seal correctly ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Identity verification of any kind. - Qualified or advanced signatures under a specific legal framework. - Advice about whether this is acceptable for a given document. - Document storage suite integrations. - Google integrations, hosted collaboration, signature workflow, and document delivery - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem If one of those is essential to you, that is the reason to keep paying for DocHub, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · DocHub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me filling, annotating and low-stakes signing of documents, with an audit trail that is the actual product to replace DocHub. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A signature's legal weight comes from the evidence around it rather than from the image of a name. What makes an electronic signature defensible is the record: who signed, when, what they were shown, what they consented to, and that the document has not changed since. The picture is the least important part. So the audit trail is the product here, not a feature. And be honest about the limit: this verifies that someone with access to an email address signed, not that a particular person did. Identity verification is what the paid services add and it is not something you can build. Requirements also differ by jurisdiction and document type, and some documents cannot be signed this way at all. Check before relying on it. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · signature records are consequential and want transactions and a real audit table - Documents: pdf.js for display, a PDF library for filling and flattening · the document a signer sees must be exactly the one they sign - Hashing: The original and the sealed output both hashed and recorded · the hash is what makes tampering detectable, which is most of the evidentiary value - Audit: Append-only records with no update path · an audit trail that can be edited is not evidence - Delivery: Signed, expiring links per signer · each signer gets their own link so the record attributes actions correctly ### Data model (create this before Phase 1) - documents(id, name, original_key, original_hash, page_count, uploaded_at, uploader_id) - fields(id, document_id, page, x, y, w, h, kind, required, assigned_to_signer_id) · kind is text, date, checkbox or signature - envelopes(id, document_id, state, created_at, completed_at, sealed_key, sealed_hash) - signers(id, envelope_id, name, email, order, token, viewed_at, signed_at, ip_hash, user_agent) - field_values(field_id, signer_id, value, entered_at) - audit(id, envelope_id, at, actor, action, detail_json, document_hash) · append only, hash recorded with every entry - Every audit entry carries the document hash at that moment, so the record shows what was being looked at when each action happened. ### Phase 1 · A document that cannot change underneath anyone Build: Upload a document, hash it, and store it read-only. Render pages for placement. Any operation references the hash rather than the file path, so a substituted file is detectable. Trap: Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. Done when: An uploaded document is hashed and the hash is recorded. Replacing the stored file is detected on the next render and blocks it. The original is never modified, verified by checksum after a full workflow. Page rendering matches the document in another reader. Do not build yet: No fields, signers or signatures. ### Phase 2 · Fields placed in document coordinates Build: Place fields by clicking on a rendered page, storing coordinates relative to the page in document units rather than screen pixels. Assign each field to a signer. Mark required fields. Trap: Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. Done when: A field placed on a desktop appears in the same document position on a phone, verified by opening the signing link on one. Fields are assigned per signer and a signer sees only their own. A required field blocks completion until filled. Coordinates survive a zoom change during placement. Do not build yet: No sending or signing yet. ### Phase 3 · Signing, and recording what was shown Build: Each signer gets their own expiring link. Record when they opened it, which pages they viewed, the consent they were shown and agreed to, their network address and client, and the document hash at the time. Capture the signature itself last. Trap: A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. Done when: Opening a signing link records a view event with the document hash. The consent text shown is recorded verbatim with the agreement. Each signer's actions are attributed to their own link rather than pooled. A signer who opens and does not sign leaves a record of having viewed. Do not build yet: No sealing or verification yet. ### Phase 4 · Sealing, so nothing can change afterwards Build: On completion, flatten field values into the page content so they are not editable form fields, append a certificate page summarising the audit trail, hash the result and store it read-only. Provide a verification function taking a file and reporting whether it matches a recorded envelope. Trap: Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. Done when: The sealed output contains no editable form fields, verified with a tool rather than by eye. Its hash is recorded and a verification check on the unmodified file passes. Modifying a single byte causes verification to fail. The certificate page lists every signer, their timestamps and the document hash. Do not build yet: No delivery or storage policy yet. ### Phase 5 · Delivery and the copies everyone keeps Build: Email the sealed document and its certificate to every signer on completion. Record each delivery. Make the sealed copy retrievable from a link that survives, since a signed agreement is something people look for years later. Trap: A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. Done when: Every signer receives the sealed document and certificate on completion. Each delivery is recorded with its outcome. A retrieval link works long after the signing links have expired. A bounced delivery is surfaced rather than assumed successful. Do not build yet: Retention and jurisdiction notes come last. ### Phase 6 · Retention, jurisdiction and honest limits Build: A retention policy per document kind with its reason. Backups of documents and audit records together. A written statement of what this verifies and what it does not, shown to signers rather than only documented. Trap: This verifies that someone with access to an email address completed a signing flow. It does not verify identity, and presenting it as though it does misleads both parties. Say so in the signing flow itself, because the signer is the person who most needs to know what they are agreeing to. Done when: A retention policy is written per document kind with its reason and is applied. A restore reproduces sealed documents whose verification still passes. The signing flow states what is and is not verified. Records past retention are removed together with their documents, verified by searching afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - Identity verification. This proves someone with access to an email address signed, and the paid services add checks this build cannot. - Qualified or advanced electronic signatures under any specific legal framework, which require certificates and accredited providers. - Advice about whether an electronic signature is acceptable for a particular document, which varies by jurisdiction and document type and sometimes is not. - Integration with document storage suites, which is where the hosted product lives in most people's workflow. ### README must contain - That the audit trail is the product and the signature image is the least important part - That this verifies access to an email address rather than identity, and where that is stated to signers - That documents are hashed and a substituted file blocks rendering - That values are flattened on sealing and how to verify no editable fields remain - The retention policy per document kind and the date of the last restore ===== AGENTS.md ===== # Agent instructions · DocHub indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with PostgreSQL, pdf.js for display, a PDF library for filling and flattening, The original and the sealed output both hashed and recorded, Append-only records with no update path, Signed, expiring links per signer. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. ===== BUILD_PLAN.md ===== # Build plan · DocHub Upload a document, place fields for the signer to fill, send a link, capture the signature with a full record of what was shown and agreed, produce a sealed output whose hash proves it has not changed, and keep an audit trail you could hand to someone who disputes it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A document that cannot change underneath anyone The uploaded document is hashed and preserved, and every later step references that hash. ### Steps 1. Hash on upload and store read-only The hash is the identity from here on. The path is an implementation detail. Files: `src/documents.js` 2. Verify before every render Re-hash and compare. A mismatch blocks rather than warns. Files: `src/render.js` 3. Render pages for placement pdf.js at a known scale so field coordinates are reproducible. Files: `public/viewer.js` ### Done when - [ ] The document is hashed on upload - [ ] A replaced file is detected and blocks rendering - [ ] The original is unmodified after a full workflow - [ ] Rendered pages match another reader ### Watch out - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. ## Phase 2 · Fields placed in document coordinates A field lands in the same place for everyone, on any screen. ### Steps 1. Store page-relative coordinates Document units, not screen pixels. Convert at placement, once. Files: `src/fields.js` 2. Assign fields per signer So a signer sees and can fill only their own. This is also what makes the audit attribution correct. Files: `src/fields.js` 3. Verify across devices Place on a desktop, open on a phone. This is the check that catches pixel coordinates. ### Done when - [ ] A field appears in the same document position on a phone - [ ] A signer sees only their assigned fields - [ ] Required fields block completion - [ ] Coordinates survive a zoom change during placement ### Watch out - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. ## Phase 3 · Signing, and recording what was shown The record says what the signer saw and agreed to, not just that they signed. ### Steps 1. Issue per-signer links Expiring, unguessable, one per signer so attribution is unambiguous. Files: `src/envelopes.js` 2. Record the whole interaction Views, pages, consent text verbatim, address and client, with the document hash on each entry. Files: `src/audit.js` 3. Capture the signature last After the consent and the fields. The order is part of the record. Files: `src/sign.js` ### Done when - [ ] Opening a link records a view with the document hash - [ ] The consent text is recorded verbatim - [ ] Actions are attributed per signer - [ ] A viewer who does not sign still leaves a record ### Watch out - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. ## Phase 4 · Sealing, so nothing can change afterwards The completed document is fixed and any later alteration is detectable. ### Steps 1. Flatten values into content No editable fields remain. Inspect the output to confirm rather than trusting the call. Files: `src/seal.js` 2. Append a certificate page Signers, timestamps, addresses, consent and hashes. The human-readable half of the audit trail. Files: `src/certificate.js` 3. Provide verification A function taking a file and an envelope and reporting a match. Test with a one byte change. Files: `src/verify.js` ### Done when - [ ] The sealed output has no editable form fields - [ ] Verification passes on the unmodified file - [ ] A one byte change fails verification - [ ] The certificate page lists signers, timestamps and hashes ### Watch out - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. ## Phase 5 · Delivery and the copies everyone keeps Everyone who signed gets the sealed document and its record. ### Steps 1. Send the file, not only a link Attached, so the counterparty holds a copy independent of your system. Files: `src/deliver.js` 2. Record every delivery Outcome and timestamp. It is part of the record that they received it. Files: `src/deliver.js` 3. Keep a durable retrieval link Separate from signing links and long-lived. People come back years later. Files: `src/retrieve.js` ### Done when - [ ] Every signer receives the sealed document and certificate - [ ] Deliveries are recorded with outcomes - [ ] A retrieval link works after signing links expire - [ ] A bounce is surfaced ### Watch out - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. ## Not in this build - Identity verification of any kind. - Qualified or advanced signatures under a specific legal framework. - Advice about whether this is acceptable for a given document. - Document storage suite integrations. ## After v1, if you want it - Sequential signing order, so a second signer only receives a link after the first signs - Reusable templates with fields already placed for recurring agreements - A timestamp from an external service, which strengthens the record at a small cost ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Documents metadata and audit records. DATABASE_URL=postgres://user:pass@localhost:5432/sign # Required. Originals and sealed outputs, both read-only after writing. DOCUMENTS_DIR=./data/documents # Required. How long a signing link lasts. Long enough to be reasonable, short enough to expire. SIGNING_LINK_TTL_DAYS=14 # Required. How long a sealed document stays retrievable. People come back years later. RETRIEVAL_LINK_TTL_DAYS=3650 # Required. The consent shown to signers. Recorded verbatim with each signature, so changing it is a versioned event. CONSENT_TEXT_PATH=./config/consent.md # Required · secret. Transactional provider credentials. EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587 # Required. Default retention, per your written policy and its reason. RETENTION_YEARS_DEFAULT=7
You are building a lean indie version of DocHub. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # DocHub · indie build Upload a document, place fields for the signer to fill, send a link, capture the signature with a full record of what was shown and agreed, produce a sealed output whose hash proves it has not changed, and keep an audit trail you could hand to someone who disputes it. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with PostgreSQL | signature records are consequential and want transactions and a real audit table | | Documents | pdf.js for display, a PDF library for filling and flattening | the document a signer sees must be exactly the one they sign | | Hashing | The original and the sealed output both hashed and recorded | the hash is what makes tampering detectable, which is most of the evidentiary value | | Audit | Append-only records with no update path | an audit trail that can be edited is not evidence | | Delivery | Signed, expiring links per signer | each signer gets their own link so the record attributes actions correctly | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the sealing pipeline. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: Audit records are append-only and consequential, which wants a real database. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **A check on whether this is acceptable for your documents** · free to read; an hour of advice if the documents matter - Why: Electronic signature acceptance differs by jurisdiction and document type, and some documents cannot be signed this way at all. Finding out after signing is expensive. - Get it: Check the position for the specific documents you intend to use this for, and record what you found and when. If it matters commercially, ask someone qualified. - Verify: A written note records the position and the date checked - [ ] **A transactional email provider with a verified domain** · free tiers cover this volume - Why: Signing links and sealed documents both arrive by email, and one in a spam folder is an unsigned agreement. - Get it: Create an account, add the DNS records, take the credentials. - Verify: A signing link arrives at three providers and is not in spam - [ ] **Real documents of the kind you will use** · free - Why: Field placement, flattening and rendering all behave differently on a form generated by a word processor, a scanned document and one produced by a design tool. - Get it: Take three real examples including at least one that already contains form fields, which is the case that breaks flattening. - Verify: All three upload, place fields and seal correctly ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Identity verification of any kind. - Qualified or advanced signatures under a specific legal framework. - Advice about whether this is acceptable for a given document. - Document storage suite integrations. - Google integrations, hosted collaboration, signature workflow, and document delivery - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem If one of those is essential to you, that is the reason to keep paying for DocHub, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · DocHub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me filling, annotating and low-stakes signing of documents, with an audit trail that is the actual product to replace DocHub. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A signature's legal weight comes from the evidence around it rather than from the image of a name. What makes an electronic signature defensible is the record: who signed, when, what they were shown, what they consented to, and that the document has not changed since. The picture is the least important part. So the audit trail is the product here, not a feature. And be honest about the limit: this verifies that someone with access to an email address signed, not that a particular person did. Identity verification is what the paid services add and it is not something you can build. Requirements also differ by jurisdiction and document type, and some documents cannot be signed this way at all. Check before relying on it. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · signature records are consequential and want transactions and a real audit table - Documents: pdf.js for display, a PDF library for filling and flattening · the document a signer sees must be exactly the one they sign - Hashing: The original and the sealed output both hashed and recorded · the hash is what makes tampering detectable, which is most of the evidentiary value - Audit: Append-only records with no update path · an audit trail that can be edited is not evidence - Delivery: Signed, expiring links per signer · each signer gets their own link so the record attributes actions correctly ### Data model (create this before Phase 1) - documents(id, name, original_key, original_hash, page_count, uploaded_at, uploader_id) - fields(id, document_id, page, x, y, w, h, kind, required, assigned_to_signer_id) · kind is text, date, checkbox or signature - envelopes(id, document_id, state, created_at, completed_at, sealed_key, sealed_hash) - signers(id, envelope_id, name, email, order, token, viewed_at, signed_at, ip_hash, user_agent) - field_values(field_id, signer_id, value, entered_at) - audit(id, envelope_id, at, actor, action, detail_json, document_hash) · append only, hash recorded with every entry - Every audit entry carries the document hash at that moment, so the record shows what was being looked at when each action happened. ### Phase 1 · A document that cannot change underneath anyone Build: Upload a document, hash it, and store it read-only. Render pages for placement. Any operation references the hash rather than the file path, so a substituted file is detectable. Trap: Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. Done when: An uploaded document is hashed and the hash is recorded. Replacing the stored file is detected on the next render and blocks it. The original is never modified, verified by checksum after a full workflow. Page rendering matches the document in another reader. Do not build yet: No fields, signers or signatures. ### Phase 2 · Fields placed in document coordinates Build: Place fields by clicking on a rendered page, storing coordinates relative to the page in document units rather than screen pixels. Assign each field to a signer. Mark required fields. Trap: Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. Done when: A field placed on a desktop appears in the same document position on a phone, verified by opening the signing link on one. Fields are assigned per signer and a signer sees only their own. A required field blocks completion until filled. Coordinates survive a zoom change during placement. Do not build yet: No sending or signing yet. ### Phase 3 · Signing, and recording what was shown Build: Each signer gets their own expiring link. Record when they opened it, which pages they viewed, the consent they were shown and agreed to, their network address and client, and the document hash at the time. Capture the signature itself last. Trap: A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. Done when: Opening a signing link records a view event with the document hash. The consent text shown is recorded verbatim with the agreement. Each signer's actions are attributed to their own link rather than pooled. A signer who opens and does not sign leaves a record of having viewed. Do not build yet: No sealing or verification yet. ### Phase 4 · Sealing, so nothing can change afterwards Build: On completion, flatten field values into the page content so they are not editable form fields, append a certificate page summarising the audit trail, hash the result and store it read-only. Provide a verification function taking a file and reporting whether it matches a recorded envelope. Trap: Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. Done when: The sealed output contains no editable form fields, verified with a tool rather than by eye. Its hash is recorded and a verification check on the unmodified file passes. Modifying a single byte causes verification to fail. The certificate page lists every signer, their timestamps and the document hash. Do not build yet: No delivery or storage policy yet. ### Phase 5 · Delivery and the copies everyone keeps Build: Email the sealed document and its certificate to every signer on completion. Record each delivery. Make the sealed copy retrievable from a link that survives, since a signed agreement is something people look for years later. Trap: A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. Done when: Every signer receives the sealed document and certificate on completion. Each delivery is recorded with its outcome. A retrieval link works long after the signing links have expired. A bounced delivery is surfaced rather than assumed successful. Do not build yet: Retention and jurisdiction notes come last. ### Phase 6 · Retention, jurisdiction and honest limits Build: A retention policy per document kind with its reason. Backups of documents and audit records together. A written statement of what this verifies and what it does not, shown to signers rather than only documented. Trap: This verifies that someone with access to an email address completed a signing flow. It does not verify identity, and presenting it as though it does misleads both parties. Say so in the signing flow itself, because the signer is the person who most needs to know what they are agreeing to. Done when: A retention policy is written per document kind with its reason and is applied. A restore reproduces sealed documents whose verification still passes. The signing flow states what is and is not verified. Records past retention are removed together with their documents, verified by searching afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - Identity verification. This proves someone with access to an email address signed, and the paid services add checks this build cannot. - Qualified or advanced electronic signatures under any specific legal framework, which require certificates and accredited providers. - Advice about whether an electronic signature is acceptable for a particular document, which varies by jurisdiction and document type and sometimes is not. - Integration with document storage suites, which is where the hosted product lives in most people's workflow. ### README must contain - That the audit trail is the product and the signature image is the least important part - That this verifies access to an email address rather than identity, and where that is stated to signers - That documents are hashed and a substituted file blocks rendering - That values are flattened on sealing and how to verify no editable fields remain - The retention policy per document kind and the date of the last restore ===== AGENTS.md ===== # Agent instructions · DocHub indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with PostgreSQL, pdf.js for display, a PDF library for filling and flattening, The original and the sealed output both hashed and recorded, Append-only records with no update path, Signed, expiring links per signer. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. ===== BUILD_PLAN.md ===== # Build plan · DocHub Upload a document, place fields for the signer to fill, send a link, capture the signature with a full record of what was shown and agreed, produce a sealed output whose hash proves it has not changed, and keep an audit trail you could hand to someone who disputes it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A document that cannot change underneath anyone The uploaded document is hashed and preserved, and every later step references that hash. ### Steps 1. Hash on upload and store read-only The hash is the identity from here on. The path is an implementation detail. Files: `src/documents.js` 2. Verify before every render Re-hash and compare. A mismatch blocks rather than warns. Files: `src/render.js` 3. Render pages for placement pdf.js at a known scale so field coordinates are reproducible. Files: `public/viewer.js` ### Done when - [ ] The document is hashed on upload - [ ] A replaced file is detected and blocks rendering - [ ] The original is unmodified after a full workflow - [ ] Rendered pages match another reader ### Watch out - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. ## Phase 2 · Fields placed in document coordinates A field lands in the same place for everyone, on any screen. ### Steps 1. Store page-relative coordinates Document units, not screen pixels. Convert at placement, once. Files: `src/fields.js` 2. Assign fields per signer So a signer sees and can fill only their own. This is also what makes the audit attribution correct. Files: `src/fields.js` 3. Verify across devices Place on a desktop, open on a phone. This is the check that catches pixel coordinates. ### Done when - [ ] A field appears in the same document position on a phone - [ ] A signer sees only their assigned fields - [ ] Required fields block completion - [ ] Coordinates survive a zoom change during placement ### Watch out - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. ## Phase 3 · Signing, and recording what was shown The record says what the signer saw and agreed to, not just that they signed. ### Steps 1. Issue per-signer links Expiring, unguessable, one per signer so attribution is unambiguous. Files: `src/envelopes.js` 2. Record the whole interaction Views, pages, consent text verbatim, address and client, with the document hash on each entry. Files: `src/audit.js` 3. Capture the signature last After the consent and the fields. The order is part of the record. Files: `src/sign.js` ### Done when - [ ] Opening a link records a view with the document hash - [ ] The consent text is recorded verbatim - [ ] Actions are attributed per signer - [ ] A viewer who does not sign still leaves a record ### Watch out - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. ## Phase 4 · Sealing, so nothing can change afterwards The completed document is fixed and any later alteration is detectable. ### Steps 1. Flatten values into content No editable fields remain. Inspect the output to confirm rather than trusting the call. Files: `src/seal.js` 2. Append a certificate page Signers, timestamps, addresses, consent and hashes. The human-readable half of the audit trail. Files: `src/certificate.js` 3. Provide verification A function taking a file and an envelope and reporting a match. Test with a one byte change. Files: `src/verify.js` ### Done when - [ ] The sealed output has no editable form fields - [ ] Verification passes on the unmodified file - [ ] A one byte change fails verification - [ ] The certificate page lists signers, timestamps and hashes ### Watch out - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. ## Phase 5 · Delivery and the copies everyone keeps Everyone who signed gets the sealed document and its record. ### Steps 1. Send the file, not only a link Attached, so the counterparty holds a copy independent of your system. Files: `src/deliver.js` 2. Record every delivery Outcome and timestamp. It is part of the record that they received it. Files: `src/deliver.js` 3. Keep a durable retrieval link Separate from signing links and long-lived. People come back years later. Files: `src/retrieve.js` ### Done when - [ ] Every signer receives the sealed document and certificate - [ ] Deliveries are recorded with outcomes - [ ] A retrieval link works after signing links expire - [ ] A bounce is surfaced ### Watch out - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. ## Not in this build - Identity verification of any kind. - Qualified or advanced signatures under a specific legal framework. - Advice about whether this is acceptable for a given document. - Document storage suite integrations. ## After v1, if you want it - Sequential signing order, so a second signer only receives a link after the first signs - Reusable templates with fields already placed for recurring agreements - A timestamp from an external service, which strengthens the record at a small cost ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Documents metadata and audit records. DATABASE_URL=postgres://user:pass@localhost:5432/sign # Required. Originals and sealed outputs, both read-only after writing. DOCUMENTS_DIR=./data/documents # Required. How long a signing link lasts. Long enough to be reasonable, short enough to expire. SIGNING_LINK_TTL_DAYS=14 # Required. How long a sealed document stays retrievable. People come back years later. RETRIEVAL_LINK_TTL_DAYS=3650 # Required. The consent shown to signers. Recorded verbatim with each signature, so changing it is a versioned event. CONSENT_TEXT_PATH=./config/consent.md # Required · secret. Transactional provider credentials. EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587 # Required. Default retention, per your written policy and its reason. RETENTION_YEARS_DEFAULT=7
You are building a production product version of DocHub. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # DocHub · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For DocHub, fill, annotate, request low-stakes signatures, and track document versions. The hard boundary is google integrations, hosted collaboration, signature workflow, and document delivery, plus document fidelity, identity, and compliance. ## Product outcome A signing flow whose record is detailed enough to be useful if challenged, whose sealed documents are provably unchanged, and which tells signers plainly what it does and does not verify. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - LibreOffice headless - local or S3-compatible storage - public HTTPS for signature links ## Explicit non-goals for v1 - Identity verification of any kind. - Qualified or advanced signatures under a specific legal framework. - Advice about whether this is acceptable for a given document. - Document storage suite integrations. - Google integrations, hosted collaboration, signature workflow, and document delivery - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem ## Success criteria - A replaced document is confirmed to block rendering - Field positions are confirmed identical across devices - The sealed output is confirmed to contain no editable fields - A one byte change is confirmed to fail verification - The signing flow states what is not verified ===== BRIEF.md ===== # Build brief · DocHub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me filling, annotating and low-stakes signing of documents, with an audit trail that is the actual product to replace DocHub. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A signature's legal weight comes from the evidence around it rather than from the image of a name. What makes an electronic signature defensible is the record: who signed, when, what they were shown, what they consented to, and that the document has not changed since. The picture is the least important part. So the audit trail is the product here, not a feature. And be honest about the limit: this verifies that someone with access to an email address signed, not that a particular person did. Identity verification is what the paid services add and it is not something you can build. Requirements also differ by jurisdiction and document type, and some documents cannot be signed this way at all. Check before relying on it. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · signature records are consequential and want transactions and a real audit table - Documents: pdf.js for display, a PDF library for filling and flattening · the document a signer sees must be exactly the one they sign - Hashing: The original and the sealed output both hashed and recorded · the hash is what makes tampering detectable, which is most of the evidentiary value - Audit: Append-only records with no update path · an audit trail that can be edited is not evidence - Delivery: Signed, expiring links per signer · each signer gets their own link so the record attributes actions correctly ### Data model (create this before Phase 1) - documents(id, name, original_key, original_hash, page_count, uploaded_at, uploader_id) - fields(id, document_id, page, x, y, w, h, kind, required, assigned_to_signer_id) · kind is text, date, checkbox or signature - envelopes(id, document_id, state, created_at, completed_at, sealed_key, sealed_hash) - signers(id, envelope_id, name, email, order, token, viewed_at, signed_at, ip_hash, user_agent) - field_values(field_id, signer_id, value, entered_at) - audit(id, envelope_id, at, actor, action, detail_json, document_hash) · append only, hash recorded with every entry - Every audit entry carries the document hash at that moment, so the record shows what was being looked at when each action happened. ### Phase 1 · A document that cannot change underneath anyone Build: Upload a document, hash it, and store it read-only. Render pages for placement. Any operation references the hash rather than the file path, so a substituted file is detectable. Trap: Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. Done when: An uploaded document is hashed and the hash is recorded. Replacing the stored file is detected on the next render and blocks it. The original is never modified, verified by checksum after a full workflow. Page rendering matches the document in another reader. Do not build yet: No fields, signers or signatures. ### Phase 2 · Fields placed in document coordinates Build: Place fields by clicking on a rendered page, storing coordinates relative to the page in document units rather than screen pixels. Assign each field to a signer. Mark required fields. Trap: Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. Done when: A field placed on a desktop appears in the same document position on a phone, verified by opening the signing link on one. Fields are assigned per signer and a signer sees only their own. A required field blocks completion until filled. Coordinates survive a zoom change during placement. Do not build yet: No sending or signing yet. ### Phase 3 · Signing, and recording what was shown Build: Each signer gets their own expiring link. Record when they opened it, which pages they viewed, the consent they were shown and agreed to, their network address and client, and the document hash at the time. Capture the signature itself last. Trap: A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. Done when: Opening a signing link records a view event with the document hash. The consent text shown is recorded verbatim with the agreement. Each signer's actions are attributed to their own link rather than pooled. A signer who opens and does not sign leaves a record of having viewed. Do not build yet: No sealing or verification yet. ### Phase 4 · Sealing, so nothing can change afterwards Build: On completion, flatten field values into the page content so they are not editable form fields, append a certificate page summarising the audit trail, hash the result and store it read-only. Provide a verification function taking a file and reporting whether it matches a recorded envelope. Trap: Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. Done when: The sealed output contains no editable form fields, verified with a tool rather than by eye. Its hash is recorded and a verification check on the unmodified file passes. Modifying a single byte causes verification to fail. The certificate page lists every signer, their timestamps and the document hash. Do not build yet: No delivery or storage policy yet. ### Phase 5 · Delivery and the copies everyone keeps Build: Email the sealed document and its certificate to every signer on completion. Record each delivery. Make the sealed copy retrievable from a link that survives, since a signed agreement is something people look for years later. Trap: A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. Done when: Every signer receives the sealed document and certificate on completion. Each delivery is recorded with its outcome. A retrieval link works long after the signing links have expired. A bounced delivery is surfaced rather than assumed successful. Do not build yet: Retention and jurisdiction notes come last. ### Phase 6 · Retention, jurisdiction and honest limits Build: A retention policy per document kind with its reason. Backups of documents and audit records together. A written statement of what this verifies and what it does not, shown to signers rather than only documented. Trap: This verifies that someone with access to an email address completed a signing flow. It does not verify identity, and presenting it as though it does misleads both parties. Say so in the signing flow itself, because the signer is the person who most needs to know what they are agreeing to. Done when: A retention policy is written per document kind with its reason and is applied. A restore reproduces sealed documents whose verification still passes. The signing flow states what is and is not verified. Records past retention are removed together with their documents, verified by searching afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - Identity verification. This proves someone with access to an email address signed, and the paid services add checks this build cannot. - Qualified or advanced electronic signatures under any specific legal framework, which require certificates and accredited providers. - Advice about whether an electronic signature is acceptable for a particular document, which varies by jurisdiction and document type and sometimes is not. - Integration with document storage suites, which is where the hosted product lives in most people's workflow. ### README must contain - That the audit trail is the product and the signature image is the least important part - That this verifies access to an email address rather than identity, and where that is stated to signers - That documents are hashed and a substituted file blocks rendering - That values are flattened on sealing and how to verify no editable fields remain - The retention policy per document kind and the date of the last restore ===== ARCHITECTURE.md ===== # Architecture · DocHub ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with PostgreSQL | signature records are consequential and want transactions and a real audit table | | Documents | pdf.js for display, a PDF library for filling and flattening | the document a signer sees must be exactly the one they sign | | Hashing | The original and the sealed output both hashed and recorded | the hash is what makes tampering detectable, which is most of the evidentiary value | | Audit | Append-only records with no update path | an audit trail that can be edited is not evidence | | Delivery | Signed, expiring links per signer | each signer gets their own link so the record attributes actions correctly | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Documents | Hashing and immutability. | Any storage, provided the hash is the identity and is verified before rendering. | | Fields | Placement in document coordinates. | Any editor, provided coordinates are page relative rather than screen pixels. | | Audit | The append-only record. | Append-only is the point. A record with an update path is not evidence. | | Seal | Flattening and the certificate. | Any PDF library, provided the output contains no editable fields and the check verifies that. | | Delivery | Getting copies to everyone. | Any provider. Sending the file rather than only a link is the property to keep. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Documents metadata and audit records. - `DOCUMENTS_DIR` · required · Originals and sealed outputs, both read-only after writing. - `SIGNING_LINK_TTL_DAYS` · required · How long a signing link lasts. Long enough to be reasonable, short enough to expire. - `RETRIEVAL_LINK_TTL_DAYS` · required · How long a sealed document stays retrievable. People come back years later. - `CONSENT_TEXT_PATH` · required · The consent shown to signers. Recorded verbatim with each signature, so changing it is a versioned event. - `EMAIL_SERVER` · required, secret · Transactional provider credentials. - `RETENTION_YEARS_DEFAULT` · required · Default retention, per your written policy and its reason. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · DocHub product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with PostgreSQL, pdf.js for display, a PDF library for filling and flattening, The original and the sealed output both hashed and recorded, Append-only records with no update path, Signed, expiring links per signer. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. - This verifies that someone with access to an email address completed a signing flow. It does not verify identity, and presenting it as though it does misleads both parties. Say so in the signing flow itself, because the signer is the person who most needs to know what they are agreeing to. ===== MILESTONES.md ===== # Delivery milestones · DocHub Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A document that cannot change underneath anyone The uploaded document is hashed and preserved, and every later step references that hash. ### Steps 1. Hash on upload and store read-only The hash is the identity from here on. The path is an implementation detail. Files: `src/documents.js` 2. Verify before every render Re-hash and compare. A mismatch blocks rather than warns. Files: `src/render.js` 3. Render pages for placement pdf.js at a known scale so field coordinates are reproducible. Files: `public/viewer.js` ### Done when - [ ] The document is hashed on upload - [ ] A replaced file is detected and blocks rendering - [ ] The original is unmodified after a full workflow - [ ] Rendered pages match another reader ### Watch out - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. ## M2 · Fields placed in document coordinates A field lands in the same place for everyone, on any screen. ### Steps 1. Store page-relative coordinates Document units, not screen pixels. Convert at placement, once. Files: `src/fields.js` 2. Assign fields per signer So a signer sees and can fill only their own. This is also what makes the audit attribution correct. Files: `src/fields.js` 3. Verify across devices Place on a desktop, open on a phone. This is the check that catches pixel coordinates. ### Done when - [ ] A field appears in the same document position on a phone - [ ] A signer sees only their assigned fields - [ ] Required fields block completion - [ ] Coordinates survive a zoom change during placement ### Watch out - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. ## M3 · Signing, and recording what was shown The record says what the signer saw and agreed to, not just that they signed. ### Steps 1. Issue per-signer links Expiring, unguessable, one per signer so attribution is unambiguous. Files: `src/envelopes.js` 2. Record the whole interaction Views, pages, consent text verbatim, address and client, with the document hash on each entry. Files: `src/audit.js` 3. Capture the signature last After the consent and the fields. The order is part of the record. Files: `src/sign.js` ### Done when - [ ] Opening a link records a view with the document hash - [ ] The consent text is recorded verbatim - [ ] Actions are attributed per signer - [ ] A viewer who does not sign still leaves a record ### Watch out - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. ## M4 · Sealing, so nothing can change afterwards The completed document is fixed and any later alteration is detectable. ### Steps 1. Flatten values into content No editable fields remain. Inspect the output to confirm rather than trusting the call. Files: `src/seal.js` 2. Append a certificate page Signers, timestamps, addresses, consent and hashes. The human-readable half of the audit trail. Files: `src/certificate.js` 3. Provide verification A function taking a file and an envelope and reporting a match. Test with a one byte change. Files: `src/verify.js` ### Done when - [ ] The sealed output has no editable form fields - [ ] Verification passes on the unmodified file - [ ] A one byte change fails verification - [ ] The certificate page lists signers, timestamps and hashes ### Watch out - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. ## M5 · Delivery and the copies everyone keeps Everyone who signed gets the sealed document and its record. ### Steps 1. Send the file, not only a link Attached, so the counterparty holds a copy independent of your system. Files: `src/deliver.js` 2. Record every delivery Outcome and timestamp. It is part of the record that they received it. Files: `src/deliver.js` 3. Keep a durable retrieval link Separate from signing links and long-lived. People come back years later. Files: `src/retrieve.js` ### Done when - [ ] Every signer receives the sealed document and certificate - [ ] Deliveries are recorded with outcomes - [ ] A retrieval link works after signing links expire - [ ] A bounce is surfaced ### Watch out - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. ## M6 · Retention, jurisdiction and honest limits (production only) You keep records as long as you should, and everyone understands what this does not prove. ### Steps 1. Write and apply retention Per document kind, with the reason. Signed agreements often have a minimum retention rather than a maximum. Files: `docs/retention.md`, `src/retention.js` 2. Back up documents and audit together A sealed document without its audit trail loses most of its evidentiary value. Files: `scripts/backup.sh` 3. State the limit in the flow One sentence to the signer about what is verified. They are the party who most needs it. Files: `public/sign.html` ### Done when - [ ] Retention is written per document kind and applied - [ ] A restore preserves documents whose verification passes - [ ] The signing flow states what is not verified - [ ] Retention removes documents and their records together ### Watch out - This verifies that someone with access to an email address completed a signing flow. It does not verify identity, and presenting it as though it does misleads both parties. Say so in the signing flow itself, because the signer is the person who most needs to know what they are agreeing to. ===== OPERATIONS.md ===== # Operations · DocHub ## Backup Documents and audit records together, encrypted, with retention set from the obligation rather than convenience. A sealed document without its audit trail loses most of what makes it useful. ## Restore Restore both and run verification against a sealed document. A restore where the file opens but verification fails has lost the thing that mattered. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Delivery failures, envelopes stuck part-signed, and documents approaching retention expiry. A part-signed envelope that nobody chases is the common operational gap. ## Incident checklist If a signature is disputed, the audit trail is the answer, which is why it is append-only and why the consent text is stored verbatim. If verification fails on a document you hold, the file has changed and the recorded hash tells you it was after sealing. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A replaced document is confirmed to block rendering - [ ] Field positions are confirmed identical across devices - [ ] The sealed output is confirmed to contain no editable fields - [ ] A one byte change is confirmed to fail verification - [ ] The signing flow states what is not verified ## Launch constraint Do not market omitted DocHub capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Documents metadata and audit records. DATABASE_URL=postgres://user:pass@localhost:5432/sign # Required. Originals and sealed outputs, both read-only after writing. DOCUMENTS_DIR=./data/documents # Required. How long a signing link lasts. Long enough to be reasonable, short enough to expire. SIGNING_LINK_TTL_DAYS=14 # Required. How long a sealed document stays retrievable. People come back years later. RETRIEVAL_LINK_TTL_DAYS=3650 # Required. The consent shown to signers. Recorded verbatim with each signature, so changing it is a versioned event. CONSENT_TEXT_PATH=./config/consent.md # Required · secret. Transactional provider credentials. EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587 # Required. Default retention, per your written policy and its reason. RETENTION_YEARS_DEFAULT=7
# DocHub · indie build Upload a document, place fields for the signer to fill, send a link, capture the signature with a full record of what was shown and agreed, produce a sealed output whose hash proves it has not changed, and keep an audit trail you could hand to someone who disputes it. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with PostgreSQL | signature records are consequential and want transactions and a real audit table | | Documents | pdf.js for display, a PDF library for filling and flattening | the document a signer sees must be exactly the one they sign | | Hashing | The original and the sealed output both hashed and recorded | the hash is what makes tampering detectable, which is most of the evidentiary value | | Audit | Append-only records with no update path | an audit trail that can be edited is not evidence | | Delivery | Signed, expiring links per signer | each signer gets their own link so the record attributes actions correctly | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the sealing pipeline. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: Audit records are append-only and consequential, which wants a real database. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **A check on whether this is acceptable for your documents** · free to read; an hour of advice if the documents matter - Why: Electronic signature acceptance differs by jurisdiction and document type, and some documents cannot be signed this way at all. Finding out after signing is expensive. - Get it: Check the position for the specific documents you intend to use this for, and record what you found and when. If it matters commercially, ask someone qualified. - Verify: A written note records the position and the date checked - [ ] **A transactional email provider with a verified domain** · free tiers cover this volume - Why: Signing links and sealed documents both arrive by email, and one in a spam folder is an unsigned agreement. - Get it: Create an account, add the DNS records, take the credentials. - Verify: A signing link arrives at three providers and is not in spam - [ ] **Real documents of the kind you will use** · free - Why: Field placement, flattening and rendering all behave differently on a form generated by a word processor, a scanned document and one produced by a design tool. - Get it: Take three real examples including at least one that already contains form fields, which is the case that breaks flattening. - Verify: All three upload, place fields and seal correctly ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Identity verification of any kind. - Qualified or advanced signatures under a specific legal framework. - Advice about whether this is acceptable for a given document. - Document storage suite integrations. - Google integrations, hosted collaboration, signature workflow, and document delivery - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem If one of those is essential to you, that is the reason to keep paying for DocHub, and the README should say so rather than pretend.
# Build brief · DocHub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me filling, annotating and low-stakes signing of documents, with an audit trail that is the actual product to replace DocHub. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A signature's legal weight comes from the evidence around it rather than from the image of a name. What makes an electronic signature defensible is the record: who signed, when, what they were shown, what they consented to, and that the document has not changed since. The picture is the least important part. So the audit trail is the product here, not a feature. And be honest about the limit: this verifies that someone with access to an email address signed, not that a particular person did. Identity verification is what the paid services add and it is not something you can build. Requirements also differ by jurisdiction and document type, and some documents cannot be signed this way at all. Check before relying on it. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · signature records are consequential and want transactions and a real audit table - Documents: pdf.js for display, a PDF library for filling and flattening · the document a signer sees must be exactly the one they sign - Hashing: The original and the sealed output both hashed and recorded · the hash is what makes tampering detectable, which is most of the evidentiary value - Audit: Append-only records with no update path · an audit trail that can be edited is not evidence - Delivery: Signed, expiring links per signer · each signer gets their own link so the record attributes actions correctly ### Data model (create this before Phase 1) - documents(id, name, original_key, original_hash, page_count, uploaded_at, uploader_id) - fields(id, document_id, page, x, y, w, h, kind, required, assigned_to_signer_id) · kind is text, date, checkbox or signature - envelopes(id, document_id, state, created_at, completed_at, sealed_key, sealed_hash) - signers(id, envelope_id, name, email, order, token, viewed_at, signed_at, ip_hash, user_agent) - field_values(field_id, signer_id, value, entered_at) - audit(id, envelope_id, at, actor, action, detail_json, document_hash) · append only, hash recorded with every entry - Every audit entry carries the document hash at that moment, so the record shows what was being looked at when each action happened. ### Phase 1 · A document that cannot change underneath anyone Build: Upload a document, hash it, and store it read-only. Render pages for placement. Any operation references the hash rather than the file path, so a substituted file is detectable. Trap: Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. Done when: An uploaded document is hashed and the hash is recorded. Replacing the stored file is detected on the next render and blocks it. The original is never modified, verified by checksum after a full workflow. Page rendering matches the document in another reader. Do not build yet: No fields, signers or signatures. ### Phase 2 · Fields placed in document coordinates Build: Place fields by clicking on a rendered page, storing coordinates relative to the page in document units rather than screen pixels. Assign each field to a signer. Mark required fields. Trap: Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. Done when: A field placed on a desktop appears in the same document position on a phone, verified by opening the signing link on one. Fields are assigned per signer and a signer sees only their own. A required field blocks completion until filled. Coordinates survive a zoom change during placement. Do not build yet: No sending or signing yet. ### Phase 3 · Signing, and recording what was shown Build: Each signer gets their own expiring link. Record when they opened it, which pages they viewed, the consent they were shown and agreed to, their network address and client, and the document hash at the time. Capture the signature itself last. Trap: A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. Done when: Opening a signing link records a view event with the document hash. The consent text shown is recorded verbatim with the agreement. Each signer's actions are attributed to their own link rather than pooled. A signer who opens and does not sign leaves a record of having viewed. Do not build yet: No sealing or verification yet. ### Phase 4 · Sealing, so nothing can change afterwards Build: On completion, flatten field values into the page content so they are not editable form fields, append a certificate page summarising the audit trail, hash the result and store it read-only. Provide a verification function taking a file and reporting whether it matches a recorded envelope. Trap: Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. Done when: The sealed output contains no editable form fields, verified with a tool rather than by eye. Its hash is recorded and a verification check on the unmodified file passes. Modifying a single byte causes verification to fail. The certificate page lists every signer, their timestamps and the document hash. Do not build yet: No delivery or storage policy yet. ### Phase 5 · Delivery and the copies everyone keeps Build: Email the sealed document and its certificate to every signer on completion. Record each delivery. Make the sealed copy retrievable from a link that survives, since a signed agreement is something people look for years later. Trap: A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. Done when: Every signer receives the sealed document and certificate on completion. Each delivery is recorded with its outcome. A retrieval link works long after the signing links have expired. A bounced delivery is surfaced rather than assumed successful. Do not build yet: Retention and jurisdiction notes come last. ### Phase 6 · Retention, jurisdiction and honest limits Build: A retention policy per document kind with its reason. Backups of documents and audit records together. A written statement of what this verifies and what it does not, shown to signers rather than only documented. Trap: This verifies that someone with access to an email address completed a signing flow. It does not verify identity, and presenting it as though it does misleads both parties. Say so in the signing flow itself, because the signer is the person who most needs to know what they are agreeing to. Done when: A retention policy is written per document kind with its reason and is applied. A restore reproduces sealed documents whose verification still passes. The signing flow states what is and is not verified. Records past retention are removed together with their documents, verified by searching afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - Identity verification. This proves someone with access to an email address signed, and the paid services add checks this build cannot. - Qualified or advanced electronic signatures under any specific legal framework, which require certificates and accredited providers. - Advice about whether an electronic signature is acceptable for a particular document, which varies by jurisdiction and document type and sometimes is not. - Integration with document storage suites, which is where the hosted product lives in most people's workflow. ### README must contain - That the audit trail is the product and the signature image is the least important part - That this verifies access to an email address rather than identity, and where that is stated to signers - That documents are hashed and a substituted file blocks rendering - That values are flattened on sealing and how to verify no editable fields remain - The retention policy per document kind and the date of the last restore
# Agent instructions · DocHub indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with PostgreSQL, pdf.js for display, a PDF library for filling and flattening, The original and the sealed output both hashed and recorded, Append-only records with no update path, Signed, expiring links per signer. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links.
# Build plan · DocHub Upload a document, place fields for the signer to fill, send a link, capture the signature with a full record of what was shown and agreed, produce a sealed output whose hash proves it has not changed, and keep an audit trail you could hand to someone who disputes it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A document that cannot change underneath anyone The uploaded document is hashed and preserved, and every later step references that hash. ### Steps 1. Hash on upload and store read-only The hash is the identity from here on. The path is an implementation detail. Files: `src/documents.js` 2. Verify before every render Re-hash and compare. A mismatch blocks rather than warns. Files: `src/render.js` 3. Render pages for placement pdf.js at a known scale so field coordinates are reproducible. Files: `public/viewer.js` ### Done when - [ ] The document is hashed on upload - [ ] A replaced file is detected and blocks rendering - [ ] The original is unmodified after a full workflow - [ ] Rendered pages match another reader ### Watch out - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. ## Phase 2 · Fields placed in document coordinates A field lands in the same place for everyone, on any screen. ### Steps 1. Store page-relative coordinates Document units, not screen pixels. Convert at placement, once. Files: `src/fields.js` 2. Assign fields per signer So a signer sees and can fill only their own. This is also what makes the audit attribution correct. Files: `src/fields.js` 3. Verify across devices Place on a desktop, open on a phone. This is the check that catches pixel coordinates. ### Done when - [ ] A field appears in the same document position on a phone - [ ] A signer sees only their assigned fields - [ ] Required fields block completion - [ ] Coordinates survive a zoom change during placement ### Watch out - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. ## Phase 3 · Signing, and recording what was shown The record says what the signer saw and agreed to, not just that they signed. ### Steps 1. Issue per-signer links Expiring, unguessable, one per signer so attribution is unambiguous. Files: `src/envelopes.js` 2. Record the whole interaction Views, pages, consent text verbatim, address and client, with the document hash on each entry. Files: `src/audit.js` 3. Capture the signature last After the consent and the fields. The order is part of the record. Files: `src/sign.js` ### Done when - [ ] Opening a link records a view with the document hash - [ ] The consent text is recorded verbatim - [ ] Actions are attributed per signer - [ ] A viewer who does not sign still leaves a record ### Watch out - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. ## Phase 4 · Sealing, so nothing can change afterwards The completed document is fixed and any later alteration is detectable. ### Steps 1. Flatten values into content No editable fields remain. Inspect the output to confirm rather than trusting the call. Files: `src/seal.js` 2. Append a certificate page Signers, timestamps, addresses, consent and hashes. The human-readable half of the audit trail. Files: `src/certificate.js` 3. Provide verification A function taking a file and an envelope and reporting a match. Test with a one byte change. Files: `src/verify.js` ### Done when - [ ] The sealed output has no editable form fields - [ ] Verification passes on the unmodified file - [ ] A one byte change fails verification - [ ] The certificate page lists signers, timestamps and hashes ### Watch out - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. ## Phase 5 · Delivery and the copies everyone keeps Everyone who signed gets the sealed document and its record. ### Steps 1. Send the file, not only a link Attached, so the counterparty holds a copy independent of your system. Files: `src/deliver.js` 2. Record every delivery Outcome and timestamp. It is part of the record that they received it. Files: `src/deliver.js` 3. Keep a durable retrieval link Separate from signing links and long-lived. People come back years later. Files: `src/retrieve.js` ### Done when - [ ] Every signer receives the sealed document and certificate - [ ] Deliveries are recorded with outcomes - [ ] A retrieval link works after signing links expire - [ ] A bounce is surfaced ### Watch out - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. ## Not in this build - Identity verification of any kind. - Qualified or advanced signatures under a specific legal framework. - Advice about whether this is acceptable for a given document. - Document storage suite integrations. ## After v1, if you want it - Sequential signing order, so a second signer only receives a link after the first signs - Reusable templates with fields already placed for recurring agreements - A timestamp from an external service, which strengthens the record at a small cost
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Documents metadata and audit records. DATABASE_URL=postgres://user:pass@localhost:5432/sign # Required. Originals and sealed outputs, both read-only after writing. DOCUMENTS_DIR=./data/documents # Required. How long a signing link lasts. Long enough to be reasonable, short enough to expire. SIGNING_LINK_TTL_DAYS=14 # Required. How long a sealed document stays retrievable. People come back years later. RETRIEVAL_LINK_TTL_DAYS=3650 # Required. The consent shown to signers. Recorded verbatim with each signature, so changing it is a versioned event. CONSENT_TEXT_PATH=./config/consent.md # Required · secret. Transactional provider credentials. EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587 # Required. Default retention, per your written policy and its reason. RETENTION_YEARS_DEFAULT=7
# DocHub · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For DocHub, fill, annotate, request low-stakes signatures, and track document versions. The hard boundary is google integrations, hosted collaboration, signature workflow, and document delivery, plus document fidelity, identity, and compliance. ## Product outcome A signing flow whose record is detailed enough to be useful if challenged, whose sealed documents are provably unchanged, and which tells signers plainly what it does and does not verify. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - LibreOffice headless - local or S3-compatible storage - public HTTPS for signature links ## Explicit non-goals for v1 - Identity verification of any kind. - Qualified or advanced signatures under a specific legal framework. - Advice about whether this is acceptable for a given document. - Document storage suite integrations. - Google integrations, hosted collaboration, signature workflow, and document delivery - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem ## Success criteria - A replaced document is confirmed to block rendering - Field positions are confirmed identical across devices - The sealed output is confirmed to contain no editable fields - A one byte change is confirmed to fail verification - The signing flow states what is not verified
# Build brief · DocHub The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me filling, annotating and low-stakes signing of documents, with an audit trail that is the actual product to replace DocHub. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A signature's legal weight comes from the evidence around it rather than from the image of a name. What makes an electronic signature defensible is the record: who signed, when, what they were shown, what they consented to, and that the document has not changed since. The picture is the least important part. So the audit trail is the product here, not a feature. And be honest about the limit: this verifies that someone with access to an email address signed, not that a particular person did. Identity verification is what the paid services add and it is not something you can build. Requirements also differ by jurisdiction and document type, and some documents cannot be signed this way at all. Check before relying on it. ### Stack (fixed, do not substitute) - Runtime: Node 22 with PostgreSQL · signature records are consequential and want transactions and a real audit table - Documents: pdf.js for display, a PDF library for filling and flattening · the document a signer sees must be exactly the one they sign - Hashing: The original and the sealed output both hashed and recorded · the hash is what makes tampering detectable, which is most of the evidentiary value - Audit: Append-only records with no update path · an audit trail that can be edited is not evidence - Delivery: Signed, expiring links per signer · each signer gets their own link so the record attributes actions correctly ### Data model (create this before Phase 1) - documents(id, name, original_key, original_hash, page_count, uploaded_at, uploader_id) - fields(id, document_id, page, x, y, w, h, kind, required, assigned_to_signer_id) · kind is text, date, checkbox or signature - envelopes(id, document_id, state, created_at, completed_at, sealed_key, sealed_hash) - signers(id, envelope_id, name, email, order, token, viewed_at, signed_at, ip_hash, user_agent) - field_values(field_id, signer_id, value, entered_at) - audit(id, envelope_id, at, actor, action, detail_json, document_hash) · append only, hash recorded with every entry - Every audit entry carries the document hash at that moment, so the record shows what was being looked at when each action happened. ### Phase 1 · A document that cannot change underneath anyone Build: Upload a document, hash it, and store it read-only. Render pages for placement. Any operation references the hash rather than the file path, so a substituted file is detectable. Trap: Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. Done when: An uploaded document is hashed and the hash is recorded. Replacing the stored file is detected on the next render and blocks it. The original is never modified, verified by checksum after a full workflow. Page rendering matches the document in another reader. Do not build yet: No fields, signers or signatures. ### Phase 2 · Fields placed in document coordinates Build: Place fields by clicking on a rendered page, storing coordinates relative to the page in document units rather than screen pixels. Assign each field to a signer. Mark required fields. Trap: Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. Done when: A field placed on a desktop appears in the same document position on a phone, verified by opening the signing link on one. Fields are assigned per signer and a signer sees only their own. A required field blocks completion until filled. Coordinates survive a zoom change during placement. Do not build yet: No sending or signing yet. ### Phase 3 · Signing, and recording what was shown Build: Each signer gets their own expiring link. Record when they opened it, which pages they viewed, the consent they were shown and agreed to, their network address and client, and the document hash at the time. Capture the signature itself last. Trap: A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. Done when: Opening a signing link records a view event with the document hash. The consent text shown is recorded verbatim with the agreement. Each signer's actions are attributed to their own link rather than pooled. A signer who opens and does not sign leaves a record of having viewed. Do not build yet: No sealing or verification yet. ### Phase 4 · Sealing, so nothing can change afterwards Build: On completion, flatten field values into the page content so they are not editable form fields, append a certificate page summarising the audit trail, hash the result and store it read-only. Provide a verification function taking a file and reporting whether it matches a recorded envelope. Trap: Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. Done when: The sealed output contains no editable form fields, verified with a tool rather than by eye. Its hash is recorded and a verification check on the unmodified file passes. Modifying a single byte causes verification to fail. The certificate page lists every signer, their timestamps and the document hash. Do not build yet: No delivery or storage policy yet. ### Phase 5 · Delivery and the copies everyone keeps Build: Email the sealed document and its certificate to every signer on completion. Record each delivery. Make the sealed copy retrievable from a link that survives, since a signed agreement is something people look for years later. Trap: A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. Done when: Every signer receives the sealed document and certificate on completion. Each delivery is recorded with its outcome. A retrieval link works long after the signing links have expired. A bounced delivery is surfaced rather than assumed successful. Do not build yet: Retention and jurisdiction notes come last. ### Phase 6 · Retention, jurisdiction and honest limits Build: A retention policy per document kind with its reason. Backups of documents and audit records together. A written statement of what this verifies and what it does not, shown to signers rather than only documented. Trap: This verifies that someone with access to an email address completed a signing flow. It does not verify identity, and presenting it as though it does misleads both parties. Say so in the signing flow itself, because the signer is the person who most needs to know what they are agreeing to. Done when: A retention policy is written per document kind with its reason and is applied. A restore reproduces sealed documents whose verification still passes. The signing flow states what is and is not verified. Records past retention are removed together with their documents, verified by searching afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - Identity verification. This proves someone with access to an email address signed, and the paid services add checks this build cannot. - Qualified or advanced electronic signatures under any specific legal framework, which require certificates and accredited providers. - Advice about whether an electronic signature is acceptable for a particular document, which varies by jurisdiction and document type and sometimes is not. - Integration with document storage suites, which is where the hosted product lives in most people's workflow. ### README must contain - That the audit trail is the product and the signature image is the least important part - That this verifies access to an email address rather than identity, and where that is stated to signers - That documents are hashed and a substituted file blocks rendering - That values are flattened on sealing and how to verify no editable fields remain - The retention policy per document kind and the date of the last restore
# Architecture · DocHub ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with PostgreSQL | signature records are consequential and want transactions and a real audit table | | Documents | pdf.js for display, a PDF library for filling and flattening | the document a signer sees must be exactly the one they sign | | Hashing | The original and the sealed output both hashed and recorded | the hash is what makes tampering detectable, which is most of the evidentiary value | | Audit | Append-only records with no update path | an audit trail that can be edited is not evidence | | Delivery | Signed, expiring links per signer | each signer gets their own link so the record attributes actions correctly | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Documents | Hashing and immutability. | Any storage, provided the hash is the identity and is verified before rendering. | | Fields | Placement in document coordinates. | Any editor, provided coordinates are page relative rather than screen pixels. | | Audit | The append-only record. | Append-only is the point. A record with an update path is not evidence. | | Seal | Flattening and the certificate. | Any PDF library, provided the output contains no editable fields and the check verifies that. | | Delivery | Getting copies to everyone. | Any provider. Sending the file rather than only a link is the property to keep. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Documents metadata and audit records. - `DOCUMENTS_DIR` · required · Originals and sealed outputs, both read-only after writing. - `SIGNING_LINK_TTL_DAYS` · required · How long a signing link lasts. Long enough to be reasonable, short enough to expire. - `RETRIEVAL_LINK_TTL_DAYS` · required · How long a sealed document stays retrievable. People come back years later. - `CONSENT_TEXT_PATH` · required · The consent shown to signers. Recorded verbatim with each signature, so changing it is a versioned event. - `EMAIL_SERVER` · required, secret · Transactional provider credentials. - `RETENTION_YEARS_DEFAULT` · required · Default retention, per your written policy and its reason. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · DocHub product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with PostgreSQL, pdf.js for display, a PDF library for filling and flattening, The original and the sealed output both hashed and recorded, Append-only records with no update path, Signed, expiring links per signer. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. - This verifies that someone with access to an email address completed a signing flow. It does not verify identity, and presenting it as though it does misleads both parties. Say so in the signing flow itself, because the signer is the person who most needs to know what they are agreeing to.
# Delivery milestones · DocHub Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A document that cannot change underneath anyone The uploaded document is hashed and preserved, and every later step references that hash. ### Steps 1. Hash on upload and store read-only The hash is the identity from here on. The path is an implementation detail. Files: `src/documents.js` 2. Verify before every render Re-hash and compare. A mismatch blocks rather than warns. Files: `src/render.js` 3. Render pages for placement pdf.js at a known scale so field coordinates are reproducible. Files: `public/viewer.js` ### Done when - [ ] The document is hashed on upload - [ ] A replaced file is detected and blocks rendering - [ ] The original is unmodified after a full workflow - [ ] Rendered pages match another reader ### Watch out - Storing only a path means a file replaced on disk changes what a signer agreed to with no trace. Hash on upload, record the hash with every audit entry, and verify it before rendering to a signer, so a substitution is caught rather than assumed impossible. ## M2 · Fields placed in document coordinates A field lands in the same place for everyone, on any screen. ### Steps 1. Store page-relative coordinates Document units, not screen pixels. Convert at placement, once. Files: `src/fields.js` 2. Assign fields per signer So a signer sees and can fill only their own. This is also what makes the audit attribution correct. Files: `src/fields.js` 3. Verify across devices Place on a desktop, open on a phone. This is the check that catches pixel coordinates. ### Done when - [ ] A field appears in the same document position on a phone - [ ] A signer sees only their assigned fields - [ ] Required fields block completion - [ ] Coordinates survive a zoom change during placement ### Watch out - Storing field positions in screen pixels at the zoom you happened to be using puts them somewhere else on every other display and in the final output. Convert to page-relative document coordinates at placement, and verify by opening the same envelope on a phone. ## M3 · Signing, and recording what was shown The record says what the signer saw and agreed to, not just that they signed. ### Steps 1. Issue per-signer links Expiring, unguessable, one per signer so attribution is unambiguous. Files: `src/envelopes.js` 2. Record the whole interaction Views, pages, consent text verbatim, address and client, with the document hash on each entry. Files: `src/audit.js` 3. Capture the signature last After the consent and the fields. The order is part of the record. Files: `src/sign.js` ### Done when - [ ] Opening a link records a view with the document hash - [ ] The consent text is recorded verbatim - [ ] Actions are attributed per signer - [ ] A viewer who does not sign still leaves a record ### Watch out - A record containing only a name and a timestamp proves very little if the signature is disputed. What carries weight is the surrounding evidence: that this link was opened from this address at this time, that the document had this hash, that this consent text was displayed and accepted. Capture all of it, because none of it can be reconstructed later. ## M4 · Sealing, so nothing can change afterwards The completed document is fixed and any later alteration is detectable. ### Steps 1. Flatten values into content No editable fields remain. Inspect the output to confirm rather than trusting the call. Files: `src/seal.js` 2. Append a certificate page Signers, timestamps, addresses, consent and hashes. The human-readable half of the audit trail. Files: `src/certificate.js` 3. Provide verification A function taking a file and an envelope and reporting a match. Test with a one byte change. Files: `src/verify.js` ### Done when - [ ] The sealed output has no editable form fields - [ ] Verification passes on the unmodified file - [ ] A one byte change fails verification - [ ] The certificate page lists signers, timestamps and hashes ### Watch out - Leaving filled values as editable form fields means a recipient can change what was signed and it will look original, which defeats the whole exercise. Flatten on completion, and verify by inspecting the output for remaining form fields rather than by looking at it. ## M5 · Delivery and the copies everyone keeps Everyone who signed gets the sealed document and its record. ### Steps 1. Send the file, not only a link Attached, so the counterparty holds a copy independent of your system. Files: `src/deliver.js` 2. Record every delivery Outcome and timestamp. It is part of the record that they received it. Files: `src/deliver.js` 3. Keep a durable retrieval link Separate from signing links and long-lived. People come back years later. Files: `src/retrieve.js` ### Done when - [ ] Every signer receives the sealed document and certificate - [ ] Deliveries are recorded with outcomes - [ ] A retrieval link works after signing links expire - [ ] A bounce is surfaced ### Watch out - A signed document that only exists in your system is a document your counterparty does not have, and the first time they ask for it is the first time you discover the link expired. Send the file itself rather than only a link, and keep any link long-lived and separate from the signing links. ## M6 · Retention, jurisdiction and honest limits (production only) You keep records as long as you should, and everyone understands what this does not prove. ### Steps 1. Write and apply retention Per document kind, with the reason. Signed agreements often have a minimum retention rather than a maximum. Files: `docs/retention.md`, `src/retention.js` 2. Back up documents and audit together A sealed document without its audit trail loses most of its evidentiary value. Files: `scripts/backup.sh` 3. State the limit in the flow One sentence to the signer about what is verified. They are the party who most needs it. Files: `public/sign.html` ### Done when - [ ] Retention is written per document kind and applied - [ ] A restore preserves documents whose verification passes - [ ] The signing flow states what is not verified - [ ] Retention removes documents and their records together ### Watch out - This verifies that someone with access to an email address completed a signing flow. It does not verify identity, and presenting it as though it does misleads both parties. Say so in the signing flow itself, because the signer is the person who most needs to know what they are agreeing to.
# Operations · DocHub ## Backup Documents and audit records together, encrypted, with retention set from the obligation rather than convenience. A sealed document without its audit trail loses most of what makes it useful. ## Restore Restore both and run verification against a sealed document. A restore where the file opens but verification fails has lost the thing that mattered. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Delivery failures, envelopes stuck part-signed, and documents approaching retention expiry. A part-signed envelope that nobody chases is the common operational gap. ## Incident checklist If a signature is disputed, the audit trail is the answer, which is why it is append-only and why the consent text is stored verbatim. If verification fails on a document you hold, the file has changed and the recorded hash tells you it was after sealing. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A replaced document is confirmed to block rendering - [ ] Field positions are confirmed identical across devices - [ ] The sealed output is confirmed to contain no editable fields - [ ] A one byte change is confirmed to fail verification - [ ] The signing flow states what is not verified ## Launch constraint Do not market omitted DocHub capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Documents metadata and audit records. DATABASE_URL=postgres://user:pass@localhost:5432/sign # Required. Originals and sealed outputs, both read-only after writing. DOCUMENTS_DIR=./data/documents # Required. How long a signing link lasts. Long enough to be reasonable, short enough to expire. SIGNING_LINK_TTL_DAYS=14 # Required. How long a sealed document stays retrievable. People come back years later. RETRIEVAL_LINK_TTL_DAYS=3650 # Required. The consent shown to signers. Recorded verbatim with each signature, so changing it is a versioned event. CONSENT_TEXT_PATH=./config/consent.md # Required · secret. Transactional provider credentials. EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587 # Required. Default retention, per your written policy and its reason. RETENTION_YEARS_DEFAULT=7
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for DocHub because the visible editor is reproducible, but reliable rendering and legally defensible signature workflows are not a casual side project. The recurring cost buys format edge cases, fonts, rendering, encryption, signatures, evidence, retention, storage, backups, and legal requirements, not just the visible interface.
xGoogle integrations, hosted collaboration, signature workflow, and document delivery
xpixel-perfect proprietary PDF engine
xidentity verification
xqualified trust services
xlarge template and integration ecosystem
DocHub pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | 3 shared uses/month across core features; 2 document collaborators; 3 folders |
| basic | $9.24/user | $6.93/user | Selected Pro features share 3 uses/month; unlimited downloads, self-signing, templates and folders; 2 organizations; 5 team members |
| pro | $13.85/user | $10.39/user | Unlimited PDF editing, sign requests and envelopes; bulk send; in-person signing; AI editing/search tools; secure storage |
| site license | custom | — | Custom quote; unlimited users; organizations and rooms; HIPAA BAA, SSO and priority support |
free tier3 shared uses/month across core editing/signing/template/envelope/link features; 2 collaborators; 3 folders
billingmonthly + annual (25% annual discount); 30-day Pro trial; prices shown in EUR and converted to USD
hidden costsEach teammate needs a paid license for Pro features even though an organization may contain unlimited members. Taxes and final local currency are determined at checkout. Nonprofits get 20% off, with an additional 30% discount for annual payment.
verified 2026-08-13 · source ↗
Vibecode DocHub
Kinda. The core of DocHub is buildable in a weekend with the prompt on this page, but there are real gaps: Google integrations, hosted collaboration, signature workflow, and document delivery, pixel-perfect proprietary PDF engine. Read the honest list above before committing.
How much does DocHub cost?
DocHub costs about $13.85/month (Pro, checked 2026-08-13), which is $166.2 per year.
What do I lose by replacing DocHub?
Honestly: Google integrations, hosted collaboration, signature workflow, and document delivery; pixel-perfect proprietary PDF engine; identity verification; qualified trust services; large template and integration ecosystem. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to DocHub?
Yes: Stirling PDF (Active open-source web application for a broad set of PDF operations.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.