<?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: Vamshi</title>
    <description>The latest articles on DEV Community by Vamshi (@urstruelysv).</description>
    <link>https://dev.to/urstruelysv</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%2F4069066%2Ffe4dbb49-68b0-4cec-9989-70a5e93746b5.png</url>
      <title>DEV Community: Vamshi</title>
      <link>https://dev.to/urstruelysv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/urstruelysv"/>
    <language>en</language>
    <item>
      <title>Stop Doing Auth in React</title>
      <dc:creator>Vamshi</dc:creator>
      <pubDate>Sat, 08 Aug 2026 17:19:00 +0000</pubDate>
      <link>https://dev.to/urstruelysv/stop-doing-auth-in-react-5dn0</link>
      <guid>https://dev.to/urstruelysv/stop-doing-auth-in-react-5dn0</guid>
      <description>&lt;p&gt;Ask where auth lives and you get answers about packages, providers, and a context holding a blob of user data. None of that is auth.&lt;/p&gt;

&lt;p&gt;Auth is two questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication.&lt;/strong&gt; Are you who you say you are?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization.&lt;/strong&gt; May that person do this?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A ban button settles the argument. You click it, and the click means nothing until something confirms you are Sam, Sam owns this room, and Sam may remove people from it. The first is authentication. The other two are authorization. All three get answered somewhere you do not control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it happens
&lt;/h2&gt;

&lt;p&gt;On the server, during a request.&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%2Fs449f3gxxjp6y3g2ilnu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs449f3gxxjp6y3g2ilnu.jpg" alt="A browser request carries a session cookie to a server, which authenticates and authorizes before answering 200 or 403" width="640" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The click is a question, not a decision. A context, a value in localStorage, or a JWT decoded in the browser: none of it survives contact with a server that checks for itself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Requests are the only place both questions have an answer, because a request is the only thing that carries proof. So the follow-up question answers itself. How do you know who the user is in React? You ask. &lt;code&gt;GET /api/me&lt;/code&gt; returns the session for whoever sent the cookie, or a 401.&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;// app/api/me/route.ts&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;cookies&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;next/headers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;getSession&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/session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&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;cookies&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&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="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&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;Response&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="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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;Response&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="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&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;Share that response through a context or React Query once it lands. The provider wrapping your app should hold two states, signed in and signed out, and stay out of the way. Every question past that point is a fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cookie is the boring part
&lt;/h2&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%2Fcoakgkpvn92ypeat0bvl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcoakgkpvn92ypeat0bvl.jpg" alt="Authentication happens once at sign-in; authorization happens again on every protected request" width="640" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sign-in runs once. The right-hand column runs on every request for the rest of the session, and it is the one that decides anything.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The token needs to reach the server on its own, which is what a cookie does and what &lt;code&gt;localStorage&lt;/code&gt; does not. Set it &lt;code&gt;HttpOnly&lt;/code&gt; and &lt;code&gt;Secure&lt;/code&gt; and the browser attaches it to every same-origin request while keeping it away from your scripts. That second property is the one that matters. A token in &lt;code&gt;localStorage&lt;/code&gt; is readable by any script on the page, which includes the analytics tag you added last week, the extension your user installed, and whatever a compromised dependency shipped this morning. It also drifts out of sync with the server, since nothing on the client can tell you a session died.&lt;/p&gt;

&lt;p&gt;Next Auth, Better Auth, and Clerk all put a signed cookie in the right place with the right flags. Use one of them and skip the rest of this paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWTs move the deadline
&lt;/h2&gt;

&lt;p&gt;A signed token flips the model. The server stops looking anything up and starts trusting math, which is fast and which means you cannot take the token back.&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%2Fo9q6cac1cqa9pnsc1zk6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo9q6cac1cqa9pnsc1zk6.jpg" alt="A session cookie revokes immediately, a short-lived JWT within a minute, and a one-hour JWT only after an hour" width="640" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A signed token is a promise you already made. Shortening the window is the whole fix, and it costs you a round trip per minute instead of one per request.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Clerk's answer is a sixty-second window: a JWT lives in the browser, and the SDK refreshes it every minute. Delete an account and the ghost gets one more minute of your permissions. Fifteen API calls fired from a page load all ride the same token without a round trip. That trade reads well to me.&lt;/p&gt;

&lt;p&gt;Whatever you do, keep the payload small. A JWT holding the user's friend list rides along on every single request for the rest of the session, and you already have a way to ask for a friend list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client renders the answer
&lt;/h2&gt;

&lt;p&gt;Say the ban button only appears for mods. That is a fetch, and then a render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;me&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchMe&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;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ButtonSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;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;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BanButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reach for &lt;code&gt;isLoading&lt;/code&gt; rather than &lt;code&gt;isFetching&lt;/code&gt;. The first means no data yet; the second flips whenever any component with that key refetches, so a sibling calling the same endpoint will blank your button for no reason.&lt;/p&gt;

&lt;p&gt;Hiding the button is a courtesy to the user. The server still has to reject the request, because the person you are hiding it from can open the network tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three circles
&lt;/h2&gt;

&lt;p&gt;Providers sell you slices of three separate things, and most arguments about them come from two people comparing different slices.&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%2Fje3sxwlghi8stt5q7zgf.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%2Fje3sxwlghi8stt5q7zgf.png" alt="A Venn diagram maps auth providers across identity verification, user information, and authentication UI" width="606" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every auth product is a claim on some subset of these three circles. The lower slivers stay empty because a login screen with nothing behind it is a login screen, and a users table nobody verifies against is a users table.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Verification is a yes or a no about identity. User info is the record: the email, the avatar, the role, the org. Auth UI is the sign-in page, the account manager, and the Google button you are not allowed to restyle, because Google will fail your OAuth review over the wrong shade of blue.&lt;/p&gt;

&lt;p&gt;Once you see the circles, the real question stops being which library and becomes &lt;strong&gt;who owns the users table.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Own it yourself and joins work. &lt;code&gt;SELECT * FROM users WHERE id IN (friend_ids)&lt;/code&gt; is one query. Better Auth and Auth.js put the table in your database, hand you adapters, and leave the gluing to you. Rent it instead and that join becomes a fan-out of SDK calls, which is fine right up until you build a friends list. Clerk hosts everything, gets you running in five minutes, and works the same from a Next.js route, an Expo app, a Chrome extension, and Swift.&lt;/p&gt;

&lt;p&gt;The middle ring is crowded because the middle is where most products want to be. Stack Auth is Clerk's shape with an exit hatch. WorkOS AuthKit covers all three: authentication, users and organizations, and a hosted UI you can replace with your own. OpenAuth sits alone in the left circle on purpose: a standalone verifier on your own infra with a KV store next to it, and no opinion about who your users are.&lt;/p&gt;

&lt;p&gt;Two entries deserve warnings. Lucia is deprecated, and its author's parting note is worth reading, since maintaining database adapters is what killed it. Passport still works and has not been touched in two years. Firebase Auth is cheap and gets misconfigured at a rate that produced a whole genre of exploit writeups.&lt;/p&gt;

&lt;h2&gt;
  
  
  The middleware argument
&lt;/h2&gt;

&lt;p&gt;Then there is the pattern where auth gets hoisted into &lt;code&gt;proxy.ts&lt;/code&gt; (Next 16's rename of &lt;code&gt;middleware.ts&lt;/code&gt;) and every route in the app pays for it.&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%2F8gh2cvzglsmf2recvmko.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gh2cvzglsmf2recvmko.jpg" alt="Putting auth in proxy middleware adds a blocking verification step to every request; route-level auth checks only the protected route" width="640" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Top: one blocking hop in front of everything, paid by the feed and the blog too. Bottom: the check sits on the route that needs it, and the other three keep their cache. The wall also cannot answer “is Sam a mod of this room”, which is the question you actually had.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two costs. The obvious one is latency: a blocking hop in front of your blog, your feed, and every static page you were about to serve from cache. The other one shows up later, when the check needs to know whether Sam is a mod of room 42. Route patterns cannot express that, so the file grows a copy of your routing logic, then a copy of your permissions model, and it still runs on &lt;code&gt;/rss.xml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Put the check where the data is. Next 16 ships &lt;code&gt;unauthorized()&lt;/code&gt; behind &lt;code&gt;experimental.authInterrupts&lt;/code&gt;, which throws to a 401 and renders your &lt;code&gt;unauthorized.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/dashboard/page.tsx&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;unauthorized&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;next/navigation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;verifySession&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/dal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;DashboardPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;verifySession&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;unauthorized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dashboard&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works from server components, server functions, and route handlers, so the same call covers a page load and the action fired from it.&lt;/p&gt;

&lt;p&gt;One trap while you are down here: a check in &lt;code&gt;layout.tsx&lt;/code&gt; does not cover the pages under it. Layouts skip re-rendering on client-side navigation, so the session goes unchecked on the route change. Call &lt;code&gt;verifySession&lt;/code&gt; in the page too, or better, call it inside the function that reads the data, where nobody can route around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign-in methods, ranked
&lt;/h2&gt;

&lt;p&gt;OAuth first. Someone else runs the password reset flow, eats the support tickets, and takes the blame for the breach.&lt;/p&gt;

&lt;p&gt;Passkeys next. They are genuinely secure and the ceremony still takes five taps, which is the only thing holding them back.&lt;/p&gt;

&lt;p&gt;Magic links work. I find them irritating in a way I cannot fully justify.&lt;/p&gt;

&lt;p&gt;Passwords last, and only if the choice was made for you. Microsoft has published what its own internal password resets cost, and the number has commas in it. Passwords cost money, generate breaches, and make your users' lives worse. When the setup screen lets you leave that box unchecked, leave it unchecked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking one
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You want it working today, on every platform, and you can spend $25/mo past ten thousand users.&lt;/strong&gt; Clerk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want the users table in your own database.&lt;/strong&gt; Better Auth. The plugin list covers 2FA, orgs, passkeys, and OTP, and you own every row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want a hosted product with a real exit.&lt;/strong&gt; Stack Auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want a verifier and nothing else, on your own infra.&lt;/strong&gt; OpenAuth. Budget a couple of days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your customers have a procurement team.&lt;/strong&gt; WorkOS AuthKit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick by what you want to own. Everything else on the list is a data-fetching problem you were going to have anyway.&lt;/p&gt;




&lt;p&gt;The diagrams are interactive in the &lt;a href="https://saivamshi.aethoscompany.in/notes/auth-explained-and-how-to-pick-a-provider" rel="noopener noreferrer"&gt;original version&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>react</category>
      <category>security</category>
    </item>
  </channel>
</rss>
