Supabase Auth: Letting Strangers Through the Door Without Handing Them the Keys

My Go-To Providers  ·  Part 5 of 6

The Fastest Way to Lose a User’s Trust Is to Lose Their Password

I don’t store passwords. Not in my apps, not in my databases, not in environment variables I’ll forget about, not anywhere. Not because I’m lazy, well, partly because I’m lazy, but because I’m honest enough with myself to know that password storage done right is a discipline, not a feature. There are people who’ve spent entire careers thinking about hashing algorithms, timing attacks, credential stuffing, and rainbow tables. I have not. I spent last Tuesday afternoon arguing with a YAML file.

So when it comes to letting users into my apps, I delegate. Hard. And the thing I delegate to is Supabase Auth.

Now, I should be upfront: I use Supabase almost exclusively for its auth layer. I know it’s a broader platform, Postgres hosting, realtime subscriptions, edge functions, storage. Good stuff. But my data and server logic live on Hetzner (covered back in Part 2), and Cloudflare handles my edge (Part 3). Supabase earns its place in my stack for one focused reason: it handles identity so I never have to.

OAuth Providers and Why I Don’t Roll My Own Anything

The core of how I use Supabase Auth is OAuth provisioning. When a user wants into one of my apps, they’re not creating a username and password with me. They’re clicking a button that says “Continue with Google” or “Continue with GitHub” and the entire credential dance happens between them and a provider they already trust.

Supabase acts as the broker. It knows how to talk to Google, GitHub, and a handful of other providers. I configure the OAuth credentials in Supabase’s dashboard, client ID, client secret, redirect URLs, and then Supabase handles the redirect flow, the token exchange, and surfacing a clean session object to my app. I never see a raw credential. I never touch a password. The user authenticates with Google, Google tells Supabase “this person is who they say they are,” and Supabase tells my app “here’s a verified session.”

The providers I’ve actually wired up:

  • Google – Default choice for most projects. Everyone has a Google account, the OAuth flow is fast, and users trust it.
  • GitHub – Goes on anything developer-facing. If I’m building a tool that attracts technical users, GitHub login reduces friction because half of them are already signed in on that browser tab.
  • Email magic link – Supabase’s passwordless email option. Not OAuth in the traditional sense, but the same principle: no password stored, no password managed. User gets a time-limited link, clicks it, they’re in.

I’ve looked at Discord and Twitter/X but haven’t needed them yet. The point isn’t to support every provider. It’s to support the ones your users actually have, and for my audience that’s almost always Google or GitHub.

How the Session Actually Flows

Here’s what actually happens when someone clicks that login button, because understanding the flow is what convinced me this wasn’t magic. It was just well-engineered plumbing.

  1. User hits my app’s login page and clicks “Continue with GitHub”
  2. My app calls Supabase’s signInWithOAuth method, specifying GitHub as the provider
  3. Supabase redirects the user to GitHub’s authorization page
  4. User approves access (or already has, so it’s instant)
  5. GitHub redirects back to a callback URL on my app with an authorization code
  6. Supabase intercepts that, exchanges the code for a token with GitHub, creates or retrieves the user record in its own database, and issues a JWT session token
  7. My app receives that session token and stores it, usually in a cookie or local storage depending on the framework
  8. Every subsequent request my app makes to protected resources includes that JWT, which my backend verifies against Supabase’s public keys
Supabase OAuth Authorization Flow User visits login page Clicks "Continue with GitHub" or Google / Magic Link App calls signInWithOAuth() Supabase client method, provider specified Supabase redirects → GitHub OAuth page User sees GitHub's authorization screen User approves access? No Auth cancelled Return to login Yes GitHub redirects → app callback URL Authorization code included in redirect SUPABASE Exchanges code for token with GitHub Creates / retrieves user in auth.users, issues JWT First-time user? Yes New record created in DB No Existing user record fetched App receives JWT session token Stored in cookie or localStorage Protected requests include JWT Backend verifies signature via Supabase JWKS User is authenticated ✓ Token refresh is handled silently by the Supabase client library. No custom refresh logic required — getSession() always returns a valid session or null.
Supabase OAuth authorization flow — from login click to verified session

The piece that clicked for me was step 6. Supabase maintains its own auth.users table. When a user signs in with GitHub for the first time, Supabase creates a record, an ID, their email if GitHub shared it, their provider, timestamps. From then on, that Supabase user ID is the canonical identifier in my system. I reference it in my own database tables, I use it for Stripe customer mapping (from Part 4), I use it everywhere. GitHub’s internal ID is irrelevant to me. The user could switch to Google login tomorrow; if the email matches, Supabase handles the linking, and my app doesn’t have to care.

What My App Actually Has to Handle

Less than you’d think. This is the part that surprised me when I first set it up with Claude Code’s help. I expected the integration to be sprawling. It wasn’t.

On the frontend, I’m calling maybe three Supabase methods in total:

  • signInWithOAuth() to kick off the flow
  • getSession() to check whether someone’s logged in when the page loads
  • signOut() to clear the session when they’re done

That’s it. Supabase’s client library does the heavy lifting around token refresh. If a user’s JWT is about to expire, the client will silently grab a new one using the refresh token. I don’t write token refresh logic. I don’t write session expiry logic. I check for a session, I get a session or I don’t, I route accordingly.

On the backend, I validate the JWT on protected endpoints. Supabase publishes a JWKS endpoint, a set of public keys, and my server uses those to verify the token signature without making a network call to Supabase on every request. Fast, stateless, and the validation logic is a handful of lines using a well-maintained JWT library, not something I invented myself.

The only thing I have to manage thoughtfully is the redirect URL configuration. OAuth flows require you to register exactly which URLs are allowed as callbacks; this is a security control to prevent redirect attacks. In Supabase’s dashboard, I whitelist my production URL and my local dev URL. If these aren’t configured correctly, the flow breaks, and that’s bitten me exactly once when I added a staging environment and forgot to add its URL. Five minutes to fix, and now it’s part of my deployment checklist.

Why “Just Build It Yourself” Isn’t the Flex It Sounds Like

Every few months someone on a forum somewhere says something like “auth isn’t that hard, you can just hash passwords with bcrypt and you’re fine.” And they’re not entirely wrong. For a toy project or a weekend experiment, that’s probably okay. But I’m running apps that take real money from real people (Stripe, Part 4), and the moment you’re billing someone, your security posture needs to be adult.

The thing about auth vulnerabilities is that they’re often invisible until they’re catastrophic. You won’t know your session tokens were predictable until someone enumerates them. You won’t know your password reset flow had a timing oracle until someone exploited it. The attack surface for a homegrown auth system isn’t just the code you wrote. It’s the code you didn’t know to write.

Supabase has a security team. They have a bug bounty program. They have people watching CVE disclosures and patching things before most developers would even know to be scared. I get all of that for free by not writing auth myself. The cost is a dependency. The benefit is that the people responsible for that dependency have skin in the game I’ll never have.

I also think there’s something honest about knowing your limits. I can scaffold a decent API. I can wire up a webhook handler without flinching now. I can configure Nginx from scratch if I have to. But cryptographic security isn’t my sharpest skill, and pretending otherwise in a production context is how people get burned. Delegating to Supabase isn’t admitting defeat; it’s using the right tool so I can spend my attention on the parts of the app only I can build.




At this point in the stack, you’ve got a request that can find my server (Cloudflare DNS), land somewhere reliable (Hetzner), identify the person making it (Supabase Auth), and charge them if needed (Stripe). In Part 6, I’m going to stop talking about these pieces individually and trace a single user request all the way through, from the moment they type a URL to the moment my app responds, with every layer doing its job. That’s where the whole thing finally snaps into focus.

Leave a Reply