<?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: RAVI TEJA SURAMPUDI</title>
    <description>The latest articles on DEV Community by RAVI TEJA SURAMPUDI (@tejasgit).</description>
    <link>https://dev.to/tejasgit</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3785754%2F8bc74828-a7cb-4df7-bf51-3a1098a7f3a7.jpeg</url>
      <title>DEV Community: RAVI TEJA SURAMPUDI</title>
      <link>https://dev.to/tejasgit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tejasgit"/>
    <language>en</language>
    <item>
      <title>Project Nylo: An open protocol for cross-domain analytics without third party cookies or fingerprinting</title>
      <dc:creator>RAVI TEJA SURAMPUDI</dc:creator>
      <pubDate>Mon, 23 Feb 2026 04:13:16 +0000</pubDate>
      <link>https://dev.to/tejasgit/project-nylo-an-open-protocol-for-cross-domain-analytics-without-third-party-cookies-or-19n</link>
      <guid>https://dev.to/tejasgit/project-nylo-an-open-protocol-for-cross-domain-analytics-without-third-party-cookies-or-19n</guid>
      <description>&lt;p&gt;Third-party cookies are dead. Safari killed them in 2017 (ITP), Firefox in 2019 (ETP), and Chrome's been slowly following with Privacy Sandbox. If you run analytics across multiple domains, Ex: A hospital site linking to a pharmacy portal, a bank linking to an investment dashboard, you've lost the ability to know the same visitor made both visits.&lt;/p&gt;

&lt;p&gt;The existing alternatives all have problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser fingerprinting&lt;/td&gt;
&lt;td&gt;Ethically questionable, increasingly blocked, legally risky under GDPR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login-based identity&lt;/td&gt;
&lt;td&gt;Forces authentication just to be counted and excludes anonymous visitors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First-party data sharing&lt;/td&gt;
&lt;td&gt;Requires business partnerships and PII exchange&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy Sandbox Topics API&lt;/td&gt;
&lt;td&gt;Chrome-only, advertising-focused, coarse-grained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adobe ECID&lt;/td&gt;
&lt;td&gt;Same eTLD+1 only, classified as personal data under GDPR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So I built &lt;strong&gt;Nylo&lt;/strong&gt;, an open-source SDK and protocol (&lt;strong&gt;WTX-1&lt;/strong&gt;) designed around a simple idea: &lt;strong&gt;you don't need to know who someone is to know they visited two pages.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works in 30 seconds
&lt;/h2&gt;

&lt;p&gt;The SDK generates a &lt;strong&gt;WaiTag&lt;/strong&gt;, a pseudonymous identifier made from 128 bits of cryptographic randomness plus a one-way domain hash. No personal information goes in, none comes out.&lt;/p&gt;

&lt;p&gt;When a user navigates from Domain A to Domain B:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Domain A requests a signed token from a verification server&lt;/li&gt;
&lt;li&gt;Server checks Domain B is authorized via DNS TXT record (like SPF for email)&lt;/li&gt;
&lt;li&gt;Token is appended as a &lt;strong&gt;URL hash fragment&lt;/strong&gt;: &lt;code&gt;destination.com/page#nylo_token=&amp;lt;token&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Domain B verifies the token server-side and restores the pseudonymous identity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight: &lt;strong&gt;hash fragments are never sent to the server&lt;/strong&gt; (RFC 3986 §3.5). No server logs, no &lt;code&gt;Referer&lt;/code&gt; headers, no network visibility. The token lives only in the browser, briefly, before cleanup.&lt;/p&gt;




&lt;h2&gt;
  
  
  The security model
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Early-cleanup inline script
&lt;/h3&gt;

&lt;p&gt;The biggest risk with hash fragments is the window between page load and SDK initialization during that time, any script can read &lt;code&gt;window.location.hash&lt;/code&gt;. To close this gap, an inline &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; script runs before everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nylo_token=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wai_token=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)){&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nylo_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wai_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__nylo_early_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nylo_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wai_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;){}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This executes synchronously before tag managers, analytics, or any third-party scripts. The token is stashed in a variable and the URL is cleaned via &lt;code&gt;history.replaceState()&lt;/code&gt;. By the time Google Tag Manager runs, the hash is empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest caveat:&lt;/strong&gt; The stashed variable is technically accessible to any script that knows its name. But &lt;code&gt;window.location.hash&lt;/code&gt; is something every script checks, an obscure variable name is not. And the token is one-time-use anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-time-use tokens with replay protection
&lt;/h3&gt;

&lt;p&gt;Every token gets a SHA-256 hash tracked server-side. Once verified, it's dead. Combined with 5-minute expiry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Captured tokens can't be replayed&lt;/li&gt;
&lt;li&gt;Shared URLs with tokens are harmless&lt;/li&gt;
&lt;li&gt;Browser history replay doesn't work&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Query params are opt-in only
&lt;/h3&gt;

&lt;p&gt;Hash fragments are the only transport by default. Query parameter fallback (for hash-routing SPAs) requires explicit opt-in. No accidental server-side token logging.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one limitation I can't fix
&lt;/h2&gt;

&lt;p&gt;Browser extensions with &lt;code&gt;"run_at": "document_start"&lt;/code&gt; execute before any page JavaScript — including the early-cleanup script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Browser parses HTML &amp;lt;head&amp;gt;
2. Extensions with "document_start" inject    ← extensions see the hash
3. Inline &amp;lt;head&amp;gt; scripts execute              ← early-cleanup runs here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is unfixable at the application layer. But it's a low-severity risk because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The user installed the extension.&lt;/strong&gt; If it has content script access, it can already read passwords, cookies, localStorage. So a pseudonymous token is the least valuable target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every web protocol has this.&lt;/strong&gt; OAuth codes, SAML assertions, JWTs are all seen by extensions. WTX-1's one-time-use + short expiry makes it harder to exploit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The token has no PII.&lt;/strong&gt; Even if captured, the extension gets a random string and a domain name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The extension has to race the SDK.&lt;/strong&gt; The SDK verifies within milliseconds. If the extension wins, the SDK simply fails and the user gets a fresh session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real fix would be a browser-native API which is one reason I've submitted this to the W3C Privacy Community Group.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in the spec
&lt;/h2&gt;

&lt;p&gt;The full WTX-1 protocol spec covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WaiTag format&lt;/strong&gt; — Cryptographic construction, what it is and isn't&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token transport&lt;/strong&gt; — Hash fragments, early-cleanup, opt-in query param fallback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification&lt;/strong&gt; — HMAC-SHA256, replay protection, 5-minute expiry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS authorization&lt;/strong&gt; — TXT records, subdomain inheritance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threat model&lt;/strong&gt; — Every attack vector with mitigations and residual risks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt; — GDPR pseudonymity analysis, data minimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consent API&lt;/strong&gt; — &lt;code&gt;setConsent({ analytics: true/false })&lt;/code&gt; with anonymous mode fallback&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/tejasgit/nylo
&lt;span class="nb"&gt;cd &lt;/span&gt;nylo/examples &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start
&lt;span class="c"&gt;# Open http://localhost:5000/demo.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero dependencies in the client SDK. Works in all modern browsers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub (SDK):&lt;/strong&gt; &lt;a href="https://github.com/tejasgit/nylo" rel="noopener noreferrer"&gt;github.com/tejasgit/nylo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub (Protocol):&lt;/strong&gt; &lt;a href="https://github.com/tejasgit/wtx-1" rel="noopener noreferrer"&gt;github.com/tejasgit/wtx-1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IETF Draft:&lt;/strong&gt; &lt;a href="https://datatracker.ietf.org/doc/draft-surampudi-wtx1/" rel="noopener noreferrer"&gt;draft-surampudi-wtx1-00&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;W3C Privacy CG:&lt;/strong&gt; proposal submitted for community review&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;I'm looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security review&lt;/strong&gt; — Anything I missed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy analysis&lt;/strong&gt; — Does the GDPR pseudonymity argument hold?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser vendor take&lt;/strong&gt; — Would hash fragment transport trigger ITP/ETP?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standards feedback&lt;/strong&gt; — Protocol solid enough for standardization?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to answer questions in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
