Skip to main content

Desktop App

The desktop app is built with Tauri 2, combining a Rust backend for security-critical operations with a React TypeScript frontend for the UI.

Design Principle: PDF Bytes Never Leave the Machine​

The React frontend handles user interaction -- file selection, signature placement, previews. All PDF manipulation happens in the Rust backend via Tauri commands. The only data that leaves the machine is:

  • Anchor ID (random, also printed in the QR code)
  • Composite hash (SHA-256 of the signing record)
  • Encrypted signing record
  • Previous signer's anchor ID (for chaining)

Rust Backend​

Module Structure​

src-tauri/src/
├── lib.rs App entry, plugin registration, deep link handling
├── state.rs AppState (API base URL, HTTP client, JWT)
├── anchor.rs Relay and verify API calls (RelayRequest, OnChainAnchor)
├── payload.rs Anchor ID and key generation, signing record, AES-128-GCM, QR URL
├── commands/
│ ├── auth.rs Keychain JWT/profile storage, open_auth_browser
│ ├── library.rs Persistent signature & text snippet storage
│ ├── pdf.rs Tauri commands: sign, verify, save, extract revision
│ └── documents.rs Document listing
└── pdf/
├── sign.rs Builds one signer's layer and checks the existing history
├── embed.rs Page content isolation, signature image, text field, and QR code embedding
├── chain.rs Signing history (/SignChainMeta) read and write
├── layers.rs Finds each signer's layer by its %%EOF
└── verify.rs Judges each signer's layer against the blockchain

Key Tauri Commands​

CommandDescription
open_auth_browserOpens auth provider URL in system browser
get_stored_jwtReads JWT from OS keychain
store_jwtSaves JWT to OS keychain
clear_stored_jwtRemoves JWT and profile from keychain
store_profileSaves signer profile JSON to keychain
get_stored_profileReads signer profile from keychain
load_libraryLoads all saved signatures and text snippets metadata
save_library_signatureSaves a signature PNG to app data directory
delete_library_signatureDeletes a saved signature
load_library_signatureLoads a signature PNG as base64
update_library_signature_labelRenames a saved signature
save_text_snippetSaves a reusable text snippet
delete_text_snippetDeletes a saved text snippet
get_sync_enabledReturns whether cloud sync is enabled
set_sync_enabledPersists cloud sync preference
open_pdf_pickerOpens OS file dialog, returns selected path
get_pdf_page_countReturns page count for a PDF
sign_documentFull signing pipeline (anchor ID and key, one layer with QR, hash, seal, anchor)
save_signed_pdfSave-as dialog for the signed PDF
verify_documentCheck every signer's layer against the blockchain; returns a VerificationReport
extract_revisionWrite the file as it was before anyone signed, or right after a given signer

PDF Processing Pipeline​

The signing pipeline uses lopdf for PDF manipulation:

  1. Check history -- Reads any existing /SignChainMeta history. Files in an earlier format are refused, and entries must link in order.
  2. Anchor ID and key -- Generates a random 16-byte anchor ID and AES-128-GCM key, and builds the QR URL from them
  3. IncrementalDocument -- Opens the PDF for one incremental update (preserving previous revisions)
  4. Clone page objects -- The catalog and stamped pages are copied into the new layer, with their Resources resolved and copied inline so existing fonts and images keep working
  5. Isolate page content -- Each stamped page's existing content is wrapped in q … Q, so a leftover transform, clip, or colour can't move or alter the stamps
  6. Embed signature -- PNG image added as XObject, placed via content stream operations
  7. Embed text fields -- Helvetica text rendered at specified coordinates
  8. Embed QR -- QR code image added as XObject with branding text
  9. Write history -- The signer's entry (anchor ID, previous anchor ID, QR URL, placements) is appended to /SignChainMeta, format version 3
  10. Save and hash -- The layer is saved once, and the document hash is the SHA-256 of exactly those bytes. Nothing is written to the file afterwards.
  11. Seal and anchor -- The signing record is built, encrypted, and sent to POST /api/relay

Verification Pipeline​

verify_document works on the file's bytes:

  1. Locate layers (layers.rs) -- Every save ends in %%EOF. A signer's layer is the first save whose history has that signer's entry last. If the layers can't be found in order, another app rewrote the file, and the result is rewritten. Files in an earlier format are legacy.
  2. Look up anchors -- Each signer's anchor ID is looked up with GET /api/verify/:anchorId
  3. Judge (verify.rs) -- For each signer, the record is decrypted with the key from the history entry's QR URL. It must hash to the on-chain composite hash, name the same anchor ID, link to the previous signer's anchor ID, and hold the SHA-256 of the file cut at that signer's layer end.

Signer details in the report come from the sealed record when it opens (detailsConfirmed). The file's own history is plain and editable, so it is only a fallback. A signer is flagged with changedBefore when another save sits between the previous signature and theirs. The document is changed-after-signing when anything other than whitespace follows the last signer's layer.

React Frontend​

State Management​

The app uses Zustand for state management with two stores:

useAuthStore -- Authentication state:

  • JWT token and decoded user claims (email, name, trust anchor, verified)
  • Loaded from OS keychain on startup via useAuthInit hook

useSigningStore -- Signing session state:

  • User identity (name, email, company, position)
  • Current file path, page count
  • Signature and text field placements
  • Signing progress (idle -> preparing -> embedding -> hashing -> anchoring -> finalising -> done)
  • Signed PDF path

useLibraryStore -- Persistent reusable assets:

  • Saved signatures (PNG images stored in app data directory)
  • Saved text snippets (name, address, company, etc.) with label, text, and font size
  • Loaded from disk on startup, persists across sessions
  • Cloud sync (opt-in): when enabled, local mutations are debounced and pushed to the API. On new devices, a CloudLibraryPrompt modal offers to download existing cloud library data

Routing​

Uses React Router with MemoryRouter (no browser URL bar in Tauri):

RoutePage
/identityAuthentication (magic link / OAuth) + profile setup
/dashboardDocument list (requires auth)
/uploadFile picker
/signSignature placement + signing
/verifyDocument verification
/document/:idDocument details
/libraryManage saved signatures and text snippets

A RequireAuth route guard redirects unauthenticated users to /identity. Authentication requires both a valid JWT and a completed signer profile.

PDF Rendering​

PDF preview uses pdfjs-dist with a centralized worker setup (src/lib/pdfjs.ts). Two viewer modes:

  • usePdfPreview -- Single-page preview with navigation
  • usePdfScrollViewer -- Scrollable multi-page view (used in signature placement)

File Associations​

The app registers as a PDF handler via Tauri's fileAssociations config. When a user opens a PDF with Veylent:

  • Linux/Windows: File path received as CLI argument
  • macOS: File path received via RunEvent::Opened

A chooser dialog offers "Sign with Veylent" or "Verify with Veylent".