<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abhinay Gokul Pulla</title>
    <description>The latest articles on DEV Community by Abhinay Gokul Pulla (@abhnaygokul).</description>
    <link>https://dev.to/abhnaygokul</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4077753%2Fc431ff3c-9a1d-4ee4-b65a-b6f1b6cfed86.jpeg</url>
      <title>DEV Community: Abhinay Gokul Pulla</title>
      <link>https://dev.to/abhnaygokul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhnaygokul"/>
    <language>en</language>
    <item>
      <title>Why a print job should never become a permanent cloud document</title>
      <dc:creator>Abhinay Gokul Pulla</dc:creator>
      <pubDate>Fri, 14 Aug 2026 13:31:57 +0000</pubDate>
      <link>https://dev.to/abhnaygokul/why-a-print-job-should-never-become-a-permanent-cloud-document-43bg</link>
      <guid>https://dev.to/abhnaygokul/why-a-print-job-should-never-become-a-permanent-cloud-document-43bg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgwsx2alsdp1y0exmnck.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgwsx2alsdp1y0exmnck.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;em&gt;How Docshy keeps files as ciphertext from phone to printer — and removes our copy when the job ends.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem in one sentence
&lt;/h2&gt;

&lt;p&gt;Print shops need a &lt;strong&gt;temporary&lt;/strong&gt; document. Messaging apps give them a &lt;strong&gt;permanent&lt;/strong&gt; one. Docshy is a &lt;strong&gt;relay&lt;/strong&gt;: encrypt → store briefly → decrypt only for print → shred.&lt;/p&gt;

&lt;p&gt;This post is the architecture behind that relay as it runs in production today (&lt;code&gt;docshy.com&lt;/code&gt;, GCP, region &lt;strong&gt;asia-south1 / Mumbai&lt;/strong&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  High-level topology
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  C[Customer phone / app] --&amp;gt;|AES-256-GCM encrypt| U[Ciphertext upload]
  U --&amp;gt; API[Cloud Run API]
  API --&amp;gt; GCS[(Encrypted object storage)]
  API --&amp;gt; R[(Redis — short-lived job state)]
  API --&amp;gt; FS[(Control-plane metadata)]
  API --&amp;gt; KMS[Cloud KMS]
  API --&amp;gt;|Authorized print| S[Shop dashboard]
  S --&amp;gt;|Print| P[Paper]
  S --&amp;gt;|Job done| X[Shred our copy]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Design rule:&lt;/strong&gt; the platform is a &lt;strong&gt;mere conduit&lt;/strong&gt;. We optimize for short-lived encrypted blobs and explicit deletion — not a document archive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actors and trust boundaries
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;th&gt;Runs where&lt;/th&gt;
&lt;th&gt;Trust assumption&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Customer app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser / native shell loading docshy.com&lt;/td&gt;
&lt;td&gt;Encrypts before upload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cloud Run&lt;/td&gt;
&lt;td&gt;Stores ciphertext; decrypts only on authorized print&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shop dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser + shop auth&lt;/td&gt;
&lt;td&gt;Sees plaintext &lt;strong&gt;at print time&lt;/strong&gt; (required to print)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Object storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Blind bucket&lt;/td&gt;
&lt;td&gt;Ciphertext only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shared job / queue store&lt;/td&gt;
&lt;td&gt;Short TTLs — not the durable file store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KMS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GCP&lt;/td&gt;
&lt;td&gt;Wraps per-file data keys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Honest boundary:&lt;/strong&gt; zero retention on &lt;strong&gt;our&lt;/strong&gt; side ≠ zero visibility at the counter. The shop must render the page to print. Screenshots and photos of paper remain physical-world risks. Architecture removes the WhatsApp-forever + cloud-library path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crypto: double envelope
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1 — Client (privacy layer)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm:&lt;/strong&gt; AES-256-GCM in the browser (Web Crypto)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When:&lt;/strong&gt; Before the upload leaves the device&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why:&lt;/strong&gt; Readable IDs/PDFs are not the default thing we receive from the customer path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Larger files use a chunked format so mobile browsers stay stable; small files encrypt in one shot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2 — Platform (infrastructure layer)
&lt;/h3&gt;

&lt;p&gt;After the upload is accepted:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a per-object &lt;strong&gt;data encryption key (DEK)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Encrypt the payload again for storage (AES-GCM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap&lt;/strong&gt; the DEK with &lt;strong&gt;Cloud KMS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Write ciphertext to object storage&lt;/li&gt;
&lt;li&gt;Keep short-lived job metadata (and wrapped key material) in &lt;strong&gt;Redis&lt;/strong&gt;; mirror control data in Firestore where needed for ops&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Property we care about:&lt;/strong&gt; dumping job metadata or listing the bucket &lt;strong&gt;without KMS&lt;/strong&gt; does not yield plaintext PDFs. GCM tags also fail decrypt if ciphertext is tampered with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request path: upload → queue → print → shred
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Bind to a shop
&lt;/h3&gt;

&lt;p&gt;Customer scans the shop’s &lt;strong&gt;static counter QR&lt;/strong&gt; (HMAC-authenticated, shop-bound). That is the production default for busy counters. Legacy rotating tokens are still accepted where older dashboards mint them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Upload
&lt;/h3&gt;

&lt;p&gt;Customer encrypts on device → uploads ciphertext with shop binding → platform re-envelopes for storage → a print job lands in the shop queue. Soft-launch caps keep a visit to a small file set (scan again for the next set).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Shop queue
&lt;/h3&gt;

&lt;p&gt;Authenticated shop owners see jobs for &lt;strong&gt;their&lt;/strong&gt; shop only. Live queue state sits in &lt;strong&gt;Redis&lt;/strong&gt; so multiple Cloud Run instances stay consistent; Firestore can reconcile when the hot set is empty.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Print (momentary plaintext)
&lt;/h3&gt;

&lt;p&gt;On an authorized print action: verify shop ownership → unwrap DEK via KMS → stream decrypt to the shop browser for print.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;only intentional plaintext window&lt;/strong&gt; on the server/shop path. Printers need pixels.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Shred
&lt;/h3&gt;

&lt;p&gt;When the job completes (or via cleanup):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delete the encrypted object&lt;/li&gt;
&lt;li&gt;Clear job / queue / related meta&lt;/li&gt;
&lt;li&gt;Optionally notify the customer that it’s printed and gone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A scheduled sweep removes aged or already-printed leftovers so failures don’t leave ciphertext for days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Shred”&lt;/strong&gt; means delete from our stores and clear pointers — not marketing-speak for wiping every disk sector. The guarantee: &lt;strong&gt;no retained product object&lt;/strong&gt; after success or expiry policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  State: what lives where
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Store&lt;/th&gt;
&lt;th&gt;Holds&lt;/th&gt;
&lt;th&gt;Lifetime intent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Object storage (GCS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Encrypted print payloads&lt;/td&gt;
&lt;td&gt;Until shred or policy expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Queues, job records, short relay temps, shop QR / session counters&lt;/td&gt;
&lt;td&gt;Minutes–hours (TTLs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Control plane DB (Firestore)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Jobs mirror, shop registry, usage&lt;/td&gt;
&lt;td&gt;Ops/audit — &lt;strong&gt;not&lt;/strong&gt; a second copy of file bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KMS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Key-encryption key for DEK wrap&lt;/td&gt;
&lt;td&gt;Long-lived infra key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Redis = brain of the live job. Object storage = blind bag of ciphertext. Confusing those two is how architecture posts go wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth and abuse controls (architecture-relevant)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Shop APIs require verified shop ownership&lt;/li&gt;
&lt;li&gt;Customer upload requires a valid shop QR binding&lt;/li&gt;
&lt;li&gt;Rate limits and strict browser security headers on the edge&lt;/li&gt;
&lt;li&gt;Signed shop tokens so QR codes can’t be trivially forged&lt;/li&gt;
&lt;li&gt;Liveness vs readiness: the process can be “up” while still reporting &lt;strong&gt;not ready&lt;/strong&gt; if Redis is down (fail closed at scale)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  DPDPA angle (technical, not legal advice)
&lt;/h2&gt;

&lt;p&gt;DPDPA stresses &lt;strong&gt;purpose&lt;/strong&gt; and not keeping personal data longer than needed. For a print job, purpose ≈ &lt;strong&gt;print&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client + server ciphertext&lt;/td&gt;
&lt;td&gt;We don’t casually store readable docs by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short-lived objects + shred&lt;/td&gt;
&lt;td&gt;Post-job retention on &lt;strong&gt;our&lt;/strong&gt; storage is designed out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shop dashboard instead of WhatsApp&lt;/td&gt;
&lt;td&gt;Stops “staff phone gallery forever” as the transport&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mumbai region&lt;/td&gt;
&lt;td&gt;Data path stays in-India for latency and trust&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What it &lt;strong&gt;does not&lt;/strong&gt; do: replace shop policy, stop someone photographing the screen, or act as a regulator-issued compliance certificate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure and cleanup modes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Print never confirmed&lt;/td&gt;
&lt;td&gt;TTLs + sweep still remove aged ciphertext/meta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis unavailable&lt;/td&gt;
&lt;td&gt;Multi-instance consistency degrades; readiness fails closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial shred&lt;/td&gt;
&lt;td&gt;Cleanup reconciles printed/expired objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KMS unavailable&lt;/td&gt;
&lt;td&gt;Decrypt/print fails closed — no plaintext fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What we’re hardening next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Richer &lt;strong&gt;session ledger&lt;/strong&gt; and shop settlement visibility (visit caps already live; full counter UI still maturing)&lt;/li&gt;
&lt;li&gt;Stronger &lt;strong&gt;audit visibility&lt;/strong&gt; of upload → print → shred for operators&lt;/li&gt;
&lt;li&gt;Optional &lt;strong&gt;B2B&lt;/strong&gt; surfaces for orgs that already own document workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Docshy’s production architecture is intentionally boring in the right places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Encrypt on device (AES-256-GCM)&lt;/li&gt;
&lt;li&gt;Re-envelope for storage (AES-GCM + Cloud KMS)&lt;/li&gt;
&lt;li&gt;Blind object storage + short-lived job state in Redis&lt;/li&gt;
&lt;li&gt;Static shop QR to bind upload to a counter&lt;/li&gt;
&lt;li&gt;Decrypt only on authorized print&lt;/li&gt;
&lt;li&gt;Delete ciphertext and clear job state when done (plus sweep)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the technical meaning of our zero-retention print relay: &lt;strong&gt;ciphertext for a short job, then removal — not a document CMS.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Abhinay Gokul Pulla&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Founder &amp;amp; Director — Perceptova Intelligence Private Limited&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docshy.com" rel="noopener noreferrer"&gt;docshy.com&lt;/a&gt; · &lt;a href="https://perceptova.com" rel="noopener noreferrer"&gt;perceptova.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>webdev</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
