Skip to content

Architecture

QSign is a containerized stack. A reverse proxy fronts a React single-page app and a Django REST backend; the backend is supported by a relational database, a cache, a search index, and an e-signature/DSC service.

┌────────────────────────────────────────────┐
│ Clients │
│ Browsers · API integrators · Signers │
└───────────────────────┬──────────────────────┘
│ HTTPS (443)
┌───────────▼───────────┐
│ nginx (reverse proxy)│ TLS termination
│ / → FE │ routing
│ /backend/* → API │
│ /esign* → esign │
└───┬─────────┬───────┬───┘
┌───────────────────┘ │ └────────────────┐
┌─────────▼─────────┐ ┌─────────────▼──────────┐ ┌─────────▼─────────┐
│ qsign-frontend │ │ qsign-backend │ │ esign / esign-api │
│ React SPA (:3000) │ │ Django + gunicorn │ │ signing + DSC / │
│ (static, served) │ │ REST API (:8000) │ │ tamper seal (TSA) │
└────────────────────┘ └───┬───────┬───────┬─────┘ └─────────┬─────────┘
│ │ │ │
┌─────────────────┘ │ └──────────┐ │
┌────────▼────────┐ ┌──────────▼──────┐ ┌───────▼──────┐ │
│ MySQL 8 │ │ Redis │ │ Solr 9 │ │
│ (app data) │ │ cache + throttle│ │ search index│ │
└──────────────────┘ └──────────────────┘ └──────────────┘ │
│ │
┌────────▼─────────────────────────────────────────────────────── ▼ ──┐
│ Document storage (volume) — originals, converted, signed, stamped │
└───────────────────────────────────────────────────────────────────────┘
Optional sidecars: ClamAV (upload AV scanning) · certbot (Let's Encrypt TLS)
Optional externals: Email (Zeptomail/Resend) · Aadhaar/Protean gateway · LLM · SMS · SSO
ContainerImage (genericized)RoleInternal port
nginxnginxTLS termination + reverse proxy + /health80 / 443
qsign-frontendqsign-frontend:<version>React SPA (static build, served on :3000)3000
qsign-backendqsign-backend:<version>Django REST API + gunicorn; document processing; cron8000
esignesign:<version>E-signature signing + DSC + RFC-3161 tamper-seal service8080
qsign-esign-apiesign-api:<version>API bridge to the esign service8090
dbmysql:8.0Primary relational datastore3306
redisredis:alpineCache + shared API-throttle counter + task broker6379
solrsolr:9.3.0Full-text document search index8983
clamav (optional)clamav/clamav:stableAntivirus scan of uploads before processing3310
certbot (optional)certbot/certbotAutomated Let’s Encrypt certificate renewal

Only the reverse proxy (nginx) needs to be reachable from outside the host. Every other container communicates over an internal Docker network by service name. Do not publish MySQL/Redis/Solr/esign ports to untrusted networks.

  • Framework: Django 4.2 (LTS) on Python 3.11, served by gunicorn.
  • Apps: site_user (auth, accounts, subscriptions/quota), contract (documents, signing workflows, envelopes, templates, Automagic), othercompanyapi (the white-label API), dsc_signatures (PKI/PKCS#11 + tamper seal), company, dashboard, notifications, plus deprecated legacy_* apps retained only for historical-data URLs.
  • Scheduled jobs (cron, in-container): reminder emails (daily), Aadhaar-quota refunds on cancelled/rejected docs, and subscription-state housekeeping.
  • Document processing: LibreOffice (Office→PDF), Ghostscript (PDF compression), Tesseract/ocrmypdf (OCR), PyMuPDF/pypdf (PDF manipulation), pyHanko (PDF CMS signing).
  • MySQL 8 — all application state (users, companies, documents metadata, recipients, signatures, plans/quotas, API accounts, call logs, webhook deliveries). UTF-8MB4.
  • Redis — two roles: a shared rate-limit/throttle counter for the API (fails open on outage, never blocks traffic) and a task/cache backend.
  • Solr 9 — full-text search index over documents; rebuildable from MySQL + storage.
  • Document storage volume — the binary document files (originals, converted PDFs, signed/stamped outputs). Local filesystem by default; optionally Azure Blob Storage.
  1. User uploads a document → (optional) ClamAV scan → stored; Office files converted to PDF; indexed in Solr.
  2. Signer fields placed (manual / preset / Automagic) and signers invited (email/SMS), or per-recipient signing links returned (headless / API).
  3. Each signer authenticates to the signing page, records ESIGN consent (timestamp + IP + user-agent), and applies their signature image.
  4. When all parties have signed, the backend calls the esign service to notarize: a CMS tamper seal is applied and the final PDF’s hash + timestamp + method are recorded. Signed copies are delivered/stored.
  1. The signer is redirected to the NSDL/Protean eSign gateway with a signed request.
  2. The signer authenticates via Aadhaar OTP; the gateway returns a signed response (PKCS#7) to the QSign callback.
  3. The backend validates the response signature and embeds the digital signature into the PDF. Quota is metered and refunded if the document is later cancelled/rejected. (Gateway hosts are allow-listed; the request/callback URLs are configured per ASP.)
  1. Prepare — pyHanko writes the PDF with a CMS placeholder and returns the document hash to sign (a short-lived signing session).
  2. Sign — the signer’s certificate/private key (hardware token via PKCS#11, driven by the cross-platform agent, or a software cert) signs the hash.
  3. Finalize — the backend splices the signature into the CMS structure, optionally adds an RFC-3161 timestamp, and stores the signed PDF.
  • External: 443/tcp (HTTPS) and 80/tcp (HTTP → redirect to HTTPS / ACME) on the nginx container only.
  • Internal Docker network: services reach each other by name — qsign-backend:8000, qsign-frontend:3000, db:3306, redis:6379, solr:8983, esign:8080, qsign-esign-api:8090.
  • Reverse-proxy routes: / → frontend; /backend/* → backend API (prefix stripped); /esign* and /qsign-esign-api/* → esign services; /health → liveness probe. Solr is not exposed externally in a hardened deployment.

A single host running Docker Compose is sufficient for small/medium volumes (see 03-system-requirements for sizing). The stack can be split across hosts (e.g., dedicated DB) or scaled horizontally behind a load balancer for HA; the backend is stateless apart from the shared MySQL/Redis/Solr/storage tiers.