CoreMVP

Authentication

Supabase Auth integration with server-side session management.

Overview

Supabase Auth manages identity and sessions. Native Start request middleware preserves refreshed cookies, and the dashboard verifies the user before rendering protected pages.

Supported Methods

  • Email/Password with verification
  • Magic links (OTP)
  • OAuth (Google and GitHub)

Architecture

service.ts
types.ts
app.ts
callback.ts
reset-password.tsx
start.ts

How It Works

The user submits a form to the existing same-origin /api/auth/* URL. The Start API adapter passes the request to Hono, the auth handler calls the auth service, and the service uses the cookie-backed Supabase server client. The request middleware asks Supabase to verify the current token with getClaims() while preserving any refreshed session cookies. Password forms keep provider and validation failures on the current form. After a successful sign-in, sign-up, password setup, or sign-out, the shared auth transition helper clears cached session and user-status results before the next route renders.

OAuth, magic-link, and email-confirmation redirects return to /auth/callback. That request-bound Route Handler exchanges Supabase's single-use PKCE code and writes the cookie session before sending the user to /dashboard. Callback failures return to sign-in with generic application copy; provider error descriptions and authorization codes are never displayed.

Auth Service

The server-only auth service (src/services/auth/service.ts) owns Supabase Auth calls and their redirect results. Password routes preserve the supplied password; only email addresses are trimmed at the Hono boundary.

Password resultDestination
Sign-up creates a session immediately/pricing with success status
Sign-up requires email confirmation/ with confirmation status
Password sign-in succeeds/dashboard with success status
Provider or validation failsCurrent auth form with error detail

New and changed passwords require at least eight characters. Keep password sign-in opaque: do not trim it or apply the creation minimum to existing users.

The API reference lists the exact response shapes. Keep the service's validation, existing-account, and provider-error branches when customizing it.

API Routes

The Hono auth module trims the email, preserves the password, calls the auth service, and returns its JSON result with a success or failure status.

The sign-up handler uses the shared request validator before calling its service:

src/api/routes/auth.ts
import { Hono } from "hono";
import { validateJson } from "@/api/validation";
import { signUpSchema } from "@/services/auth/schemas";
import { signUp } from "@/services/auth";

export const authRoutes = new Hono();

authRoutes.post("/sign-up", validateJson(signUpSchema), async (c) => {
  const result = await signUp(c.req.valid("json"));
  return c.json(result, result.ok ? 200 : 400);
});

Malformed JSON and invalid fields return the shared 400 invalid_request response before the service runs. Keep that validation boundary when extending an authentication endpoint.

Client-side password transitions

Use the shipped mutation hooks rather than calling Supabase from browser components. After a successful password transition, clear session-dependent query results before navigating:

const queryClient = useQueryClient();
const signUpMutation = useSignUpMutation();
const result = await signUpMutation.mutateAsync({ email, password });

if (result.ok) {
  reconcileAuthTransition(queryClient);
  router.clearCache();
  await router.navigate({ href: result.redirectPath });
  router.clearCache();
}

Session Management

Request session refresh

src/start.ts calls the session helper in src/lib/supabase/middleware.ts for matched page requests. The Supabase factory uses native request/response cookies, preserving every refreshed chunk and cache header through server rendering, raw Markdown and redirects. Hono routes use the same factory.

Getting the Current User

Inside server functions, services or API handlers:

import { createSupabaseServerClient } from "@/lib/supabase/server";

async function readCurrentUser() {
  const supabase = await createSupabaseServerClient();
  const { data: { user } } = await supabase.auth.getUser();
  return user;
}

In client components, use the session hook:

import { useSession } from "@/hooks/auth";

const session = useSession();
const user = session?.user ?? null;

Protected Routes

src/routes/dashboard/route.tsx loads the server bootstrap in beforeLoad, before its shell and nested pages render. The admission check in src/lib/dashboard.server.ts validates the current user:

getDashboardBootstrap() calls supabase.auth.getUser() inside the active server request and throws redirect({ to: '/signin' }) when the provider returns an error or no user. It then loads schema readiness, locale, and the safe user projection. Keep request-bound cookie reads inside that function or an active server-function handler; module initialization has no request.

Keep client status queries for account and billing data, not for route admission. An unauthenticated request redirects before protected HTML is sent.

OAuth Setup

Enable Provider in Supabase

Go to Supabase Dashboard → Authentication → Providers

Configure OAuth App

Create an OAuth client in the Google developer console and get credentials. Google is the default OAuth option in the sign-in and sign-up forms. To expose GitHub sign-in, configure that provider and add a GitHub control to both forms using the existing OAuth handler.

Add Credentials to Supabase

Enter Client ID and Secret in Supabase provider settings

Set Redirect URLs

Add https://<project-ref>.supabase.co/auth/v1/callback to the OAuth provider's allowed redirects. This is the provider callback, not your app's hosted URL.

Configure the Hosted App URL

After your hosted domain is assigned, open Supabase URL Configuration. Set Site URL to https://<your-domain>, add https://<your-domain>/auth/callback and https://<your-domain>/auth/reset-password to Redirect URLs, and save before testing OAuth, confirmation links, magic links, or recovery.

Set the Hosted Password Minimum

In your Supabase project's Auth settings, set the minimum password length to 8. The template and local Supabase configuration already enforce the same boundary, but the hosted setting is what prevents a direct Auth API call from bypassing it.

Password recovery

Recovery and guest password-setup links return to /auth/reset-password. The page validates the Supabase recovery session, requires an eight-character password, and then continues to Dashboard. The callback route also redirects older, still-valid fragment recovery links to this page during the route transition.

Test Google in the deployed Production environment. If you enable GitHub, test it separately because each provider has independent credentials and callback configuration. Local tests prove application routing and recovery compatibility; they do not prove a hosted provider configuration.

Database Trigger

New users are automatically added to the users table via a database trigger:

supabase/migrations/handle_new_user.sql
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger AS $$
BEGIN
  INSERT INTO public.users (id, email)
  VALUES (new.id, new.email);
  RETURN new;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();

Was this page helpful?

On this page