<?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: Ilya-hye</title>
    <description>The latest articles on DEV Community by Ilya-hye (@ilyahye).</description>
    <link>https://dev.to/ilyahye</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%2F4002519%2Fd7a0c341-e3c9-4451-8b97-0b2788e0d020.png</url>
      <title>DEV Community: Ilya-hye</title>
      <link>https://dev.to/ilyahye</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ilyahye"/>
    <language>en</language>
    <item>
      <title>Stateless auth without a database round-trip: how Bondify's proof model works</title>
      <dc:creator>Ilya-hye</dc:creator>
      <pubDate>Thu, 02 Jul 2026 16:47:33 +0000</pubDate>
      <link>https://dev.to/ilyahye/stateless-auth-without-a-database-round-trip-how-bondifys-proof-model-works-20hm</link>
      <guid>https://dev.to/ilyahye/stateless-auth-without-a-database-round-trip-how-bondifys-proof-model-works-20hm</guid>
      <description>&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;Most "login with X" integrations work the same way: your server gets a token, and to find out if it's real, it has to ask the provider. That round-trip is one more network call on your auth hot path, one more thing that can time out, and one more dependency between your uptime and theirs.&lt;/p&gt;

&lt;p&gt;Bondify doesn't do that. Verification is &lt;strong&gt;local&lt;/strong&gt; — a single HMAC check against a secret only you and Bondify know. No call back to &lt;code&gt;api.bondify.dev&lt;/code&gt; is required to trust a login. Here's how that's possible, and what it buys you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a session&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Your client calls the public generate endpoint (or your server calls the secret-key version). Bondify returns a &lt;code&gt;deeplink&lt;/code&gt;, a &lt;code&gt;session_token&lt;/code&gt;, and an &lt;code&gt;expires_at&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open Telegram&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The client opens the deep link — &lt;code&gt;https://t.me/&amp;lt;your_bot&amp;gt;?start=&amp;lt;session_token&amp;gt;&lt;/code&gt; — and the user lands in your bot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confirm in Telegram&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The user taps &lt;strong&gt;Confirm&lt;/strong&gt;. Bondify marks the session &lt;code&gt;confirmed&lt;/code&gt; and attaches the Telegram identity to it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Detect confirmation&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Your client polls verify (the SDK does this for you), or your server polls it, or — better — you skip polling entirely and let a webhook push the result to you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify on your backend&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The verify response includes a signed &lt;strong&gt;proof&lt;/strong&gt;. Your server checks the signature locally. If it's valid, you mint your own session — cookie, JWT, whatever you already use.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 5 is the part worth dwelling on, because it's where the "no round-trip" property actually comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's inside a proof
&lt;/h2&gt;

&lt;p&gt;A proof is a JWT, signed with &lt;code&gt;HS256&lt;/code&gt;. Decode it and you get something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"telegram_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"telegram_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada Lovelace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"telegram_username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"project_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"proj_xxxxxxxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4e5f6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bondify"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1700000123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1700000423&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exotic. The interesting part isn't the payload — it's the signature. The token is signed with your project's &lt;strong&gt;webhook secret&lt;/strong&gt; (&lt;code&gt;whsec_…&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HS256(header.payload, webhook_secret)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The proof is signed with the &lt;strong&gt;webhook secret&lt;/strong&gt;, not the secret key (&lt;code&gt;sk_…&lt;/code&gt;). The same &lt;code&gt;whsec_…&lt;/code&gt; verifies both proofs and webhook signatures, so your backend only needs to manage one secret for both flows.&lt;/p&gt;

&lt;p&gt;Because only your server and Bondify know &lt;code&gt;whsec_…&lt;/code&gt;, a valid signature is proof (hence the name) that Bondify issued the token and that nobody tampered with the payload in transit. Your server can check this with one line of JWT-library code and zero network calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BondifyServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@bondify/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bondify&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;BondifyServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;jwtSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BONDIFY_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bondify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// user.telegram_id, user.telegram_name, user.telegram_username&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire trust boundary. No HTTP client, no timeout handling, no "what if Bondify's API is slow right now" — just a synchronous cryptographic check that either passes or throws.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it sounds like it should
&lt;/h2&gt;

&lt;p&gt;It's tempting to read "saves one HTTP call" and shrug. In practice it changes three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency is bounded by your own infrastructure, not ours.&lt;/strong&gt; Your login endpoint's p99 no longer includes a leg to a third-party API. If Bondify has a bad five minutes, sessions that already reached confirmed still verify — because verification never asked Bondify in the first place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's one fewer thing that can go down in your critical path.&lt;/strong&gt; A network partition between your backend and Bondify's API doesn't fail logins that have already produced a proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The security boundary is auditable in isolation.&lt;/strong&gt; "Is this login real?" reduces to "does this HMAC check pass with this secret?" — a property you can unit test without mocking an HTTP client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same reason JWTs displaced server-side session lookups in a lot of API design: shifting verification from &lt;em&gt;ask the source of truth&lt;/em&gt; to &lt;em&gt;check a signature from the source of truth&lt;/em&gt; turns a remote dependency into a local computation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The properties that make it safe to do this
&lt;/h2&gt;

&lt;p&gt;Self-contained tokens are only as trustworthy as their constraints. Bondify's proof has three:&lt;/p&gt;

&lt;h3&gt;
  
  
  It expires fast
&lt;/h3&gt;

&lt;p&gt;A proof's &lt;code&gt;exp&lt;/code&gt; is &lt;strong&gt;5 minutes&lt;/strong&gt; after issuance. Standard JWT libraries reject it automatically once expired, so "verify promptly" isn't a suggestion you have to remember — it's enforced by the format. Don't cache a proof and check it later; check it the moment you receive it.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's single-use
&lt;/h3&gt;

&lt;p&gt;A session can be read once via the verify endpoint. The first &lt;code&gt;confirmed&lt;/code&gt; read flips it to &lt;code&gt;used&lt;/code&gt;, and it will never produce another proof after that. There's no scenario where the same proof gets handed to two different backends and both accept it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The signing secret never touches the browser
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;webhook_secret&lt;/code&gt; and &lt;code&gt;secret_key&lt;/code&gt; live only on your server. The browser only ever sees the public &lt;code&gt;project_id&lt;/code&gt;, which can &lt;em&gt;start&lt;/em&gt; a session but cannot verify proofs or call management endpoints. Even if someone inspects your frontend's network tab, there's nothing there to forge a proof with.&lt;/p&gt;

&lt;p&gt;A proof that passes signature verification is sufficient to trust the Telegram identity inside it — you don't need to call Bondify's API again to "double check." Re-verifying server-side adds latency without adding security, since the HMAC already proves provenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where webhooks fit in
&lt;/h2&gt;

&lt;p&gt;Polling step 4 above works, but for most backends a &lt;a href="https://dev.to/blog/handling-webhooks-correctly"&gt;webhook&lt;/a&gt; is the better default: Bondify pushes the confirmed identity to your endpoint the moment it happens, signed the same way (&lt;code&gt;X-Bondify-Signature&lt;/code&gt;, HMAC-SHA256 over the raw body, same &lt;code&gt;whsec_…&lt;/code&gt;). No proof JWT is involved there — the webhook carries the identity directly, and the signature &lt;em&gt;is&lt;/em&gt; the trust mechanism. We'll cover the delivery and retry semantics in a follow-up post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Bondify intentionally doesn't store
&lt;/h2&gt;

&lt;p&gt;The stateless design extends to data retention. Bondify keeps Telegram identity (&lt;code&gt;telegram_id&lt;/code&gt;, &lt;code&gt;telegram_name&lt;/code&gt;, &lt;code&gt;telegram_username&lt;/code&gt;), session metadata, and — only if you've enabled phone collection on Pro/Business — &lt;code&gt;telegram_phone&lt;/code&gt;. No passwords, no OAuth tokens, nothing your backend doesn't already need to make its own decision about who the user is.&lt;/p&gt;

&lt;p&gt;If you're integrating this for the first time, the &lt;a href="https://docs.bondify.dev/quickstart/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; walks through generating a session and verifying a proof end to end in about fifteen minutes.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>security</category>
      <category>sms</category>
      <category>otp</category>
    </item>
    <item>
      <title>What SMS OTP actually costs you, beyond the Twilio invoice</title>
      <dc:creator>Ilya-hye</dc:creator>
      <pubDate>Thu, 25 Jun 2026 15:10:29 +0000</pubDate>
      <link>https://dev.to/ilyahye/what-sms-otp-actually-costs-you-beyond-the-twilio-invoice-3571</link>
      <guid>https://dev.to/ilyahye/what-sms-otp-actually-costs-you-beyond-the-twilio-invoice-3571</guid>
      <description>&lt;p&gt;If your signup flow sends a text message, you already know it costs money per send. What's less visible on the invoice is everything SMS OTP costs you that never shows up as a line item: the users who bounce waiting for a code, the toll-fraud traffic nobody noticed until the bill doubled, and the engineering hours spent handling carrier edge cases that have nothing to do with your product.&lt;/p&gt;

&lt;p&gt;This isn't an argument that SMS is broken in some abstract sense - it's an argument that for an auth step specifically, you're paying for properties you don't need and not getting the one you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The direct cost is the easy part to see
&lt;/h2&gt;

&lt;p&gt;Twilio-style SMS pricing for OTP sits roughly in the $0.03–$0.08 range per message depending on destination country, and that's before you account for delivery retries when a carrier silently drops the first attempt. At 10,000 monthly active users, each going through login at least once, you're looking at somewhere between $300 and $800 a month - for a step that, conceptually, is just "prove you control this identifier."&lt;/p&gt;

&lt;p&gt;Compare that to what a messenger-based login costs structurally: nothing per-message, because there's no message to send through a paid carrier route. Bondify's Pro plan covers 10,000 MAU at a flat $20/month, with overage at $0.02 per additional MAU beyond that, paid from a balance you top up once - no auto-charging.&lt;/p&gt;

&lt;p&gt;The gap isn't a rounding error. It's the difference between a cost that scales linearly with every login attempt - including failed ones - and a cost that scales with monthly &lt;em&gt;active users&lt;/em&gt;, a number your business already has reasons to track and forecast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost that doesn't show up on an invoice: conversion
&lt;/h2&gt;

&lt;p&gt;SMS OTP adds a wait. The user submits a phone number, switches to their messages app - assuming they're on the device that number belongs to - finds the code among whatever else is in their inbox, switches back, and types six digits correctly before a timer expires. Every one of those steps is a place to lose someone, and aggregate signup-funnel data across SMS-gated flows consistently shows meaningful drop-off at the OTP step specifically, not before it.&lt;/p&gt;

&lt;p&gt;A messenger-based tap collapses that into: tap a button, tap Confirm in an app that's usually already open, done. There's no code to transcribe and no context switch to a different app entirely - Telegram's deep link opens the bot, the user taps once, and control returns to your product. The mechanism that makes this possible is the same stateless proof system &lt;a href="https://bondify.dev/blog/stateless-auth-proof-model" rel="noopener noreferrer"&gt;covered in our architecture post&lt;/a&gt; - your backend gets a verifiable identity assertion the instant the user confirms, without your frontend having to manage a code-entry UI at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost nobody budgets for: fraud
&lt;/h2&gt;

&lt;p&gt;Toll fraud - bots requesting OTPs to numbers that rack up premium-rate charges, or simply burning through your SMS budget to see what sticks - is a known failure mode of any flow where "send a text" is reachable by an unauthenticated request. Every fraudulent OTP request is a real charge from your SMS provider, and the attack scales as fast as a script can hit your endpoint.&lt;/p&gt;

&lt;p&gt;There's no equivalent surface here, because there's no per-attempt cost to externalize. Generating a Bondify session doesn't dispatch a billed message to a phone number you haven't verified belongs to anyone - it produces a deep link that does nothing until a real Telegram account taps Confirm. An attacker spamming your generate endpoint costs you nothing per request, which removes the financial incentive that makes OTP-bombing worth attempting in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost that's hardest to estimate: integration time
&lt;/h2&gt;

&lt;p&gt;A from-scratch OTP flow isn't just "call an SMS API." It's a backend service to generate and store codes with expiry, rate-limiting on both the request and verify steps, a UI for code entry with resend logic, and a long tail of carrier-specific edge cases - messages that arrive out of order, numbers that get reassigned, countries where delivery is unreliable enough that you build a fallback provider.&lt;/p&gt;

&lt;p&gt;The integration surface for messenger auth is smaller because there's less surface to begin with: no code-entry UI, no resend timer, no message delivery to monitor. The &lt;a href="https://docs.bondify.dev/quickstart/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; covers generating a session, redirecting to Telegram, and verifying the result - the kind of thing that fits in an afternoon rather than a sprint, precisely because the hard parts (signing, session lifecycle, replay prevention) are already handled on Bondify's side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where SMS still makes sense
&lt;/h2&gt;

&lt;p&gt;To be fair to SMS: it doesn't require the user to have a messenger account, and it works on devices with nothing installed beyond the phone app. If your user base skews toward demographics or regions where Telegram penetration is low, that's a real constraint worth weighing - messenger auth is a bet on your users already being on the platform, not a universal replacement for phone-number verification as a concept.&lt;/p&gt;

&lt;p&gt;What it isn't a good fit for is the common case this post is about: a SaaS or app where your users already have Telegram (or soon, WhatsApp and Discord) installed, and where the per-login cost and conversion drag of SMS are pure overhead relative to a one-tap alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it without committing to anything
&lt;/h2&gt;

&lt;p&gt;Bondify is currently in &lt;strong&gt;Public Beta&lt;/strong&gt;, and the platform is &lt;strong&gt;100% free&lt;/strong&gt; to use right now while we stress-test our database. &lt;/p&gt;

&lt;p&gt;Every new developer account automatically gets a &lt;strong&gt;7-day Pro trial&lt;/strong&gt; with custom bot support and phone number collection enabled. Since our automated billing is currently paused during the beta, if you need to extend your Pro features or increase your limits, just drop us a message inside the dashboard, and we’ll manually upgrade your workspace for free.&lt;/p&gt;

&lt;p&gt;The Hobby plan covers up to 1,000 MAU at $0 forever. If the numbers above hold for your funnel, check out our &lt;a href="https://docs.bondify.dev" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; and ship messenger-based auth in 15 minutes!&lt;/p&gt;

</description>
      <category>saas</category>
      <category>webdev</category>
      <category>security</category>
    </item>
  </channel>
</rss>
