<?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: Bilaniuc Dragos</title>
    <description>The latest articles on DEV Community by Bilaniuc Dragos (@dragosbln).</description>
    <link>https://dev.to/dragosbln</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%2F667037%2F9cecb184-ee2a-4249-9a4b-09008692eb32.jpeg</url>
      <title>DEV Community: Bilaniuc Dragos</title>
      <link>https://dev.to/dragosbln</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dragosbln"/>
    <language>en</language>
    <item>
      <title>I designed an AI auth-auditing tool around honesty. On its first real run, it confidently lied to me.</title>
      <dc:creator>Bilaniuc Dragos</dc:creator>
      <pubDate>Thu, 25 Jun 2026 11:24:49 +0000</pubDate>
      <link>https://dev.to/dragosbln/i-designed-an-ai-auth-auditing-tool-around-honesty-on-its-first-real-run-it-confidently-lied-to-e3g</link>
      <guid>https://dev.to/dragosbln/i-designed-an-ai-auth-auditing-tool-around-honesty-on-its-first-real-run-it-confidently-lied-to-e3g</guid>
      <description>&lt;p&gt;&lt;em&gt;How I rebuilt it so honesty and reliability were a structural constraint, not just a prompt request.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It lied to me on the first real run
&lt;/h2&gt;

&lt;p&gt;I spent days building an AI auth-auditing tool that detects what it can verify and refuses to guess at the rest. The whole point was to make it reliable, grounded, and honest. The first time I ran it on real production code, it told me with no hedge that the app stored its auth tokens in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It doesn't. That frontend keeps them in cookies. I configured it that way myself.&lt;/p&gt;

&lt;p&gt;My next thought was the obvious one: it said localStorage, but I put them in cookies, so what else is it lying about? A tool that's confidently wrong on the basics is worse than no tool, because it spends your trust and gives you nothing safe in return. Making it truly reliable became the priority.&lt;/p&gt;

&lt;p&gt;This piece is about why it lied, the risk in building tools like this, and how I designed around that risk. The fix had little to do with the prompt and almost everything to do with the structure around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I got here
&lt;/h2&gt;

&lt;p&gt;This is the third piece in a series on auth architecture in a large-scale production system. &lt;a href="https://dev.to/dragosbln/securing-auth-in-a-large-scale-production-system-three-industry-standard-architectures-and-why-279b"&gt;Part 1&lt;/a&gt; laid out the consequences of calcification on a large auth system, which made a proper rewrite too risky and costly. In that context a deliberate, documented acceptance of a flawed state, paired with foundation work, was preferable. &lt;a href="https://dev.to/dragosbln/keep-the-steering-wheel-3-ways-to-future-proof-your-auth-ee3"&gt;Part 2&lt;/a&gt; turned that into a defense: own the boundary and the volatile runtime behaviors instead of inheriting the vendor's, and noted that the coding agents which generate the calcified version by default are also the cheapest way to build the resilient one.&lt;/p&gt;

&lt;p&gt;Part 3 takes that seriously. I built a skill that reads a codebase and reports how hard-wired its auth is to a vendor's defaults, and what a future change would cost. The bar was high because the skill ships publicly alongside this series, and my gold standard was a skeptical senior engineer reading through the output of the skill and trusting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool does
&lt;/h2&gt;

&lt;p&gt;It audits one seam: the app-layer code that talks to your identity provider (Cognito, Auth0, or whatever you use), not the infrastructure or the gateway.&lt;/p&gt;

&lt;p&gt;A system is calcified when a change that should be local becomes a cross-cutting rewrite. Swapping token storage, changing refresh, or replacing the provider should each touch one place; in a calcified system each touches dozens of call sites, making any meaningful changes to the system risky and costly.&lt;/p&gt;

&lt;p&gt;The trouble is that code findings aren't enough. Calcification has to be evaluated in the real-world context of the project, its priorities, and the organization. That's why the tool needs to excel at detecting and classifying, but, more importantly, understand what it needs to escalate to human judgment — and do it!&lt;/p&gt;

&lt;p&gt;Four non-negotiables enforce it: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;never fabricate the human axes; &lt;/li&gt;
&lt;li&gt;never produce a false all-clear, where "no finding" has to mean "looked and found nothing"; &lt;/li&gt;
&lt;li&gt;report and propose, don't rewrite auth code; &lt;/li&gt;
&lt;li&gt;assess changeability, not security. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stating them in the prompt was never going to be enough; the structure of the skill had to make them the path of least resistance, which is most of what the rest of this article is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoiding calcification in an anti-calcification tool
&lt;/h2&gt;

&lt;p&gt;There was one more constraint, and it turned out to be the series eating its own cooking: no vendor could be hard-coded. Provider knowledge lives in one markdown file per vendor, and the engine never names a vendor, so adding a third is writing one more file. Hard-coding Cognito into the core would have been the calcified version of my own anti-calcification tool. &lt;/p&gt;

&lt;p&gt;A profile is just facts the vendor-agnostic core reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Token storage seam (vendors/amplify-cognito.md)&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Default storage: localStorage (in-memory fallback)
&lt;span class="p"&gt;-&lt;/span&gt; Custom storage API: cognitoUserPoolsTokenProvider.setKeyValueStorage(storage)
&lt;span class="p"&gt;-&lt;/span&gt; Look-alikes (NOT custom): built-in defaultStorage / sessionStorage / new CookieStorage()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  I tested it before trusting it
&lt;/h2&gt;

&lt;p&gt;"I tried it once and it looked right" was not an acceptable stance, so before running it on anything real I built adversarial fixtures for distinct failure modes. A fully calcified codebase tests recall (does it find what's there?), a cleanly bounded one tests precision (does it stay quiet instead of inventing findings?), and the same bounded app on a second provider tests generalization (does the methodology travel?). I made generalization structural: the app-layer files in the two bounded fixtures are byte-for-byte identical (verified with &lt;code&gt;diff&lt;/code&gt;), and only the adapter differs.&lt;/p&gt;

&lt;p&gt;The fourth fixture is the interesting one. It stresses four things at once: two providers in the same repo, an unknown provider with no profile (which has to surface as a coverage gap, not vanish), a mid-migration boundary (the new side bounded, the legacy side calcified), and the look-alike storage trap. That last one is a built-in selector that reads like a custom storage adapter but isn't:&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="c1"&gt;// Looks like owned storage. Isn't.&lt;/span&gt;
&lt;span class="c1"&gt;// A built-in selector chooses where tokens persist; it is not a custom adapter you own.&lt;/span&gt;
&lt;span class="nx"&gt;cognitoUserPoolsTokenProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setKeyValueStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crediting that as "owned storage" would be a confident, plausible, wrong finding. Running the fixture also forced two rules I had missed: assess multi-vendor and partial-boundary codebases per vendor instead of collapsing them to one verdict, and treat a real boundary with internal gaps as exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it lied on the first real codebase
&lt;/h2&gt;

&lt;p&gt;The failure didn't come from a fixture. It came from real code, on the most basic axis, in a case the fixtures hadn't covered. The Cognito profile documented how token storage works in Amplify v6, where you override it by calling &lt;code&gt;setKeyValueStorage&lt;/code&gt;:&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="c1"&gt;// What the profile knew to look for (Amplify v6):&lt;/span&gt;
&lt;span class="nx"&gt;cognitoUserPoolsTokenProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setKeyValueStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The codebase was Amplify v5, where you don't call that function. Storage is a field in the config object, a completely different shape:&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="c1"&gt;// What the codebase actually did (Amplify v5):&lt;/span&gt;
&lt;span class="nx"&gt;Amplify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cookieStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model searched for the v6 pattern, found nothing, and concluded "default storage, localStorage." The cookie configuration was right there in the file, in a shape the profile had never been told about. One missed pattern, one confidently wrong "no finding," and it was the exact failure the tool exists to prevent, happening inside the tool.&lt;/p&gt;

&lt;p&gt;When I fixed it, I checked Amplify's source rather than trusting memory: the default is localStorage in both versions. So the tool wasn't wrong about the default. It was wrong because it never noticed this codebase had overridden the default through a shape it didn't recognize. "The default is localStorage" and "this codebase uses localStorage" are completely different claims, and the distinction is subtle but the most valuable thing the tool can get right.&lt;/p&gt;

&lt;p&gt;That points at the real lesson: the tool didn't lie because it was careless, it lied because the task is hard. Storage alone has two completely different shapes across two versions of one vendor; multiply that across four axes, several providers, and the open-ended variety of real codebases, and you get a huge space where "I found nothing" can mean "truly absent" or "here in a form I wasn't taught." That space is where confident fabrication lives, and the harness has to scale with it. You can't close the gap by asking the model nicely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing for honesty instead of asking for it
&lt;/h2&gt;

&lt;p&gt;A line in the prompt saying "be accurate, don't fabricate" does almost nothing; a model under pressure to produce a useful-looking report will produce one. The work was making fabrication structurally hard.&lt;/p&gt;

&lt;p&gt;A small example sets the pattern. I started with "all findings should be verifiable," a wish. It became operational: every finding must carry a link to the exact file and line of evidence, one the reader can click. To produce that link the model has to have found the line, so verifiability stopped being a request and became something the output has to contain.&lt;/p&gt;

&lt;p&gt;The same principle drives the rule that keeps the mechanical pass honest. A text match is a candidate, never a finding:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Locate: use the profile's identifiers to find candidate locations.&lt;/li&gt;
&lt;li&gt;Confirm: open each candidate and read the surrounding code. A vendor type inside the adapter is correct; the same type in app-layer code is a leak.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;The textbook case is a wrapper that looks like a boundary and isn't. The search finds it, and a shallow pass calls it owned; only reading the return type shows the vendor's shape passing straight through to every caller:&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="c1"&gt;// Looks like a boundary. Isn't.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AuthSession&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetchAuthSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// returns Amplify's session type, unchanged&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules also live in the shape of the output. The report template makes the load-bearing sections mandatory: a Coverage section stating what was and wasn't analyzed, so "no finding" can't read as "clean"; per-finding evidence so every claim is checkable; and a "Judgment calls for you" section collecting the questions the audit refused to answer. A model can still misbehave, but it has to do so against the structure, which also hands the reader a way to catch it: if the Coverage section admits it skipped a file you know is full of auth logic, that gap is itself a signal you can act on.&lt;/p&gt;

&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%2F1n4m9x6whmtqbn3rq23g.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%2F1n4m9x6whmtqbn3rq23g.png" alt="Coverage section stating what was and wasn't analyzed" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The interview: from evidence to meaning
&lt;/h2&gt;

&lt;p&gt;In its first form the tool stopped at the evidence: finding X, finding Y, each with a file and line. Accurate, and not useful, because the reader still has to turn "you read claims inline in fourteen places" into "and for us, that means something." Having the model make that leap is where it starts inventing, because meaning depends on what it can't see: your roadmap, your org, the contracts that depend on the current choice.&lt;/p&gt;

&lt;p&gt;So I put an interview between the evidence and the meaning. The tool presents what it found, grouped by axis, then asks the questions only the maintainer can answer, one at a time, with an explicit "don't know" that routes that axis into "Judgment calls" rather than forcing an answer. These were the guiding questions, adaptable to different contexts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Token storage: "Is a storage change on the table, like a move to HttpOnly cookies?"&lt;/li&gt;
&lt;li&gt;Refresh: "Is a change to refresh coming, often downstream of a storage move?"&lt;/li&gt;
&lt;li&gt;Identity provider: "Is a provider swap realistically possible in the next year or two? Roughly how many call sites would it touch?"&lt;/li&gt;
&lt;li&gt;Authorization: "Is an authorization-model change planned (RBAC, ID to access token)? What backend contracts depend on the current choice?"&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Their answers supply likelihood, the mechanical pass supplies the cost evidence, and only together do they produce a ranking, with every rank traceable to which input was the human's. With no human in the loop, the tool refuses to rank and stops at the findings and open questions. The refusal is a feature.&lt;/p&gt;

&lt;p&gt;Building the interview into the skill changed how it analyzes, not just how it reports. Because the run is oriented toward an interview from the start, the mechanical pass is already asking "what here is a judgment call I will have to escalate, and what is the most useful question to ask about it?" instead of "what answer can I produce?" A model planning to ask is a model less inclined to invent; the interview is an output stage and an anti-fabrication mechanism at once.&lt;/p&gt;

&lt;p&gt;The broader lesson holds past auth: between a finding and its meaning is a set of things only the human knows, and designing the tool to ask for them sharpens its own detection too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distilling without diluting
&lt;/h2&gt;

&lt;p&gt;Running it on real codebases surfaced a quieter failure: the reports were thorough and far too long to use. A valuable finding would sit in paragraph nine, indistinguishable from six routine ones. A finding nobody reads might as well be one the tool never made.&lt;/p&gt;

&lt;p&gt;So I added a one-screen summary, distilled from the full report rather than written separately. That ordering matters more than it looks. Because the full report already carries the harness (the coverage section, the per-finding file links, the refusal to score what only the human knows), the summary inherits that reliability and doesn't have to re-earn it. So the summary can play by lighter rules: focus on finding and presenting the most relevant aspects of the report, instead of validating and double-checking.&lt;/p&gt;

&lt;p&gt;With the right structure, you can get both the actionable clarity of a concise summary and the proven reliability of a full report behind it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Auth Calcification Summary: &amp;lt;repo&amp;gt;&lt;/span&gt;

Vendor: Amazon Cognito (Amplify v5). Boundary: a leaky facade, not a real seam.

Most important: RBAC is on your roadmap, but the API is authorized with the ID token
while permissions arrive in the access token. The plan has an unnamed dependency.

| Axis          | Today                 | If you change it                          |
| ------------- | --------------------- | ----------------------------------------- |
| Token storage | v5 cookieStorage      | Low, once the facade becomes a real seam  |
| Refresh       | Inherited from vendor | Medium; no single-flight, no failure path |
| Provider      | Cognito-specific      | High; groups/claims read in 14 call sites |
| Authorization | ID token, inline      | High; blocks the RBAC move above          |

Full evidence and file links are in the report.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;p&gt;The skill is live, open source, and installable as a Claude Code plugin. The repository is &lt;a href="https://github.com/dragosbln/auth-calcification" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Add the repo as a plugin marketplace, install from it, then point Claude at a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# in Claude Code (exact names are in the repo README)&lt;/span&gt;
/plugin marketplace add dragosbln/auth-calcification
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;auth-calcification-audit@auth-calcification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, ask it to audit your auth layer. It runs the mechanical pass, asks you the short interview, and writes three things: the full report, the one-screen summary, and an actionable backlog ordered by priority using your answers.&lt;/p&gt;

&lt;p&gt;Right now it ships with Cognito and Auth0, but the ports-and-adapters design means you can extend it. Clone the repo, hand &lt;code&gt;references/vendor-profile-schema.md&lt;/code&gt; and your provider's docs to Claude Code (or your AI IDE) to draft a new profile, drop it in &lt;code&gt;vendors/&lt;/code&gt;, and run the skill from your fork. No core changes; that's the whole point of the split. If you do, I'd welcome the findings, the misses, and the suggestions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The harder and broader the task, the more the harness matters. Confident fabrication concentrates where the work is complex and the inputs vary.&lt;/li&gt;
&lt;li&gt;Prompting for honesty isn't enough. A clickable file-and-line link, a mandatory coverage section, and a refusal to rank without human input are structure, not requests.&lt;/li&gt;
&lt;li&gt;Don't fabricate judgment; escalate it. Detect mechanically, then hand back what only the maintainer can answer.&lt;/li&gt;
&lt;li&gt;Tools over-produce, so distillation is part of the product. A correct finding buried on page three fails like a wrong one.&lt;/li&gt;
&lt;li&gt;An interview built into the tool disciplines the model as much as it informs the human.&lt;/li&gt;
&lt;li&gt;The boundary discipline you would apply to auth applies to the tool itself: keep the vendor-specific parts swappable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Every one of these fixes came from the same root: a tool that has to reason about a wide, nuanced space will, sooner or later, hit a corner it wasn't taught and answer anyway, with total confidence. You don't fix that by asking it to be honest; you fix it with a harness that makes the honest output the easy one to produce, and the harness has to scale with the task, because that complexity is exactly where fabrication breeds.&lt;/p&gt;

&lt;p&gt;In the agentic era, the typing gets outsourced and the judgment doesn't. The mechanical work that used to make teams skip the boundary is nearly free now; what's left, and what's worth your attention, is the judgment about what any of it means. A good tool automates that detection, escalates the judgment, and presents the findings with clarity — baked into its structure, not asked for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If your team is about to make an expensive-to-reverse decision in auth, cloud, or platform architecture and wants an independent second pair of eyes, that's the kind of work I take on through Luckylabs. DMs open.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Keep the steering wheel: 3 ways to future-proof your auth</title>
      <dc:creator>Bilaniuc Dragos</dc:creator>
      <pubDate>Fri, 12 Jun 2026 09:36:17 +0000</pubDate>
      <link>https://dev.to/dragosbln/keep-the-steering-wheel-3-ways-to-future-proof-your-auth-ee3</link>
      <guid>https://dev.to/dragosbln/keep-the-steering-wheel-3-ways-to-future-proof-your-auth-ee3</guid>
      <description>&lt;p&gt;&lt;em&gt;Your auth library's defaults quietly become your architecture. Here's how to stay in control while still renting the hard parts from people who specialize in them.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth is the ultimate hot path
&lt;/h2&gt;

&lt;p&gt;Most components in your system sit on one critical path. Authentication sits on three at once.&lt;/p&gt;

&lt;p&gt;It's the &lt;strong&gt;entry point&lt;/strong&gt;: the first thing a user touches, where friction costs you people before they're even inside the product. It's on the &lt;strong&gt;critical path of every authenticated API call&lt;/strong&gt;: every request carries a credential, so auth's latency, reliability, and cost are levied on all of them. And it's the &lt;strong&gt;highest-value security target&lt;/strong&gt; in the system, the one component where a single flaw doesn't degrade something — it compromises everything.&lt;/p&gt;

&lt;p&gt;Picking the right vendor for your requirements matters, obviously. But even with the ideal vendor, there's a hidden risk. When you adopt a vendor auth library, you outsource the &lt;em&gt;implementation&lt;/em&gt;, which is good — you should. But you also silently outsource the &lt;em&gt;architecture&lt;/em&gt;. The library's defaults become load-bearing walls you never decided to build. By the time you want to move one, you discover the whole house is resting on it.&lt;/p&gt;

&lt;p&gt;A note on scope before we start: this piece is about the &lt;strong&gt;client side of auth&lt;/strong&gt; — SPAs and micro-frontends, token storage, refresh, the seam between your frontend code and a managed identity provider. The principles travel, but the examples live in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  How auth calcifies
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://dev.to/dragosbln/securing-auth-in-a-large-scale-production-system-three-industry-standard-architectures-and-why-279b"&gt;previous article&lt;/a&gt; I walked through a real situation: a penetration test flagged that our auth tokens were stored where client-side JavaScript could read them, and we &lt;em&gt;deliberately accepted&lt;/em&gt; the vulnerability rather than fixing it immediately. Not out of negligence. The auth system hadn't been built with change in mind, so what should have been a configuration change had calcified into a months-long re-architecture touching multiple micro-frontends and 100+ backend services that consume those tokens.&lt;/p&gt;

&lt;p&gt;I made a claim in that piece: that "do nothing for now" actually meant doing a lot of quiet foundation work — the kind that turns a future migration from a re-architecture into a flipped switch.&lt;/p&gt;

&lt;p&gt;This article is that foundation work: three ways to keep auth changeable. It applies whether you're starting greenfield, where building it in costs almost nothing, or running a large production system, where you do it proactively, in the windows where you have bandwidth, ahead of a move you can see coming. In the second case there's a bonus: making auth changeable forces you to understand the system you already have, in ways that pay off well beyond auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way 1: Own the boundary (and make it more than a facade)
&lt;/h2&gt;

&lt;p&gt;Create your own auth interface — but make sure it isn't a &lt;em&gt;leaky facade&lt;/em&gt;. That's the common trap: a &lt;code&gt;lib/auth.ts&lt;/code&gt; that re-exports the vendor's functions but still returns the vendor's session object, throws the vendor's errors, and uses the vendor's claim names. The vendor's shape passes straight through to every call site. You've added a file, not a boundary.&lt;/p&gt;

&lt;p&gt;A boundary that actually future-proofs you does three things a facade doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It's an anti-corruption layer.&lt;/strong&gt; It speaks &lt;em&gt;your&lt;/em&gt; domain's language — your &lt;code&gt;Session&lt;/code&gt;, &lt;code&gt;Principal&lt;/code&gt;, &lt;code&gt;AuthError&lt;/code&gt; — and the vendor's types never cross it.&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="c1"&gt;// ❌ Leaky: the vendor's shape escapes into every caller.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getSession&lt;/span&gt; &lt;span class="o"&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="nf"&gt;fetchAuthSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// returns Amplify's AuthSession&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Bounded: callers only ever see your types.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuthPort&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;getAuthHeaders&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;getPrincipal&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;onRefresh&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;onSessionExpired&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Capabilities are injected, not imported.&lt;/strong&gt; The common wrapper is an imported singleton, so every call site is hard-wired to one implementation. That makes it impossible to swap per context and untestable without mocking modules.&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="c1"&gt;// ❌ Imported singleton: every call site hard-wired to one implementation.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getToken&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="s2"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inject the capability instead. Now you can swap it per execution context (client, server, test) and stand up your whole data layer against a fake in tests, with zero network:&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="c1"&gt;// ✅ Injected: the client depends on the AuthPort contract, not a concrete import.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthPort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;apiFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;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;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&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="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthHeaders&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onRefresh&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// refresh → retry&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onSessionExpired&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Session expired&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(What &lt;code&gt;onRefresh&lt;/code&gt; actually does is the subject of Way 3. For now, notice that it's a &lt;em&gt;slot&lt;/em&gt;. The call sites don't know or care.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The boundary is contract-tested.&lt;/strong&gt; This is the part almost nobody does, and it's what makes "future-proof" verifiable instead of aspirational. Because the boundary is an explicit interface, you write &lt;em&gt;one&lt;/em&gt; conformance suite, and every adapter you ever write must pass it:&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="c1"&gt;// auth-port.contract.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Run the SAME suite against every adapter: Amplify today, Auth0 tomorrow,&lt;/span&gt;
&lt;span class="c1"&gt;// the in-memory fake your tests use forever.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runAuthContractTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;makeAdapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestScenario&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;AuthPort&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="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`AuthPort contract: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returns a bearer header when authenticated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tokenState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;valid&lt;/span&gt;&lt;span class="dl"&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;headers&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthHeaders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^Bearer /&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returns no principal when unauthenticated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tokenState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;absent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPrincipal&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recovers from one expired token via onRefresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tokenState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refreshOutcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onRefresh&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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;headers&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthHeaders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^Bearer /&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// new token, same contract&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;signals expiry exactly once when refresh fails&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;onExpired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;tokenState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;refreshOutcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;onSessionExpired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onExpired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onRefresh&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onSessionExpired&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onExpired&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledTimes&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="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dedupes concurrent refreshes into a single underlying call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestScenario&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tokenState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refreshOutcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onRefresh&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onRefresh&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onRefresh&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshCallCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="c1"&gt;// see Way 3&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="c1"&gt;// Then each adapter is one line of registration:&lt;/span&gt;
&lt;span class="nf"&gt;runAuthContractTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amplify&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="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;makeAmplifyAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;runAuthContractTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;in-memory fake&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="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;makeFakeAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes what a migration &lt;em&gt;is&lt;/em&gt;. It stops being "swap it and pray across 100 call sites" and becomes "make the new adapter green." Your migration now has a definition of done — and your test fake is held to the same standard as production, so green tests against the fake actually mean something.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fjblm1lwklstfobvi438s.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2Fjblm1lwklstfobvi438s.jpg" alt="Way 1 — Ports and adapters: app code depends on your AuthPort; vendors are swappable adapters behind it" width="799" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The boundary is the &lt;em&gt;shape&lt;/em&gt; of future-proof auth. The next two ways are about drawing it correctly and filling in its hardest piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way 2: Know your tools deeply enough to replace them
&lt;/h2&gt;

&lt;p&gt;You can't draw a good boundary around something you don't understand. And you don't understand an auth library by reading its quickstart — you understand it by &lt;em&gt;extending&lt;/em&gt; it until its machinery is visible.&lt;/p&gt;

&lt;p&gt;One high-leverage exercise: replace its token storage. Even if you only reproduce what the library already does, the act forces every question that matters. What's holding my tokens right now? Where else could they live — &lt;code&gt;localStorage&lt;/code&gt;, a cookie, an HttpOnly cookie, memory? What are the tradeoffs? How would I switch later? Done with a clean, well-documented adapter, the result is a concise artifact your whole team benefits from: the storage mechanism explicit and in front of you, instead of buried in vendor docs and guesswork.&lt;/p&gt;

&lt;p&gt;Every serious library exposes this seam, and learning where is half the lesson:&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="c1"&gt;// Amplify v6 — pluggable key-value storage for tokens.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cognitoUserPoolsTokenProvider&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="s2"&gt;aws-amplify/auth/cognito&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;cognitoUserPoolsTokenProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setKeyValueStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* cookie? memory? your call */&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auth0&lt;/strong&gt;'s SPA SDK takes a custom &lt;code&gt;cache&lt;/code&gt; implementing &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;set&lt;/code&gt;/&lt;code&gt;remove&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase&lt;/strong&gt; lets you &lt;em&gt;select&lt;/em&gt; among persistence strategies (&lt;code&gt;browserLocal&lt;/code&gt;, &lt;code&gt;browserSession&lt;/code&gt;, &lt;code&gt;indexedDB&lt;/code&gt;, &lt;code&gt;inMemory&lt;/code&gt;) via &lt;code&gt;setPersistence&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different surfaces, same underlying seam. Find it in one library and you know what to look for in the next.&lt;/p&gt;

&lt;p&gt;Storage is just the lead example. The same "extend it to understand it" move applies elsewhere: swapping in a custom HTTP client, hooking the token-refresh cycle, subscribing to the library's auth lifecycle events, plugging in a custom credentials provider. Each one makes a different part of the machine visible.&lt;/p&gt;

&lt;p&gt;The deliverable isn't the adapter. It's the understanding — the thing that lets you write an adapter you trust, and recognize in advance which of the vendor's defaults are about to become your walls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way 3: Own your critical runtime behaviors, starting with refresh
&lt;/h2&gt;

&lt;p&gt;Token refresh is where vendor magic most quietly becomes load-bearing. In our Amplify setup, calling &lt;code&gt;fetchAuthSession()&lt;/code&gt; refreshed tokens silently and automatically. Very convenient — until you look closer and notice the convenience depends on the tokens living in storage the browser's JavaScript can read, the same client-accessible storage that's open to XSS exfiltration. (That's the root problem behind the &lt;a href="https://dev.to/dragosbln/securing-auth-in-a-large-scale-production-system-three-industry-standard-architectures-and-why-279b"&gt;first article&lt;/a&gt;.) The moment you move tokens into HttpOnly cookies to close that hole, client code can no longer read them, and the silent client-side refresh you built everything on stops working. All at once, everywhere.&lt;/p&gt;

&lt;p&gt;Own the behavior instead, so a change like that touches one place. Refresh has three parts worth understanding rather than inheriting: &lt;em&gt;when&lt;/em&gt; you refresh, how you avoid stampedes, and what happens when it fails.&lt;/p&gt;

&lt;p&gt;The robust shape is reactive — refresh on a &lt;code&gt;401&lt;/code&gt;, then retry — with &lt;strong&gt;single-flight deduplication&lt;/strong&gt;, so that ten concurrent &lt;code&gt;401&lt;/code&gt;s trigger one refresh, not ten:&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refreshOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doRefresh&lt;/span&gt;&lt;span class="p"&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;inflight&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nf"&gt;doRefresh&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inflight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// every concurrent caller awaits the same refresh&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the &lt;code&gt;onRefresh&lt;/code&gt; slot from Way 1, finally filled in. Owned by you, swappable behind the boundary, and held to the contract suite (that's what the "dedupes concurrent refreshes" test was checking).&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fbaxcagezzaf2xwqsic41.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2Fbaxcagezzaf2xwqsic41.jpg" alt="Way 3 — reactive token refresh" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Refresh is the headline example, but the principle generalizes to every behavior the vendor does invisibly: multi-tab session sync, sign-out propagation, silent re-auth. None of these are wrong to inherit. On a hot path, though, you want to &lt;em&gt;know&lt;/em&gt; you've inherited them, and be able to take the wheel when you need to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the three ways point
&lt;/h3&gt;

&lt;p&gt;Notice that none of the three is &lt;em&gt;about&lt;/em&gt; swapping your identity provider, yet together they push you toward it. A boundary that speaks OIDC and OAuth2 rather than vendor-isms, an understanding deep enough to write a conforming adapter, and volatile behaviors you own behind the seam all pull in the same direction: making the IdP closer to configuration than architecture.&lt;/p&gt;

&lt;p&gt;You won't get there entirely for free. Vendor-specific features (Cognito groups, custom attributes, admin APIs) leak, and the more you lean on them, the more coupling stays behind. But these steps localize that coupling to one adapter instead of spreading it across the codebase. If you do reach the point where switching providers means writing a new adapter and making it pass your contract suite, that's one of the most valuable decoupling points a system can have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the line sits: rent vs. own
&lt;/h2&gt;

&lt;p&gt;If you're doing all this, a reasonable question is whether the vendor is earning its keep at all. Why not drop it and own the whole thing?&lt;/p&gt;

&lt;p&gt;Because the vendor is doing the genuinely hard, genuinely dangerous work you do &lt;em&gt;not&lt;/em&gt; want to own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cryptographic correctness&lt;/strong&gt; — password hashing, token signing, PKCE, constant-time comparisons, the dozen ways to get this subtly and catastrophically wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The edge-case swamp&lt;/strong&gt; — MFA, account recovery, brute-force lockouts, credential-stuffing detection, bot defense. Each is a project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance and certification&lt;/strong&gt; — SOC 2 and the security audits a managed provider passes so you don't have to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 24/7 operational burden&lt;/strong&gt; — auth is a hot path, so if your hand-rolled auth goes down, everything goes down. Now it's in your on-call rotation forever, against an adversary whose techniques evolve weekly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/dragosbln/securing-auth-in-a-large-scale-production-system-three-industry-standard-architectures-and-why-279b#:~:text=We%20now%20own%20the%20entire%20auth%20substrate."&gt;previous article&lt;/a&gt;, the single most decisive argument against one of the candidate architectures was exactly this: it would have meant owning the entire auth substrate — a new system of record, patched and scaled and on-call'd by us instead of by people who do only that. That cost sank the option.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Rent the engine. Own the steering.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The line has another side, and falling off it is just as expensive. Take "future-proof" too far — abstract everything, build seams nobody will ever flex, reproduce half the vendor's library "just in case" — and you land somewhere worse than where you started: over-built for today and still wrong for tomorrow, because the future arrived in a shape you didn't predict.&lt;/p&gt;

&lt;p&gt;The discipline is to build seams only where change is both &lt;strong&gt;likely&lt;/strong&gt; and &lt;strong&gt;expensive to retrofit&lt;/strong&gt;. Notice what you're betting on: &lt;em&gt;which&lt;/em&gt; axes will move, not &lt;em&gt;how&lt;/em&gt;. You don't need to know whether you'll move to HttpOnly cookies or switch providers, only that those axes are live and worth making cheap to change in any direction.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A seam is cheaper than a prediction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Flexibility everywhere is its own kind of rigidity. The skill is choosing where to be flexible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agentic era cuts both ways
&lt;/h2&gt;

&lt;p&gt;The calcification trap predates AI, but coding agents make it faster and more pervasive, for a structural reason. An agent reaches for the vendor's documented happy path: &lt;code&gt;import { signIn } from "vendor"&lt;/code&gt;, used in-line, no boundary, vendor-magic refresh. It optimizes for "compiles and ships," not "still changeable in two years," and it won't weigh changeability unless you tell it to. The patterns that calcify now get generated at volume, often by people scaffolding auth they don't fully understand. What used to happen one developer at a time now happens across a codebase before anyone makes a conscious architectural decision.&lt;/p&gt;

&lt;p&gt;But the same agent that defaults to the calcified version is also the cheapest way you've ever had to build the resilient one. The AuthPort, the adapter, the conformance suite, the single-flight refresh — the mechanical work that used to be the reason teams skipped the boundary ("no time for all that abstraction") — is now nearly free to generate. The cost that justified cutting the corner has collapsed.&lt;/p&gt;

&lt;p&gt;So the balance tips toward doing the three ways, not away. The judgment stays yours: where the seams belong, which axes are live, what's likely and expensive enough to bound. The typing is the agent's. The newly essential discipline is encoding your intent so the agent stays inside it — a boundary spec it builds against, the contract suite as its target, lint rules that fail the build when a vendor type leaks past the line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A wrapper isn't a boundary.&lt;/strong&gt; If the vendor's types, errors, and claim names cross your abstraction, you've built a leaky facade. Speak your own domain's language and keep the vendor on one side of the line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inject auth as a capability&lt;/strong&gt;, not an imported singleton, so it's swappable per context and your data layer is testable with zero network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract-test the boundary.&lt;/strong&gt; One conformance suite per interface turns "future-proof" into a definition of done for any future migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understand your library by extending it.&lt;/strong&gt; Replacing token storage is the highest-leverage exercise, and the result is an explicit artifact your team can read instead of guess at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own the volatile runtime behaviors&lt;/strong&gt;, refresh first, with single-flight dedup and an explicit failure path. Know what you've inherited even when you choose to keep it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future-proofing is not building auth yourself.&lt;/strong&gt; Rent the crypto, the edge cases, the compliance, the on-call. Own the seams and the judgment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't over-rotate.&lt;/strong&gt; Build seams only where change is both likely and expensive to retrofit. Bet on which axes move, not how.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents accelerate calcification — but also the fix.&lt;/strong&gt; They default to the vendor's calcified happy path at scale, but make the boundary nearly free to build. Supply the architectural intent; encode it as specs, contract tests, and lint rules the agent can't route around.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;This is part two of a short series on auth architecture in production. &lt;a href="https://dev.to/dragosbln/securing-auth-in-a-large-scale-production-system-three-industry-standard-architectures-and-why-279b"&gt;Part one&lt;/a&gt; is the narrative: how an under-designed auth system cornered a large platform into accepting a known vulnerability, and why that was the right call given the constraints. This piece is the constructive flip side — the foundation work that keeps you out of that corner in the first place.&lt;/p&gt;

&lt;p&gt;Next in the series: I'm building an agent skill that audits a codebase for exactly the failures described here — missing boundaries, leaking vendor types, inherited refresh — and proposes a backlog to fix them. Follow along if that's useful.&lt;/p&gt;

&lt;p&gt;And if your team is about to make an expensive-to-reverse decision in auth, cloud, or platform architecture and wants an independent second pair of eyes, that's the kind of work I take on through Luckylabs. Reach out if it sounds useful.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>webdev</category>
      <category>security</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Securing auth in a large-scale production system: three industry-standard architectures — and why none survived a closer look</title>
      <dc:creator>Bilaniuc Dragos</dc:creator>
      <pubDate>Tue, 26 May 2026 09:47:36 +0000</pubDate>
      <link>https://dev.to/dragosbln/securing-auth-in-a-large-scale-production-system-three-industry-standard-architectures-and-why-279b</link>
      <guid>https://dev.to/dragosbln/securing-auth-in-a-large-scale-production-system-three-industry-standard-architectures-and-why-279b</guid>
      <description>&lt;p&gt;&lt;em&gt;A case study in why the verdict on an architecture decision can shift entirely once you dive into implementation details — using auth security on a large Next.js + AWS system as the vehicle. This article walks the three architectural paths we considered — plus a fourth option that, in this context, won.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Context: a sensitive problem in a large scale system
&lt;/h2&gt;

&lt;p&gt;The system: a large multi-frontend production stack. Multiple Next.js apps spread across subdomains, 100+ backend microservices, AWS Cognito for identity, API Gateway in front of the backend, downstream APIs expecting &lt;code&gt;Authorization: Bearer &amp;lt;id_token&amp;gt;&lt;/code&gt; on every request.&lt;/p&gt;

&lt;p&gt;The finding: a penetration test flags that our Cognito tokens — ID, access, refresh — are stored in non-HttpOnly cookies. Client-side JavaScript can read them. Any successful XSS exfiltrates them. Any leaked refresh token gives an attacker effectively indefinite access.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in flow form — the current client-side API call pattern, with the moment of vulnerability highlighted:&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Ft6wu7d7zr1p2sriqdrxf.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.amazonaws.com%2Fuploads%2Farticles%2Ft6wu7d7zr1p2sriqdrxf.png" alt="Current Implementation — Direct Client-to-Backend with JS-Accessible Tokens" width="799" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things worth noticing before we go further. First, this flow is cheap — the client calls the backend directly, no proxy layer in the path, every component doing the minimum it has to. Second, the entire vulnerability is concentrated in the single step where browser-side code reads the token out of document.cookie to attach it to the outbound request. Third, every alternative we'll walk through changes the shape of that single moment — and in doing so, ripples through every other component on the diagram.&lt;/p&gt;

&lt;p&gt;The shape of the fix is well-known: tokens should not be JS-readable. Move them off the client.&lt;/p&gt;

&lt;p&gt;So why are we writing a 3,500-word article about it?&lt;/p&gt;

&lt;p&gt;Because auth in a system like this isn't a feature. It's a hot path — twice. It's the entry point to the system, and then it's a participant in every API call that follows. The architecture choice you make doesn't just impact "the login screen." It impacts every request, every page render, every cost line item, every incident postmortem for the foreseeable future.&lt;/p&gt;

&lt;p&gt;That's the kind of problem where the obvious architectural answer is the dangerous one — not because it's wrong, but because it's incomplete. The shape is right; the implementation cost can take you over the edge.&lt;/p&gt;

&lt;p&gt;This article walks four architectural paths to fix the same vulnerability in the same system. Each path is sound in isolation. Each path's verdict shifts — sometimes inverts — once you stop thinking at the architecture level and start thinking at the implementation level.&lt;/p&gt;

&lt;p&gt;The stack details are the vehicle. The cargo is the thinking pattern: &lt;strong&gt;architectural thinking alone won't tell you which of these four paths is right. Implementation depth will.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The "obvious" answer, and why to distrust it
&lt;/h2&gt;

&lt;p&gt;The chain of obvious reasoning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tokens accessible to JavaScript are vulnerable to XSS exfiltration.&lt;/li&gt;
&lt;li&gt;Therefore, move tokens out of JavaScript's reach.&lt;/li&gt;
&lt;li&gt;Therefore, use HttpOnly cookies, set by the server, never read by the client.&lt;/li&gt;
&lt;li&gt;Done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the &lt;em&gt;shape&lt;/em&gt; level, this is correct.&lt;/p&gt;

&lt;p&gt;The trouble is that step 4. "Done" is not done — it's actually doing about 90% of the work in that argument. "Done" implies the implementation falls out cleanly. It almost never does.&lt;/p&gt;

&lt;p&gt;The architectural move is to refuse "done" as an output until you've answered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does the client-side code now authenticate API calls it used to authenticate directly?&lt;/li&gt;
&lt;li&gt;How does the backend now extract a credential it used to receive in a known format?&lt;/li&gt;
&lt;li&gt;How does the system refresh tokens that can no longer be seen by the code that previously refreshed them?&lt;/li&gt;
&lt;li&gt;What's the new attack surface? (HttpOnly cookies close XSS; they open CSRF.)&lt;/li&gt;
&lt;li&gt;What does the cost model look like at production scale?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those questions has multiple architecturally-valid answers. Each answer creates a different system. The point of the analysis isn't to find &lt;em&gt;an&lt;/em&gt; answer — it's to walk each answer to its third-order consequences before picking.&lt;/p&gt;

&lt;p&gt;Let's do that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The constraint stack
&lt;/h2&gt;

&lt;p&gt;The constraints below are real — pulled from the actual system. They're what every architectural option gets weighed against. The discipline here is &lt;em&gt;map options against constraints, not against feature checklists.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Frontend framework.&lt;/strong&gt; Next.js App Router, heavy use of Server Components, hybrid client-server data fetching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Downstream contract.&lt;/strong&gt; ~100+ backend microservices, all expecting &lt;code&gt;Authorization: Bearer &amp;lt;id_token&amp;gt;&lt;/code&gt;. The API Gateway in front of them validates JWTs via its built-in authorizer reading the &lt;code&gt;Authorization&lt;/code&gt; header. Changing this contract means changing it across all 100+ services in a coordinated manner. Doing it in fewer than all of them is not an option — the contract is uniform on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Hosting cost model.&lt;/strong&gt; The Next.js apps run on Vercel. Vercel bills serverless functions by the &lt;em&gt;wall clock time&lt;/em&gt; of every request — including the time spent waiting on a slow downstream call. A 5-second hung backend response gets billed for 5 seconds of compute, every time it happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Client/server call ratio.&lt;/strong&gt; Roughly 65-70% of API calls in the system are made directly from the browser (client components, after-hydration data fetches). The remaining 30-35% happen server-side during SSR. Any architecture that proxies "all client calls through the server" effectively doubles the proxied request volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Token characteristics.&lt;/strong&gt; Cognito JWTs are large — ID tokens commonly run 1-2KB, sometimes more depending on groups and claims. Browser cookie size limits sit at 4KB per cookie, and total request header limits (the source of HTTP 431 errors) sit at 8KB on many gateways. Putting multiple Cognito tokens in cookies and sending them on every request is a known production failure mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Vendor library maturity.&lt;/strong&gt; AWS Amplify v6 supports HttpOnly cookies for Next.js — but only via an experimental server-side auth feature, and only when paired with Cognito Managed Login.&lt;/p&gt;

&lt;p&gt;That's the stack. Every option below has to survive it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Path 1: Next.js BFF proxy
&lt;/h2&gt;

&lt;p&gt;The pattern most commonly recommended in the Next.js ecosystem, and the one AWS Premium Support proposed in the first round of their guidance. It's also the default shape that libraries like Auth.js (formerly NextAuth) push you toward when wrapping OAuth providers — manage session cookies on the server, proxy token-bearing calls through Next.js route handlers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shape:&lt;/strong&gt; All client-side API calls stop going directly to the backend. Instead, they hit Next.js route handlers (or Server Actions). Those handlers read tokens from HttpOnly cookies — which they can, because they're server-side — and forward the request to the backend with the &lt;code&gt;Authorization: Bearer&lt;/code&gt; header attached.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2F6ailc41s0savkgndsgxo.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.amazonaws.com%2Fuploads%2Farticles%2F6ailc41s0savkgndsgxo.png" alt="Path 1 — Next.js BFF Proxy" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The architectural appeal.&lt;/strong&gt; Clean. Well-documented. Vendor-blessed. The pattern most public material recommends. Tokens never touch the client; the downstream contract doesn't change; the API Gateway's existing JWT authorizer keeps working.&lt;/p&gt;

&lt;p&gt;At the architecture level, this looks like the right answer. It is, in fact, the right answer for many systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it stops looking right — implementation level.&lt;/strong&gt; Two things surface as you zoom in:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The cost model breaks.&lt;/em&gt; Recall constraints #3 and #4. We have a hosting model that bills for wall-clock wait time, and we'd be putting 65-70% of API traffic that previously bypassed Vercel directly &lt;em&gt;through&lt;/em&gt; Vercel. Every slow backend call now costs us twice — once for the original execution time, once for the time the Vercel function spends waiting. A backend incident where calls take 30 seconds to time out becomes a Vercel bill incident.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Vercel lock-in becomes structural.&lt;/em&gt; In the BFF model, 100% of API traffic flows through Next.js route handlers on Vercel — making Vercel part of every API call's critical path, and shaping the architecture around Vercel-optimized patterns. Any future migration to a different host (for cost reasons, feature reasons, or just optionality) stops being a packaging change and becomes a real re-architecture. Vercel's billing model has, at this scale, already produced unwelcome surprises; locking the auth hot path to a single hosting vendor compounds that exposure rather than reducing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict shift.&lt;/strong&gt; Strong default. Wrong for &lt;em&gt;this&lt;/em&gt; stack — the cost surface and the vendor lock-in are the real disqualifiers; everything else is downstream of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Path 2: Token-broker BFF with session DB
&lt;/h2&gt;

&lt;p&gt;A variant that escapes Vercel by introducing a dedicated AWS-native proxy. The browser holds an HttpOnly session cookie (opaque); a Lambda sits in front of the backend, reads the cookie, looks the session up in a database, retrieves the Cognito token, and forwards the request with the Bearer header attached.&lt;/p&gt;

&lt;p&gt;This is essentially the architecture that &lt;strong&gt;Better Auth&lt;/strong&gt; promotes when it's wired up to an external OAuth provider like Cognito: a server-side session store with the provider's tokens held inside it, and the browser carrying only an opaque session reference. Better Auth also supports a stateless variant — where the session is encrypted directly into the cookie and the DB lookup disappears — which trades the database hot path for cookie-size and key-rotation problems. We'll focus on the DB-backed shape, as it's the one most production deployments converge on.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fcdv48xxobqzniwcb88ew.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.amazonaws.com%2Fuploads%2Farticles%2Fcdv48xxobqzniwcb88ew.png" alt="Path 2 — Token Broker with Session DB" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The architectural appeal.&lt;/strong&gt; Full session control — you can revoke individual sessions, track devices, enforce idle timeouts, support multiple identity providers without each one polluting the cookie payload. Vercel cost goes away. The downstream contract is preserved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it stops looking right — implementation level.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We now own the entire auth substrate.&lt;/em&gt; The biggest hidden cost in this path isn't in any individual diagram; it's in the operational total. A new database layer to provision, scale, and back up. A new session mechanism to write, test, patch, and version. A new piece of critical infrastructure to bring into the on-call rotation. Security updates and resilience improvements that previously came "for free" from the vendor stack — Amplify patches, Cognito-side improvements — now have to be applied (or replicated) by us. Auth is the part of a system you most want operated by people who specialize in operating auth; this path moves you in the opposite direction.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The session DB becomes a hot path.&lt;/em&gt; Every API call now pays a DB lookup latency. Even at with high-throughput DBs (like Dynamo) that's added to every request that crosses the system boundary, and the database becomes the system's availability ceiling for any authenticated traffic.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Database choice is a one-way door.&lt;/em&gt; DynamoDB is the AWS-native default, but if you ever need a relational session model — joins, audit trails, complex revocation rules — you're either retrofitting an inappropriate store or migrating. ElastiCache buys sub-millisecond reads but adds VPC complexity. Aurora + RDS Proxy buys SQL but needs explicit guard-rails against connection storms. Each is sticky once chosen.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Observability fragments.&lt;/em&gt; There's now an extra hop, possibly in a different language, in a different deployment unit. Distributed tracing has to follow it. Failures in the broker look different from failures in the backend — and that distinction matters during the worst incident calls, when you need to localize a problem in under five minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is where it's worth distinguishing stateful complexity from feature complexity.&lt;/strong&gt; Both Path 1 and Path 2 add complexity, but in different shapes. Path 1's complexity is &lt;em&gt;operational&lt;/em&gt; — a new layer that needs to scale and stay up. Path 2's complexity is &lt;em&gt;stateful and ownership-shaped&lt;/em&gt; — a new system-of-record whose failure modes are harder to reason about, whose data has independent lifecycle from your application, and whose operation you now own end-to-end. Stateful complexity tends to be more expensive in the long run because state attracts more state, and ownership tends to be more expensive than people estimate at decision time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict shift.&lt;/strong&gt; Justified only if you actually need the session-control features that come with it (multi-IdP, fine-grained revocation, device tracking as a product feature). Otherwise, you've taken on durable architectural debt — and a permanent operational burden — to solve a hosting-cost problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Path 3: API Gateway Lambda authorizer reading cookies
&lt;/h2&gt;

&lt;p&gt;The cleanest path &lt;em&gt;if you control the gateway&lt;/em&gt;. Skip the BFF entirely. Configure the API Gateway with a custom Lambda authorizer that reads the auth credential from a cookie instead of the &lt;code&gt;Authorization&lt;/code&gt; header. Browsers send the cookie automatically on requests with &lt;code&gt;credentials: 'include'&lt;/code&gt;; the authorizer extracts and validates it; downstream services receive a Bearer token injected by the gateway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shape:&lt;/strong&gt; HttpOnly cookie carries the Cognito JWT directly. The Lambda authorizer reads the cookie, validates the JWT against Cognito's JWKS endpoint (a library like &lt;a href="https://github.com/awslabs/aws-jwt-verify" rel="noopener noreferrer"&gt;&lt;code&gt;aws-jwt-verify&lt;/code&gt;&lt;/a&gt; handles the signature and claims validation cleanly), and tells the gateway to inject the token into the &lt;code&gt;Authorization&lt;/code&gt; header for the downstream call.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fuwikq2y7yvfazdx6gmim.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.amazonaws.com%2Fuploads%2Farticles%2Fuwikq2y7yvfazdx6gmim.png" alt="Path 3 — API Gateway Lambda Authorizer" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the architecture level, this is the most elegant of the four paths. No BFF. No session DB. No Vercel cost increase. Tokens never reach JS. Downstream services keep their existing JWT-from-header contract because the gateway re-injects it. Lambda authorizer responses are cached by API Gateway (default 300s), so the validation cost amortizes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it stops looking right — implementation level.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cookie size limits.&lt;/em&gt; Constraint #5 was waiting for this option. A full Cognito ID token plus an access token plus a refresh token can easily exceed the 4KB per-cookie browser limit. And on every request the browser sends, those cookies sit inside the total header size budget — most API Gateways cap around 8KB. Hit the cap, and the gateway returns HTTP 431 &lt;em&gt;before any of your code runs&lt;/em&gt;. This isn't a hypothetical; it's a documented production failure mode for Cognito users who try this naïvely.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Refresh token handling migrates into the auth infrastructure.&lt;/em&gt; This is the surface most underestimated at decision time. In the JS-readable token world, the auth library handles refresh transparently from the client: the client asks for a session, the library checks expiry, refreshes if needed, returns the token. The client never sees the mechanics. In this model, the client can't see expiration at all — and the authorizer's job is structurally "allow or deny," not "issue new credentials."&lt;/p&gt;

&lt;p&gt;So who refreshes? Three sub-options, each with cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;The authorizer itself,&lt;/em&gt; on every expired-token detection — but it then needs to write new cookies onto the response, which is awkward in an authorizer's lifecycle, and concurrent requests can stampede the refresh endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;A dedicated refresh route handler&lt;/em&gt; the client calls when it gets a 401 — this is the cleanest of the three, and the future state we ended up optimizing toward. It has well-defined seams, it's easy to test, and the client-side 401-then-refresh-then-retry pattern is a standard piece of HTTP plumbing when implemented carefully (with refresh deduplication so concurrent 401s don't trigger N parallel refreshes).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Pre-emptive refresh from middleware&lt;/em&gt; — but middleware doesn't have a natural seam to mutate cookies on outbound responses in a way Next.js, the browser, and the gateway all agree on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are blockers. All of them are real work. And refresh is the part of auth where bugs become production incidents fastest, because they manifest as intermittent 401s that are hard to reproduce.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;CSRF surface opens.&lt;/em&gt; When the credential travels in an &lt;code&gt;Authorization&lt;/code&gt; header set by your code, CSRF is structurally impossible — the attacker's page can't make the browser attach a custom header. When the credential travels in a cookie automatically attached by the browser, CSRF re-enters the threat model. You now need &lt;code&gt;SameSite=Lax&lt;/code&gt; (or &lt;code&gt;Strict&lt;/code&gt;), explicit anti-CSRF tokens for state-changing operations, or both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The non-technical reason this path didn't win — for us, this round.&lt;/strong&gt; Even with the refresh-route-handler model as the cleanest variant, we didn't pick Path 3, and the reason wasn't technical. The Lambda authorizer sat in another team's ownership, and that team had an active RBAC initiative consuming most of their bandwidth. Kicking off the cookie-authorizer rewrite at that moment would have meant either derailing their roadmap or eating cross-team coordination overhead that would have stalled both efforts. The right time for Path 3 was &lt;em&gt;after&lt;/em&gt; the RBAC work landed — which gave us a natural future state to optimize toward, rather than a path to force through now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth naming: architecture is org-shaped.&lt;/strong&gt; The "right" technical path is sometimes blocked not by technology but by ownership and timing — another team owns the relevant component, that team has competing priorities, kicking off a cross-team effort would derail their roadmap. That's not a failure mode; it's a constraint. Name it explicitly in the analysis, and shape today's decisions around the future state where the timing flips — rather than pretending the org-state is somebody else's problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict shift.&lt;/strong&gt; Cleanest of the active fix paths if downstream services and gateway are yours to modify &lt;em&gt;and&lt;/em&gt; the timing is right with the team that owns them. Costly enough in implementation work — particularly the refresh flow — that it should be entered into with eyes open, not signed up for at the elegant-diagram stage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Path 4: Conscious deferral + foundation work
&lt;/h2&gt;

&lt;p&gt;The "boring" option. Presented last because it only earns its place after the first three have lost some shine.&lt;/p&gt;

&lt;p&gt;Keep the current non-HttpOnly cookie storage. Mitigate the XSS surface via the usual hardening (strict CSP, short-lived access tokens, careful XSS posture in dependencies). Document the risk acceptance explicitly. Define what triggers the eventual migration. &lt;em&gt;Do the foundation work now&lt;/em&gt; that makes the eventual migration cheap when the trigger fires.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Flgsqpo16fduna0ac4jg7.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.amazonaws.com%2Fuploads%2Farticles%2Flgsqpo16fduna0ac4jg7.png" alt="Path 4 — Conscious Deferral with Foundation Work" width="799" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not "do nothing." It's a substantial decision with substantial work attached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the foundation work actually looked like:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrated from an older Amplify version that used self-hosted login pages — which structurally could not support HttpOnly cookies — to Amplify v6 with &lt;code&gt;ssr: true&lt;/code&gt; and Cognito Managed Login. That's the version whose experimental HttpOnly feature &lt;em&gt;exists&lt;/em&gt;. Moving to it converts the eventual migration from a six-month re-architecture into a config flag.&lt;/li&gt;
&lt;li&gt;Re-pointed the application's login UX from self-hosted pages to Cognito Managed Login, which is a prerequisite for Amplify's HttpOnly mode and also de-risks the future migration (no custom login UI to retrofit).&lt;/li&gt;
&lt;li&gt;Wrote an explicit risk acceptance — not a passing mention in a Slack thread, but a stakeholder-signed document describing the vulnerability we were knowingly keeping live, the mitigations in place, and the triggers that flip the decision.&lt;/li&gt;
&lt;li&gt;Defined the migration trigger. The primary trigger was the sunset of legacy frontend systems that were structurally incompatible with the new auth flow. Until those were gone, migrating the new system wouldn't actually have eliminated the vulnerability at the org level. A secondary trigger was a parallel RBAC initiative that would create a natural alignment point for backend/infra teams to revisit authentication anyway.&lt;/li&gt;
&lt;li&gt;Captured the full decision and option analysis — including the work that went into Paths 1-3 — so the next architect picking this up doesn't redo months of research.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The architectural appeal.&lt;/strong&gt; No disruption to a 100+ service system. Full understanding captured. The eventual fix is now &lt;em&gt;cheap&lt;/em&gt;, not &lt;em&gt;prohibitive&lt;/em&gt;. The decision is reviewable, with clear conditions under which it should be revisited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the work that separates "ship the broken pattern" from "defer the fix with intent."&lt;/strong&gt; When the right answer is "don't change the externally-visible behavior yet," the work that earns that answer is invisible from the outside — but it's the work that determines whether the eventual change is a flag flip or a re-architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict.&lt;/strong&gt; In &lt;em&gt;this&lt;/em&gt; constraint stack, with these specific triggers visible in the roadmap, the right answer. Anchored explicitly: in a different stack (no Vercel, smaller backend, fewer downstream services, no upcoming roadmap alignment point), the verdict could land on any of Paths 1-3.&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision matrix
&lt;/h2&gt;

&lt;p&gt;Compressing everything above into a comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Frontend impact&lt;/th&gt;
&lt;th&gt;Backend impact&lt;/th&gt;
&lt;th&gt;Cost surface&lt;/th&gt;
&lt;th&gt;New attack surface&lt;/th&gt;
&lt;th&gt;Reversibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Path 1 — Next.js BFF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (all client API calls re-routed through Next.js)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Vercel compute doubles on proxied traffic&lt;/td&gt;
&lt;td&gt;CSRF if cookies auto-attached&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Path 2 — Broker + session DB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low (gateway untouched)&lt;/td&gt;
&lt;td&gt;DB hot path; new infra cost&lt;/td&gt;
&lt;td&gt;CSRF + new failure mode&lt;/td&gt;
&lt;td&gt;Low (DB choice is sticky)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Path 3 — Gateway authorizer w/ cookies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Medium (refresh flow re-architecture)&lt;/td&gt;
&lt;td&gt;Medium (new Lambda authorizer + gateway config)&lt;/td&gt;
&lt;td&gt;Modest (Lambda invocations, amortized by cache)&lt;/td&gt;
&lt;td&gt;CSRF + cookie size risk + new refresh failure modes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Path 4 — Defer + foundation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low (substrate upgrade)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Unchanged (deferred)&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The matrix shifts if specific constraints flip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Not on Vercel?&lt;/em&gt; Path 1's cost surface mostly disappears. It becomes a strong default again.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Don't control the API Gateway?&lt;/em&gt; Path 3 evaporates.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Need session revocation as a product feature?&lt;/em&gt; Path 2 moves from over-engineered to required.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;No legacy frontend dependency?&lt;/em&gt; Path 4 loses its trigger and you go pick a real fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of sensitivity is the point. The verdict isn't intrinsic to the path; it's intrinsic to the path &lt;em&gt;plus&lt;/em&gt; the constraint stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where architectural thinking ends and architectural judgment begins
&lt;/h2&gt;

&lt;p&gt;The meta-point of this article, explicitly:&lt;/p&gt;

&lt;p&gt;Architectural thinking, by itself, can absolutely tell you that Cognito tokens shouldn't be readable in JS. It can tell you that the broad shapes of the fix involve some combination of server-held state, BFF proxying, or gateway-level credential extraction. It can sketch you a clean diagram for each path and identify their structural trade-offs.&lt;/p&gt;

&lt;p&gt;Architectural thinking &lt;em&gt;alone&lt;/em&gt; cannot tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That Vercel's billing model makes Path 1 expensive in a way that doesn't show up on the diagram.&lt;/li&gt;
&lt;li&gt;That Cognito tokens are too big to fit inside cookies the way Path 3 wants them to.&lt;/li&gt;
&lt;li&gt;That moving from one auth library version to another &lt;em&gt;is itself&lt;/em&gt; the substrate work that makes the eventual fix cheap.&lt;/li&gt;
&lt;li&gt;That vendor support (AWS Premium Support in our case) can give you two contrary answers if you ask twice — and that both can be correct under different assumptions.&lt;/li&gt;
&lt;li&gt;That your stakeholders can accept "we're deferring the fix" if you give them an artifact, instead of just giving them a meeting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These details don't live in the architecture diagrams. They live in implementation specifics, vendor docs, billing models, threat models, organizational state, and roadmap context. The job is to &lt;em&gt;go get them&lt;/em&gt; — read the vendor docs to the bottom, run the cost numbers, sketch the migration in enough detail that the unknowns become visible — and feed them back into the architectural choice.&lt;/p&gt;

&lt;p&gt;That's the thing worth leaving you with. &lt;strong&gt;Architecture is a discipline you do all the way down to implementation depth, or it's not architecture — it's a slide deck.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Acceptance of an existing flawed pattern can be a legitimate architectural outcome. So can adopting an experimental vendor feature, or building a new infrastructure component, or kicking the decision to the team that owns the gateway. What separates a good outcome from a bad one is whether the choice was made &lt;em&gt;with&lt;/em&gt; full visibility into its second- and third-order consequences, or &lt;em&gt;without&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Surfaced throughout the article, collected here for reference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Map options against constraints, not features.&lt;/strong&gt; A pattern's quality is a function of context; the same pattern can be right or wrong in different stacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distinguish stateful complexity from feature complexity.&lt;/strong&gt; Both are complexity; they fail differently and accumulate differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use foundation moves.&lt;/strong&gt; Modernize substrate now to make future fixes cheap. The work is invisible from outside but determines the cost of the eventual decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ADR-as-artifact.&lt;/strong&gt; When you defer a decision, the deliverable is the documented decision and risk acceptance — not the absence of the change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture is org-shaped.&lt;/strong&gt; The "right" technical path is sometimes blocked by ownership and timing rather than technology. Name it explicitly in the analysis; design today's decision around the moment the timing flips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor support is an input, not a conclusion.&lt;/strong&gt; Useful, often correct, never the last word. Ask twice; you'll get different answers when you clarify constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When deferring, write the trigger.&lt;/strong&gt; A deferral without a defined trigger is procrastination. With one, it's a roadmap entry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk implementation details for every option.&lt;/strong&gt; Without it, architecture is artificial in the best case, severely misleading in the worst.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I wrote a companion piece on LinkedIn about the narrative side of this — what it felt like inside the team to spend weeks researching a security fix and then deliberately ship the pattern the pentest had flagged. [Link soon to come]&lt;/p&gt;

&lt;p&gt;The technical depth here is the substantiation. The thinking pattern — implementation-aware architecture — is the actual point.&lt;/p&gt;

&lt;p&gt;If your team is about to make an expensive-to-reverse architecture decision and wants an independent second pair of eyes, that's exactly the kind of work I run as part of architecture and cloud advisory engagements through Luckylabs. The analysis above is roughly the format of a typical engagement, applied to one specific problem. If it sounds useful, the fastest way to reach out is &lt;a href="mailto:dragos@dbln.me"&gt;dragos@dbln.me&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>security</category>
      <category>aws</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Sketching out the extremes: an approach to designing software architectures in highly unpredictable projects</title>
      <dc:creator>Bilaniuc Dragos</dc:creator>
      <pubDate>Mon, 12 Aug 2024 07:29:23 +0000</pubDate>
      <link>https://dev.to/dragosbln/sketching-out-the-extremes-an-approach-to-designing-software-architectures-in-highly-unpredictable-projects-1hd5</link>
      <guid>https://dev.to/dragosbln/sketching-out-the-extremes-an-approach-to-designing-software-architectures-in-highly-unpredictable-projects-1hd5</guid>
      <description>&lt;p&gt;Imagine a scenario: a fresh, ambitious project with enormous potential but limited resources. The stakes are high, and the product needs to hit the market as soon as possible to validate its potential and seize the opportunity. The roadmap is hazy, making flexibility crucial.&lt;/p&gt;

&lt;p&gt;Classic story until now. But here’s where things get spicy: the company that started the project has a huge community ready to jump on this new product they’ve been advertising for months. Thus, at the initial beta release, the system must be prepared to jump from 10 internal people testing it to anywhere from 20-100k daily active users and millions of events per hour.&lt;/p&gt;

&lt;p&gt;This mix of conditions creates a particular set of requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the system must be &lt;strong&gt;simple&lt;/strong&gt; enough to be handled by a few devs at the beginning, with short iteration time and cost-effectiveness being key&lt;/li&gt;
&lt;li&gt;it must be &lt;strong&gt;scalable&lt;/strong&gt; enough to handle the spike of users around the initial release date&lt;/li&gt;
&lt;li&gt;it must be &lt;strong&gt;flexible&lt;/strong&gt; enough to permit a rapid move toward scalability later on, delivering more and more complex features along the way&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The process
&lt;/h2&gt;

&lt;p&gt;The first step was to create an architecture that would satisfy all these requirements. &lt;/p&gt;

&lt;p&gt;As a preparation, I read and re-read the following books:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.amazon.com/Fundamentals-Software-Architecture-Comprehensive-Characteristics/dp/1492043451" rel="noopener noreferrer"&gt;Fundamentals of Software Architecture: An Engineering Approach&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.com/Building-Microservices-Sam-Newman-ebook/dp/B09B5L4NVT" rel="noopener noreferrer"&gt;Building Microservices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321" rel="noopener noreferrer"&gt;Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drawing on the insights from these resources, along with my prior experience, I knew that some form of distributed architecture would be essential to handle the expected traffic.&lt;/p&gt;

&lt;p&gt;Roughly speaking, I followed these steps to arrive at an initial sketch of how the system would look like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Analyse requirements and &lt;strong&gt;gather events&lt;/strong&gt; that will need to be handled by the system&lt;/li&gt;
&lt;li&gt;Group those events into &lt;strong&gt;domain boundaries&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Identify &lt;strong&gt;architecture characteristics&lt;/strong&gt; (scalability, security, robustness - these kinds of things)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That initial sketch looked like this (in order to protect the intellectual property of the company, I anonymized all event and module names, keeping only the shape of the sketches):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjhaht8zinlw81svb7nt2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjhaht8zinlw81svb7nt2.png" alt="Diagram first version"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://drive.google.com/file/d/1tWDXhaaZA0qOCKnevK51Vf8-p38ZBVsf/view?usp=sharing" rel="noopener noreferrer"&gt;Readable version here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After lots of refinement and discussion sessions, two things became clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one big chunk of code (i.e a monolithic architecture) wouldn’t fit our scalability and flexibility needs&lt;/li&gt;
&lt;li&gt;a full-blown microservice architecture would be too complex and costly at this stage of the game&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, as with most things in life, the solution was somewhere in the middle. Finding that middle was now the challenge.&lt;/p&gt;

&lt;p&gt;Here’s where the approach that I’m proposing comes into play:&lt;/p&gt;

&lt;h2&gt;
  
  
  Sketching out the extremes
&lt;/h2&gt;

&lt;p&gt;Initially, the mindset was to design the simplest architecture that would satisfy our requirements and see if I noticed any specific areas of risk/improvement.&lt;/p&gt;

&lt;p&gt;The simplest architecture I could think of was a &lt;strong&gt;modular monolith&lt;/strong&gt;, where those modules would be as separated and clearly defined as possible. It looked something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F64p6s1mbo9x6jrv1lt3z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F64p6s1mbo9x6jrv1lt3z.png" alt="Architecture V1"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://drive.google.com/file/d/1CxW2T8DDMv0hw5KmKpHxwXbBdkySZx-P/view?usp=sharing" rel="noopener noreferrer"&gt;Readable version here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The thing that I thought would be the “secret sauce” here was the &lt;strong&gt;data access library&lt;/strong&gt;, a library shared between all modules, it would sit in front of the database layer and would be designed in such a way that extracting a module into its own microservice would take as little effort as possible, and we would only have to tamper with the shared library.&lt;/p&gt;

&lt;p&gt;As a bonus, and to be prepared “even more” for the move towards microservices, I was planning to deploy each module into its own independent function on the cloud infrastructure we were planning to use.&lt;/p&gt;

&lt;p&gt;After analyzing the approach a little more, two things became obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;implementing the “secret sauce” library in the way that I imagined wasn’t realistic, and it would lead to lots of complications&lt;/li&gt;
&lt;li&gt;deploying modules in different containers that accessed each other's databases risked creating a &lt;strong&gt;"Distributed Monolith"&lt;/strong&gt;—a problematic architecture that combines the worst aspects of monolithic and distributed systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, since I went to one extreme, i.e. the cheapest/fastest one, I thought it would be interesting to go to the other one as well: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What would the architecture look like, if we had all the resources we needed to create it?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After some more sketching, I arrived at this rough design of a microservice architecture for our system (I only highlighted some the inter-service events here):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hw5rnewr4u0u939xdov.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hw5rnewr4u0u939xdov.png" alt="Architecture V2"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://drive.google.com/file/d/1CMwSF_n2WJBvUIt6QmJllG6QEn_bnKRX/view?usp=sharing" rel="noopener noreferrer"&gt;Readable version here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this version, we have some clear advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;individual scalability&lt;/strong&gt; of each module, based on loads&lt;/li&gt;
&lt;li&gt;clear &lt;strong&gt;ownership of the data&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;each module can be &lt;strong&gt;independently deployed&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, these advantages come at a cost: &lt;strong&gt;complexity&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Most of that complexity came from the inter-service communication. In V1 of the architecture, if module C needs data that is stored in database E, it can directly access it via the shared library. In this version, it has to request the data from Module C &lt;strong&gt;over the network&lt;/strong&gt; - which adds more points of failure and makes tracking and debugging more difficult.&lt;/p&gt;

&lt;p&gt;But even though this wasn’t the best solution for our current situation, it led us to some important insights based on which we built the actual architecture of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;After taking a closer look, we noticed that most of the inter-service communication was happening between 3 of the 6 modules: Module C, Module D, and Module F. If the network communication between those services was somehow taken out of the picture, the remaining microservices would have a reasonable number of events and requests to share between them.&lt;/p&gt;

&lt;p&gt;So that is exactly what we did: compressed the 3 highly interrelated modules into a single modular microservice, and transformed the other modules into their own microservices. &lt;/p&gt;

&lt;p&gt;After some more tweaking and refining, this is the architecture we’ve come to:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsc0p31g752gfrw6hjgz7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsc0p31g752gfrw6hjgz7.png" alt="Architecture V3"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://drive.google.com/file/d/1ReSScUSipffLwXTS13esrxdbPPiBmQYI/view?usp=sharing" rel="noopener noreferrer"&gt;Readable version here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the advantages of this architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it retained the &lt;strong&gt;scalability advantages of microservices&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;the downsides of inter-service communication were reduced to a manageable level, enough so that our small initial team could &lt;strong&gt;implement the architecture in an efficient manner&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;the "big" microservice was &lt;strong&gt;implemented modularly&lt;/strong&gt;, allowing for future extraction of individual modules if needed&lt;/li&gt;
&lt;li&gt;the established microservice framework meant there would be &lt;strong&gt;less friction&lt;/strong&gt; when splitting additional components into new microservices&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Oftentimes, as software developers and architects, we are expected to come up with the ideal solution for any set of circumstances, requirements, or problems. We are aware, however, that everything in software development is a tradeoff.&lt;/p&gt;

&lt;p&gt;While this little story isn’t about promoting any specific type of software architecture, its purpose is to showcase a promising strategy for building software architectures, and it sounds like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;When trying to figure out where exactly to stop between two extremes, a good starting point is to sketch out how both of these extremes would look like.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In terms of software architecture, designing the cheapest and fastest architecture that would sort of satisfy the requirements, and putting it next to the most comprehensive and scalable solution can be very insightful. It can bring a better understanding of the system, its risks, and possibilities - as well as provide a clearer general direction for the product as a whole.&lt;/p&gt;

&lt;p&gt;In our case, this strategy showed us how a little tweak in the way we were organizing our components allowed us to aim much closer to the “ideal” microservice architecture than we had initially thought was possible. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;What insights would such an approach bring to your project and team?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>microservices</category>
      <category>learning</category>
    </item>
    <item>
      <title>How I stopped freaking out when speaking to people | A journey with 4 lessons</title>
      <dc:creator>Bilaniuc Dragos</dc:creator>
      <pubDate>Fri, 23 Jul 2021 16:33:27 +0000</pubDate>
      <link>https://dev.to/dragosbln/how-i-stopped-freaking-out-when-speaking-to-people-a-journey-with-4-lessons-24jn</link>
      <guid>https://dev.to/dragosbln/how-i-stopped-freaking-out-when-speaking-to-people-a-journey-with-4-lessons-24jn</guid>
      <description>&lt;p&gt;Imagine: You're in 6th grade. Your English teacher gives you the main role in a poetry event that will appear on TV. On the big day, you walk on the stage. The camera turns upon you. Everyone's eyes turn upon you. You take a deep breath, and… blank.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IRDlaNEI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/he5jlp4i3jytpmhjwiwi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IRDlaNEI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/he5jlp4i3jytpmhjwiwi.jpg" alt="dark image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I always like to point to that story as the reason for my excessive fear of speaking in front of people. Of course, there must be lots of other reasons for it, but having a brain fart at 11 years old - on TV and in front of everyone - sounds dramatic enough, so we'll stick to that. The fact is, this became one of my deepest fears.&lt;/p&gt;

&lt;p&gt;Learning to live with this fear - and (spoiler alert) going from that panicked little kid to winning the Speaker of the Year award in front of over 300 people - was the most transformational journey of my life. It taught me some of the most important lessons I know, so I decided to write about them. Maybe they'll help you, too&amp;nbsp;:)&lt;/p&gt;

&lt;h2&gt;
  
  
  Denial
&lt;/h2&gt;

&lt;p&gt;For as long as I can remember, whenever I'd have to speak in front of people, my heart would start thumping, my mind would start racing and I'd start blurting out words rapidly and with no coherence. And the fact that &lt;em&gt;I was in complete denial of this fear&lt;/em&gt; didn't help at all.&lt;/p&gt;

&lt;p&gt;I quickly fell into a very nasty loop: I'd try to mask my insecurities with "confidence hacks" from youtube, people around me would smell that and become even more repulsed, which confirmed my insecurities, which in turn made me seek even more tricks and hacks… and so on.&lt;/p&gt;

&lt;p&gt;And this loop led me into a really dark place. I couldn't feel an authentic connection with anyone - not because there was anything wrong with the people around me, but because I never allowed them to see who I really was.&lt;/p&gt;

&lt;p&gt;All I was doing was keeping up this facade of fake confidence, and getting disappointed and frustrated every time it would inevitably fail to solve my problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--z5ORQUQZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w24rstik8desxkoz9in.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--z5ORQUQZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w24rstik8desxkoz9in.jpg" alt="masks"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Change
&lt;/h2&gt;

&lt;p&gt;The trigger for change came at a leadership event that I participated in. I was my usual self, feeling awkward and trying to mask it with tricks and hacks.&lt;/p&gt;

&lt;p&gt;And then, there was this guy - speaking calmly, telling the right jokes at the right times, never really "trying" anything, but making everyone feel good - especially when he got in front of the group. Everyone wanted to be around him. And I wanted to &lt;em&gt;be him&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Following his advice, I joined Toastmasters, an organization that had regular meetings for practicing public speaking - and this is where the real pain of confronting my fears and insecurities began.&lt;/p&gt;

&lt;p&gt;At first, I was obsessively practicing my speeches (30–40–50 times!), just to make sure what happened in 6th grade would never happen again. And it went quite well, insofar as blocking on the stage went.&lt;/p&gt;

&lt;p&gt;But the painful part came after a speech was over, and I would look over the recording. I could barely stand to look for more than 2 minutes - that's how unnatural and awkward I saw myself on the stage. All my insecurities were exposed in front of everyone. And that was a hard pill to swallow.&lt;/p&gt;

&lt;p&gt;The only thing that kept me going was gradually learning the lessons I'll talk about. The made me stick to this journey, one painful step at a time, until I was holding this speech:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3-rij92I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o8gasmqx0r38ryqgygbp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3-rij92I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o8gasmqx0r38ryqgygbp.jpg" alt="speech"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;that ended up with me holding this beauty, as one of the biggest achievements of my life:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jBtcL3DP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tqypv5cfloppph74l5cy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jBtcL3DP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tqypv5cfloppph74l5cy.jpg" alt="award"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, on to the lessons…&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The most powerful thing to know is that it's okay to fuck&amp;nbsp;up
&lt;/h2&gt;

&lt;p&gt;And I'm not saying it in a make-me-feel-better way. This is actually the best way to minimize the mental chatter that's such a big part of fear.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;Most times, fear would begin with the question "what happens if I fuck up?", which would trigger an endless loop of apocalyptic scenarios in my head - scenarios that were totally ungrounded in reality. Once I accepted that it's okay to fuck up, I could short-circuit the loop by answering that question with a realistic "nothing much".&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jEz0FJWt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8aqkvkvfexhmu3ke04dy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jEz0FJWt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8aqkvkvfexhmu3ke04dy.jpg" alt="shrug"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Toastmasters taught me that environment can play a crucial role here. If the feedback for my first speech was an honest "you suck, just go home" - you can bet I'd still be filling the pockets of "hack your confidence" gurus on youtube.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;Instead, the message was always: it's not a big deal - it happens to everyone. And that's about the most empowering message you can get when confronting fear and insecurities.&lt;/p&gt;

&lt;p&gt;Another thing that really helped me with this point was seeing other people around me awkwardly trying to confront the same fear and fucking up themselves. Knowing that I was not the only one struggling (and failing) was what kept me going, during some of my most intense moments of self-criticism.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;And this brings me to my second eye-opening lesson:&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Encouraging others is the best way of dealing with your own&amp;nbsp;fear
&lt;/h2&gt;

&lt;p&gt;And this is something I  didn't expect at all: the most effective method of dealing with my own fear didn't have much to do with me. It was all about encouraging others to confront their own version of this fear of public speaking.&lt;/p&gt;

&lt;p&gt;And not just because it's the right thing to do in a higher, humanitarian way. It's actually the best thing you can do, egotistically speaking. Because when you see someone else dealing with the same kind of fear you're dealing with - and start encouraging them - something amazing happens:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;that fear becomes smaller for both of&amp;nbsp;you&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This was another huge advantage at Toastmasters: being a member, I didn't only give speeches. I would also have to evaluate others, encourage them and give them the best feedback I was capable of. &lt;em&gt;And telling others that they can do it, I suddenly found myself believing it, too!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Find someone that's dealing with the same kind of fear. Encourage them and let them know that it's okay to struggle and even to fail. Then feel the empowerment this simple act brings upon you, in your own struggles.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--o978qBju--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9hrkb33mdtjbqy7oorjd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--o978qBju--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9hrkb33mdtjbqy7oorjd.jpg" alt="friends at the lake"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this lesson kept me fighting, until I finally realised that:&lt;/p&gt;

&lt;h2&gt;
  
  
  3. You don't conquer fear. You learn to live with&amp;nbsp;it
&lt;/h2&gt;

&lt;p&gt;About 2 years passed since I won that trophy that marked the success of my "public speaking project". Since then, I've had quite a lot of events to moderate and speeches to present. In short, I kept gaining experience.&lt;/p&gt;

&lt;p&gt;But just the other day, I was going to this salsa&amp;amp;bachata party where I didn't know anyone. I arrived a little earlier, and there were a few people gathered up in a discussion. As I approached them, my heart started thumping, my thoughts started racing - all the good old friends were right back.&lt;/p&gt;

&lt;p&gt;And this is the biggest takeaway from my journey:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My fear never went away. I just learned to live with&amp;nbsp;it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;More specifically, I learned to expect fear, &lt;em&gt;without expecting that it will ruin everything&lt;/em&gt;. To leave it in the background, and keep going anyways.&lt;/p&gt;

&lt;p&gt;I believe I learned to do this simply through experience. Speech after speech - no matter how much I rehearsed or how small the audience was - the same feelings of fear would arise. And, speech after speech, they would prove to be less tragic than I imagined.&lt;/p&gt;

&lt;p&gt;Eventually, those feelings became just part of the landscape - not going away, but not consuming all my attention, either. And that's how I started to actually enjoying public speaking. That's how my gruesome fight came to an end- not because I won or lost, but because &lt;em&gt;I stopped fighting.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;What I wish I knew then is how much mediation can help with this - since its core training is leaving thoughts and emotions in the background. And it's such an effective way of doing it that, in hindsight, I believe it would've saved me months of pain and frustration I had to go through, just to begin to learn this lesson.&lt;/p&gt;

&lt;p&gt;Just for the record, after trying out lots of apps and techniques, the most effective way of meditating for me right now is through &lt;a href="https://app.www.calm.com/program/mVcvqWcR8C/how-to-meditate"&gt;the guided sessions of Jeff Warren, available on Calm&lt;/a&gt;. If you have a hyperactive mind like mine, he might resonate with you, as well&amp;nbsp;:)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TrcDhcfQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f94vvlwtz0u8cvbfxnne.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TrcDhcfQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f94vvlwtz0u8cvbfxnne.jpg" alt="stones"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the last big lesson this journey had to teach me was that:&lt;/p&gt;

&lt;h2&gt;
  
  
  4. It's absolutely worth&amp;nbsp;it
&lt;/h2&gt;

&lt;p&gt;I believe that one of the most perverse things when it comes to dealing with fear and insecurities is how easily the battle can be lost before it even begins.&lt;/p&gt;

&lt;p&gt;For such a long time, all I did was find circumstances and excuses. I'd tell myself it's alright, I'll eventually cover those insecurities up with confidence hacks. Or I'd have a little fuckup and see it as confirmation that I'll never be able to do it. Or, most often, I'd just delay things, expecting some enlightenment moment where all my fears and insecurities would disappear, just like magic.&lt;/p&gt;

&lt;p&gt;And now, I can confidently say that these were the worst ways in which I was sabotaging myself. Because learning to live with only this one fear, changed the way I looked at every other area of my life.&lt;/p&gt;

&lt;p&gt;More and more often, when faced with insecurities, fears and uncomfortable situations, I found myself thinking:&lt;br&gt;
&amp;nbsp;&lt;br&gt;
&lt;em&gt;"if public speaking didn't kill me, this won't kill me&amp;nbsp;either"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And soon enough, this automatic way of thinking made me take on opportunities that would've terrified me before - opportunities that, among others, ended up sending my professional growth through the roof.&lt;/p&gt;

&lt;p&gt;I gained the courage to begin freelancing, to assume the role of Tech Lead in my projects, and, soon after, to become the CTO of a very promising startup.&lt;/p&gt;

&lt;p&gt;All of these moves came with lots of doubts, fears and insecurities packed in. And the only way I could get over them was through the lessons learned in this seemingly unrelated journey of confronting my deep fear of public speaking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;So, that's about it for my journey. Confronting my fear taught me some of the most valuable lessons I know: that &lt;em&gt;it's okay to fuck up&lt;/em&gt;, that &lt;em&gt;encouraging others pays huge dividends&lt;/em&gt;, that &lt;em&gt;you can live with fear&lt;/em&gt; and that &lt;em&gt;it's totally worth doing it&lt;/em&gt;. I hope you found some value in those lessons, as well.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;But, at the end of the day, this is just one journey among millions. Fear and insecurities are everywhere, and each of us is trying our best to deal with them. If you're struggling, rest assured that you are not alone, not by a long shot.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;And if my specific lessons don't work for you, what will definitely work is what you're already doing now: keeping an open mind, seeking out new ideas and enlarging your own perspectives. A little bit from here, a little bit from there, and that's how you're building up your own recipe for dealing with this existential human condition.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Kudos to you, for keeping an open mind in front of so many fears and insecurities!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pe9CCXwN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/abxjoba9oerahvpl0p2a.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pe9CCXwN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/abxjoba9oerahvpl0p2a.jpg" alt="smiling frog"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>motivation</category>
      <category>leadership</category>
      <category>learning</category>
    </item>
    <item>
      <title>I tracked every minute of my time for the last 4 months. Here are 7 totally unexpected results</title>
      <dc:creator>Bilaniuc Dragos</dc:creator>
      <pubDate>Wed, 14 Jul 2021 13:11:09 +0000</pubDate>
      <link>https://dev.to/dragosbln/i-tracked-every-minute-of-my-time-for-the-last-4-months-here-are-7-totally-unexpected-results-2dna</link>
      <guid>https://dev.to/dragosbln/i-tracked-every-minute-of-my-time-for-the-last-4-months-here-are-7-totally-unexpected-results-2dna</guid>
      <description>&lt;p&gt;A 30 minutes lunch break? Time entry. 12 minutes of arguing with mom? Time entry. 7 minutes of stalking my ex on Instagram? You guessed it!&lt;/p&gt;

&lt;p&gt;That was my life over the last 4 months, being my own time tracking cop while carrying out the most outrageous experiment of my life so far. The results amazed me in ways that I would never have expected, so I decided to write about them. Who knows, maybe you’re also going to find something of value in my journey.&lt;/p&gt;

&lt;p&gt;But first…&lt;/p&gt;

&lt;h1&gt;
  
  
  What the hell made me do it?
&lt;/h1&gt;

&lt;p&gt;To be honest, it was a combination of frustration, guilt and fear — all generated by the fact that I kept setting big goals for myself, but never seemed to make any progress towards them. Time was passing, and I had nothing to show for it.&lt;/p&gt;

&lt;p&gt;All I had were vague reports at work, where “working” covered everything from writing code, to talking to people, to watching Russian slapping contests on youtube. &lt;/p&gt;

&lt;p&gt;I knew I had to do start tracking my time if I wanted to keep my sanity. And I’ve seen how something casual/half-hearted would quickly become too vague to be of any use. If I wanted to do it, I had to go all-in.&lt;/p&gt;

&lt;p&gt;So…&lt;/p&gt;

&lt;h1&gt;
  
  
  How exactly did I do it?
&lt;/h1&gt;

&lt;p&gt;I used an app called Toggl to do all my tracking. I chose this specific app because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it’s available on all platforms and syncs very nicely between them&lt;/li&gt;
&lt;li&gt;it has an intuitive UI, with suggestions that become more and more accurate over time&lt;/li&gt;
&lt;li&gt;it allows for very fine customization and reporting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, I devised 7 main areas where all my entries would fall, using tags and colors to mark them in the app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1whxgy6oalqmzo09yjjo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1whxgy6oalqmzo09yjjo.png" alt="areas"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, I created some high-level projects that I knew I was already spending time on:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjmvgtnv0grk1n7x4rolj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjmvgtnv0grk1n7x4rolj.png" alt="projects"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the setup was done, I would simply start a new time entry, every time I would move from one activity to the other.&lt;/p&gt;

&lt;p&gt;One important note here was that, because I decided to go all-in, I had to be particularly careful not to have any entries “cover up” unrelated things that I was doing. For example, if I would be writing code on my project and I’d receive a call, I’d have to create a separate time entry for it, even if it only took 5 minutes.&lt;/p&gt;

&lt;p&gt;This level of strictness was a bit taxing at the beginning, but after a while, Toggl’s UI came in very handy. I rarely had to create new entries. Most of the times when I’d switch to something else (even just short interruptions), I would just have to press “continue” on a previous entry.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed886sh38nmdoryvk4or.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed886sh38nmdoryvk4or.png" alt="continue"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the end of the week, here’s how my timetable would look like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw2hb3mi44c4z062mlafa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw2hb3mi44c4z062mlafa.png" alt="timetable"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And now that the “how” is also out of the way, let’s move on to the exciting stuff. Here are the biggest changes I noticed, over these 4 months:&lt;/p&gt;

&lt;h1&gt;
  
  
  1. Drastically improved my focus and productivity
&lt;/h1&gt;

&lt;p&gt;Yep, the biggest impact of this time tracking journey didn’t come in the form of a mind-blowing insight at the end of the week, when I was looking over my timetable. The biggest impact was in how I was spending my time &lt;em&gt;while I was doing the tracking.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At first, nothing big happened. But after a while, I found myself more and more often in an interesting situation. Whenever I was working and felt the need to “zone out” by jumping “just a bit” on youtube or Instagram, I’d find myself thinking “nah, I don’t want to start a new entry now. I’ll just get this thing done, and then take a bigger break for that”.&lt;/p&gt;

&lt;p&gt;And, more often than not, I wouldn’t even feel that need for youtube or Instagram anymore, once I actually finished the task. The feeling of accomplishment was enough to give me the refreshment I needed.&lt;/p&gt;

&lt;p&gt;This increase in focus sent my productivity through the roof. Yes, I’ve always read and heard of how context switching is a very bad idea. But, until I tracked it down, I never knew how often I was doing it — and I never knew that getting rid of it would make me get things done literally twice as fast! After seeing all this for myself, I felt like I somehow hacked my own life.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Saved a lot of time from unexpected places
&lt;/h1&gt;

&lt;p&gt;One of the most shocking insights that I had in the beginning, after looking at my reports for a week, was that I was spending over 20 hours a week on food-related activities (buying food, cooking, eating).&lt;/p&gt;

&lt;p&gt;Every day, 3 times a day, I would take some time off to cook. And, since eating healthy was always an important thing for me, I didn’t feel bad if it sometimes took a little longer. But I never knew I was doing the work of a part-time cook out there! At least if I’d learned to cook something more exciting than this boring salad:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fih3ep2rs57plb0ii97h1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fih3ep2rs57plb0ii97h1.jpg" alt="salad"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once I became aware of this, I automatically started thinking about how I could make it a bit more efficient. A simple thing that popped into mind was that, instead of cooking everything from scratch in each of the 3 breaks, I could simply prepare everything in the evening before, and have my food ready to go for the next day, with minimum adjustments.&lt;/p&gt;

&lt;p&gt;This trick solved all my cooking for the day in under 1 hour, instead of 2–3, which amounted to over 10 hours of saved time, per week. And the best part was that I didn’t have to sacrifice anything in terms of how healthy or tasty my food was!&lt;/p&gt;

&lt;p&gt;Such a simple solution. However, I would never have thought of it, if there wasn’t this table that was telling me straight to my face: “here’s how much time you’re actually spending on this thing”.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Formed habits &amp;amp; routines much easier
&lt;/h1&gt;

&lt;p&gt;Turns out time tracking helped with my habits and routines, as well, in two not-so-obvious ways:&lt;/p&gt;

&lt;p&gt;1 - If I would have the same entries at roughly the same hours, Toggl’s mobile app would start suggesting them, in those time intervals. This made my tracking way easier, and turned the clock into a kind of trigger for the habits I was trying to form:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4xypkxk53w0xmoflige.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4xypkxk53w0xmoflige.png" alt="suggestions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2 - There was a deep satisfaction when I would look at my weekly timetable and see all the entries nicely aligned. Here’s one of the weeks that I’m most proud of. Just look at how satisfying those morning entries look:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qh2d9wt03fpeuiqtl2h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qh2d9wt03fpeuiqtl2h.png" alt="habits"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It might sound trivial, but just these two simple tricks were enough to make me form and cement very helpful habits, without thinking too much about them.&lt;/p&gt;

&lt;h1&gt;
  
  
  4. Enjoyed my “chill time” more than ever
&lt;/h1&gt;

&lt;p&gt;Here’s something interesting. This experiment, which was chiefly meant to increase my productivity, had another very unexpected — but very welcomed — effect: it increased the quality of my “chill time”, as well!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8h66gso3imtvopke017p.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8h66gso3imtvopke017p.jpg" alt="chill"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Firstly, as I was doing my work much more efficiently and saving time from all kinds of unexpected places, I found myself with more and bigger blocks of time on my hands. That meant I could fit in some leisure activities that were more time-consuming — but that I really enjoyed doing.&lt;/p&gt;

&lt;p&gt;Secondly, all this increased amount of “chill time” came without the usual guilt I felt before, when I was “chilling” by scrolling through the Instagram feed, while I knew I had to fix that stupid programming bug at work.&lt;/p&gt;

&lt;p&gt;This made me enjoy my free time like never before, and rekindle old passions like salsa &amp;amp; bachata —passions that I forgot how good they made me feel, amongst all this clutter of work and guilt and continuous distractions.&lt;/p&gt;

&lt;h1&gt;
  
  
  5. Made better decisions, overall
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F550qasv72f2vac0cs6w1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F550qasv72f2vac0cs6w1.jpg" alt="choices"&gt;&lt;/a&gt;&lt;em&gt;one of the toughest decisions in life&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Since I was being as honest as I could with those time entries, weighing my decisions wasn’t that complicated anymore: I would look over my timetable, see exactly how much time I spent on something I decided to do, and look at how much I got out of it.&lt;/p&gt;

&lt;p&gt;This way, “Is it a good idea to write that article for my 23rd birthday?” became “Is it a good idea to write that article for my 23rd birthday, given the fact that last week I spent 11 hours, and got nowhere with it?”. Not so hard to see the issue, from that perspective.&lt;/p&gt;

&lt;p&gt;As a bonus, tracking my time solved another one of the biggest problems I had, in terms of decision making. I always had the tendency to overestimate my abilities and take on much more than I could actually handle. This obviously had some very bad consequences, like frustrating other people that were working with me.&lt;/p&gt;

&lt;p&gt;Well, once I had this sheet that was telling me “this is how much it actually takes you to do it” and “this is how much free time you have on your hands right now”, deciding whether I could or could not handle something new became a factual decision, rather than a whim of my arrogant ego.&lt;/p&gt;

&lt;h1&gt;
  
  
  6. I finally felt in control of my time
&lt;/h1&gt;

&lt;p&gt;As I said in the beginning, the irreversible passing of time was one of the biggest sources of stress and anxiety for me. And, while I’m still having my fair share of existential crises about it, doing this experiment drastically reduced that stress and anxiety. The main reason was that, again,&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I finally felt in control of my time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yes, I was still binge-watching cat videos on youtube, probably more than I should. But at least it was a conscious decision, rather than a reaction to some algorithms designed by smart people to make me mindlessly spend hours on end on their website. And this “conscious decision” part was what made all the difference, for me.&lt;/p&gt;

&lt;p&gt;And the coolest thing was that I didn’t have to change anything in the way I was spending my time, in order to feel better about it. Simply having it noted down was enough to give me that empowering feeling of control.&lt;/p&gt;

&lt;h1&gt;
  
  
  7. Felt more tense, uneasy and guilty (at first)
&lt;/h1&gt;

&lt;p&gt;Surprise-surprise: this is not a silver bullet to shoot down all of life’s problems. It actually came with some not-so-nice consequences that I have to mention, if I am to paint an honest picture of my journey.&lt;/p&gt;

&lt;p&gt;First up, I became more tensed and uneasy, especially in the beginning. I think that’s not too unexpected — just imagine having a cop on your back, 24/7, that sees every single thing your doing, and tracks it down on a sheet that’s gonna stare you in the face every Sunday. Quite a reason to feel uneasy…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbc514o3pa2fk2i8xcrxt.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbc514o3pa2fk2i8xcrxt.jpeg" alt="police"&gt;&lt;/a&gt;&lt;em&gt;shoutout to Mr. policeman, for not arresting me when I randomly asked him to pose for my blog&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The other thing was guilt. Again, especially in the beginning. After the first few weeks, I was confronted with a hard truth: I was wasting time. Lots of it. And not in a vague way. I could actually count the hours I spent on things that were totally unproductive, projects that failed and times when I did something completely different than what I had to. Talk about pills that are hard to swallow…&lt;/p&gt;

&lt;p&gt;I eventually managed to keep those bad sides under control through other habits, like meditation. That being said, this journey was definitely not all fun and rainbows.&lt;/p&gt;




&lt;p&gt;So, that’s about it for my journey. Currently, I got so used to this system and it seems to work so well for me, that I’m gonna keep doing it — as long as I’ll be able to keep those negative sides under control.&lt;/p&gt;

&lt;h1&gt;
  
  
  Afterthought: it’s not as hard as it seems
&lt;/h1&gt;

&lt;p&gt;I’ve been showing this article to some friends, to get some feedback before publishing it. A common response was: “Wow, that’s so impressive! You must be so disciplined! I could never do that.” And while it tickled my ego to hear this, the truth is, it’s really not that hard.&lt;/p&gt;

&lt;p&gt;Yes, like all things, it requires effort in the beginning. But after you got it all set up and run it for a few days, it becomes pretty easy. 99% of the time, you’ll just press “continue” on a previous entry, which only takes 5 seconds. And since you’ll have to do it dozens of times a day, it will become instinctive much sooner than the usual “3–8 weeks to form a new habit”.&lt;/p&gt;

&lt;p&gt;Even now, my timer runs “Blog — time tracking” for 9 minutes and 20 seconds. I don’t remember having made a conscious decision to start it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Tips &amp;amp; tricks
&lt;/h1&gt;

&lt;p&gt;If you like the idea and consider trying it for yourself, here are some things I wish I knew when I started, and that I hope will help you in your journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Try to be as honest as possible&lt;/strong&gt;. The benefits are gone if you find yourself scrolling through your Instagram feed, while your timer says “preparing yearly report”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it personal.&lt;/strong&gt; Don’t mix it up with any tracking software from work. Don’t tell anyone about it. Basically, don’t do anything that would make you feel even remotely uncomfortable about recording those 2 hours of binge-watching Japanese eating contests on youtube.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frq6hgm2b4lwugw5nqfqk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frq6hgm2b4lwugw5nqfqk.jpg" alt="private"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep it simple. As simple as possible.&lt;/strong&gt; Don’t overkill it with tags and projects (the way I did, in the beginning). You can always add those later. The whole system should be a pleasure to use, or you’ll have trouble using it at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One step at a time.&lt;/strong&gt; At first, don’t change anything in how you spend your time. Just get used to tracking down what you’re already doing. The main focus is to get used to pressing “play” every time you’re doing something new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be patient with yourself.&lt;/strong&gt; Once you start doing this in full honesty, you’ll probably see that your time isn’t really spent the way you imagined. That’s an easy way for guilt to creep in. But don’t despair. Once you become aware of this, things will get better by themselves. And keep in mind that, just by trying it out, you’ve already done something very courageous that’s a huge achievement in and of itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Tracking every minute of my time is, by far, the most effective thing I’ve ever tried, in terms of becoming more focused, getting more done and improving the quality of my time.&lt;/p&gt;

&lt;p&gt;But, at the end of the day, this is just another way in which someone on this planet is trying to deal with the fact that life is hard. And if my trick doesn’t work for you, what will definitely work is keeping this healthy mindset that makes you read these kinds of articles, in the first place. Kudos to you, for keeping an open mind in the face of life’s hardships!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We’re all trying to make the most out of our time. Thank you for spending some of yours with me, today!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4vvoxex6a5xbfyjg3kz7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4vvoxex6a5xbfyjg3kz7.jpg" alt="light"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PS: This is the first article I’ve ever written, so I think you can imagine how anxious I am about it 😬 Any though (positive or constructive), clap, comment, share, or feedback in any form would be very, very much appreciated!&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>motivation</category>
    </item>
  </channel>
</rss>
