Skip to content
BlackFig

Fixing authentication in an AI-built app

AntPublished 5 min read

Quick answer

Broken auth in AI-built apps usually comes down to five things: Supabase's default email sender, a Site URL still set to localhost, a session refresh file with the wrong name for your Next.js version, server code trusting getSession(), and pages that check login but not permissions. Each is a setting or a few lines of code.

On this page
  1. Symptom
  2. Why it happens
  3. The fix
  4. How to prevent it
  5. When to get help

Symptom

"Auth isn't working" is usually one of five different bugs, and the fix depends on which. Supabase returns a specific error code for most of them, so check the code on the error object before anything else. Supabase's own advice is to match on codes, not message text.

Sign-up confirmation emails never arrive. You see one of these:

The link in the email opens localhost:3000, or lands on an error page. If the link is old or has been opened already, the error is:

Users are signed out at random, often after a refresh or after leaving a tab open. Server logs show refresh_token_not_found or session_not_found.

Users can't sign in after signing up:

Everything works, but users can see things they shouldn't. No error at all. This is the dangerous one.

Why it happens

You tested with one account. Your email address is on your Supabase team, so the default sender delivers to you. Supabase's docs say that sender is not meant for production: it sends 2 emails an hour and refuses addresses outside the team.

The Site URL was never changed. Supabase uses the Site URL as the default destination after email confirmation and password resets, and its email templates build links from it. New projects start on http://localhost:3000. If nobody changed it, every link in every email points at a laptop.

The session plumbing is fragile. In a server-rendered Next.js app, the session lives in cookies, and a small file that runs before each request refreshes it. Next.js 16 renamed that file from middleware.ts to proxy.ts. Supabase's docs are blunt about the consequence: on Next.js 15 or earlier, a proxy.ts file is never called, so sessions never refresh and users get signed out. AI tools learned from code written for both versions, so a mismatch is easy to generate and easy to miss.

Server code trusts the cookie. supabase.auth.getSession() reads the session from the cookie without checking it. Supabase warns that anyone can forge that cookie, and says to use getClaims() to protect pages and data.

The check is in the wrong place. AI-built apps often hide pages in the browser ("if not logged in, redirect") and never check on the server or in the database. That stops honest users and nobody else.

The fix

1. Send email through your own provider

In the Supabase dashboard, open Authentication, find the SMTP settings and add a transactional email provider. Once custom SMTP is on, Supabase sends to every address, and you can raise the email limit under Authentication > Rate Limits.

Then set up your sending domain's records with that provider, or your confirmation emails will arrive in spam, which looks exactly like not arriving at all.

2. Fix the Site URL and redirect list

Under Authentication > URL Configuration, set the Site URL to your production domain. Add every other address you use to the Redirect URLs list. Supabase's docs give these patterns for local development and Vercel previews, where ** matches anything including slashes:

http://localhost:3000/**
https://*-<team-or-account-slug>.vercel.app/**

Pass the redirect explicitly when users sign up, so each environment sends people back to itself:

const { error } = await supabase.auth.signUp({
  email,
  password,
  options: { emailRedirectTo: `${window.location.origin}/auth/confirm` },
});

For server-rendered apps, Supabase's Next.js tutorial also changes the Confirm signup email template to link to your own confirm route with a token hash, instead of the default confirmation URL:

{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email

Your app/auth/confirm/route.ts then calls supabase.auth.verifyOtp({ type, token_hash }) and redirects. If that route also reads a next parameter, only accept paths that start with a single /, or you have built an open redirect.

3. Name the session file for your Next.js version

Check package.json for your Next.js version, then make sure exactly one of these exists at the project root (or inside src/ if you use one):

  • Next.js 15 or earlier: middleware.ts, exporting async function middleware.
  • Next.js 16 or later: proxy.ts, exporting async function proxy.

Here is the Next.js 15 version, based on the official Supabase example:

// middleware.ts (Next.js 15). On Next.js 16, name it proxy.ts and export `proxy`.
import { type NextRequest } from "next/server";
import { updateSession } from "@/lib/supabase/middleware";
 
export async function middleware(request: NextRequest) {
  return await updateSession(request);
}
 
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"],
};

Inside updateSession, the example creates a server client with getAll and setAll cookie handlers, calls supabase.auth.getClaims() immediately, and returns the response object the cookie handler built. Two comments in the official code explain most random logouts: don't run code between creating the client and calling getClaims(), and if you return a different response, copy the cookies across. AI edits break both rules regularly, for example by adding a redirect that builds a fresh response.

4. Check sessions on the server with getClaims

Replace getSession() in any server code with getClaims(), which validates the token:

// app/dashboard/page.tsx
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
 
export default async function DashboardPage() {
  const supabase = await createClient();
  const { data } = await supabase.auth.getClaims();
  if (!data?.claims) redirect("/auth/login");
 
  const { data: projects } = await supabase
    .from("projects")
    .select("id, name")
    .order("created_at", { ascending: false });
 
  return <ProjectList projects={projects ?? []} />;
}

createClient here is the server client from lib/supabase/server.ts, which reads cookies with getAll and writes them with setAll. Create it inside each request, never as a shared global.

5. Check permissions where the data is

A signed-in check answers "who are you?". It doesn't answer "is this yours?". That answer has to come from Row Level Security policies in the database, and from an ownership check in every Server Action and route handler. Next.js's security guide points out that Server Actions can be called by direct POST, so a check on the page does not protect them. I cover the policies and the action pattern in 10 security holes in vibe-coded apps.

6. Show useful errors

Map error codes to messages a person can act on, rather than showing "Something went wrong" or the raw text:

import { isAuthApiError } from "@supabase/supabase-js";
 
const { error } = await supabase.auth.signInWithPassword({ email, password });
 
if (error && isAuthApiError(error)) {
  switch (error.code) {
    case "email_not_confirmed":
      return "Check your inbox for the confirmation link, or ask for a new one.";
    case "invalid_credentials":
      return "That email and password don't match.";
    default:
      return "Sign-in failed. Please try again in a moment.";
  }
}

How to prevent it

  • Test as a stranger. Sign up with an address that isn't on your Supabase team, on your phone, in a private window. Then come back the next day and check you are still signed in.
  • Keep one auth module. Put the Supabase clients and session helpers in lib/supabase, and tell your AI tool in its project instructions not to edit them without asking.
  • Separate environments. Use a different Supabase project, with its own Site URL, for previews and production.
  • Write one end-to-end test. Sign up, confirm, sign in, sign out. Run it before every deploy. It is the journey that loses you users silently when it breaks.
  • Re-check after upgrades. Moving between Next.js 15 and 16 renames the session file. Check it survived.

When to get help

The steps above fix most broken email-and-password auth. Get help when:

  • Social login is involved. Google or Apple sign-in adds provider consoles, client IDs and their own redirect lists, and mismatches between them are fiddly.
  • You need teams or roles. Who can see what across organisations is a data model question as much as an auth one. Read how to fix the data model first.
  • You are migrating users from one auth system to another without forcing everyone to reset their password.
  • The app is live and people are locked out. Every hour costs sign-ups.

A Rescue Sprint usually starts here, because auth is the front door: I fix sign-up, sessions and permissions, then the rest of the checklist in the fix guide. If you are not sure how deep the problem goes, run the free health check first.

Sources

The vibe-code rescue checklist, as a PDF

About 40 checks across auth, data, payments, errors, mobile and secrets, on two pages.

You get the PDF now and two short follow-ups over the next week. Reply “stop” and the rest never arrive.

Questions founders ask

Why does sign-up work for me but not for my users?

Supabase's built-in email sender only delivers to addresses on your project's team, and only 2 emails an hour. Your address is on the team, your users' are not. Set up a custom SMTP provider in the Authentication settings.

Why do my users get logged out when they refresh the page?

In a Next.js app using Supabase, the session is refreshed by a middleware.ts file (Next.js 15 and earlier) or proxy.ts file (Next.js 16). If the file is missing, misnamed for your version, or doesn't return the response it was given, the session never refreshes.

Should I switch to a different auth provider?

Rarely. The problems in this article are configuration and wiring, and they follow you to any provider. Fix them first, then decide whether you need features another provider offers.

Ant

Product designer who builds MVPs, based in Nottingham. 10+ years designing digital products, and the only person at BlackFig. More about me