One-Time Password (OTP) Email Authentication Flow with Supabase Auth in TCG Pocket Collection Tracker

The TCG Pocket Collection Tracker implements a passwordless authentication system using Supabase Auth that emails users a 6-digit numeric OTP, verifies the token to establish a JWT session, and maintains authenticated state through React Query caching.

The TCG Pocket Collection Tracker eliminates traditional passwords by leveraging Supabase Auth’s built-in One-Time Password (OTP) email capabilities. This authentication flow allows users to register and log in securely using only their email address, receiving a time-sensitive verification code that creates a persistent JSON Web Token (JWT) session upon validation.

How the OTP Authentication Flow Works

The authentication process follows a strict two-step verification pattern: first requesting the OTP via email, then validating the code to establish a session. All authentication logic is centralized in the frontend service layer, while Supabase handles the cryptographic generation and delivery of secure tokens.

Step 1: Initiating the Sign-In Request

When a user enters their email address in the Login component and clicks the sign-in button, the application invokes the signInWithOtp function from the auth service. This function forwards the email to Supabase Auth, which automatically generates a 6-digit numeric OTP and sends it to the provided address.

// frontend/src/services/auth/useAuth.ts
export async function signInWithOtp({ email }: { email: string }) {
  const { error } = await supabase.auth.signInWithOtp({ email })
  if (error) {
    console.log('supabase sign in with OTP error', error)
    throw new Error('Error sending the OTP')
  }
}

Upon successful initiation, the UI toggles the emailSubmitted state flag, triggering a view transition from the email input form to the OTP entry interface.

Step 2: Entering the Verification Code

The Login component renders a specialized InputOTP component that accepts exactly six characters. When the user completes entering the code, the onComplete callback fires the otpEntered handler, which triggers the verification mutation.

// frontend/src/components/Login.tsx
if (emailSubmitted) {
  return (
    <InputOTP maxLength={6} onComplete={otpEntered}>
      {/* OTP input slots */}
    </InputOTP>
  )
}

This UI state management ensures users cannot access the OTP input field until the initial email request successfully reaches Supabase servers.

Step 3: Verifying the OTP and Creating the Session

The otpEntered callback executes verifyOtpMutation.mutate(), passing the email and OTP token to the useVerifyOTP mutation hook. This handler calls supabase.auth.verifyOtp with the type: 'email' parameter to validate the token against Supabase’s stored hash.

// frontend/src/services/auth/useAuth.ts
export function useVerifyOTP() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async ({ email, otp }: { email: string; otp: string }) => {
      const { error } = await supabase.auth.verifyOtp({
        email,
        token: otp,
        type: 'email',
      })
      if (error) {
        console.log('supabase OTP error', error)
        throw new Error('Error verifying the OTP')
      }
    },
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: ['user'] })
    },
  })
}

Upon successful verification, Supabase automatically creates a session containing an access token and refresh token, storing these credentials in the browser’s local storage. The onSuccess handler immediately invalidates the cached user query to force a session refetch.

Step 4: Session Persistence and State Management

After mutation success, the useUser hook executes getCurrentUser from the auth service to retrieve the active session. This function queries Supabase for the current session state, returning the JWT and user metadata required for authenticated API requests.

// frontend/src/services/auth/authService.ts
export async function getCurrentUser() {
  const { data, error } = await supabase.auth.getSession()
  if (error) {
    throw new Error(error.message)
  }
  return data.session // contains access_token, refresh_token, user...
}

Once hydrated, the React Query cache maintains the user object across component renders, and the shared Supabase client automatically injects the JWT into the Authorization header for all subsequent requests to protected tables such as accounts, collection, and trades.

Step 5: Logging Out and Clearing State

The logout process terminates the session both server-side and client-side. The signOut function in authService.ts invokes the Supabase sign-out method, which revokes the refresh token and clears local storage credentials.

// frontend/src/services/auth/authService.ts
export async function signOut() {
  const { error } = await supabase.auth.signOut()
  if (error) {
    throw new Error(error.message)
  }
}

Following the API call, the application clears the React Query cache, ensuring no stale user data persists in memory after logout.

Core Authentication Files and Their Roles

The authentication architecture separates concerns across four primary files:

  • frontend/src/components/Login.tsx – Orchestrates the UI state machine, managing transitions between email input and OTP verification views using the emailSubmitted flag.
  • frontend/src/services/auth/useAuth.ts – Contains the core authentication mutations signInWithOtp and useVerifyOTP, wrapping Supabase SDK methods with React Query integration for caching and error handling.
  • frontend/src/services/auth/authService.ts – Provides session inspection via getCurrentUser and session termination via signOut, abstracting direct Supabase client interactions.
  • frontend/src/lib/supabase.ts – Initializes the Supabase client with the SUPABASE_URL and SUPABASE_ANON_KEY required for all authentication API calls.

Summary

  • Supabase Auth handles OTP generation, email delivery, and cryptographic verification without requiring backend code in the application repository.
  • The 6-digit numeric code flow eliminates passwords entirely, reducing attack surfaces while maintaining security through time-sensitive tokens.
  • React Query manages server state, invalidating user caches immediately after verification to ensure the UI reflects the authenticated session.
  • All session tokens are stored in browser local storage by Supabase’s client library, enabling persistent logins across page refreshes until explicit logout or token expiration.
  • The modular architecture separates UI components (Login.tsx), mutation hooks (useAuth.ts), and session services (authService.ts) for maintainable authentication logic.

Frequently Asked Questions

How does the application generate and send the OTP email?

The application does not generate the OTP internally. Instead, the signInWithOtp function in frontend/src/services/auth/useAuth.ts delegates this responsibility to Supabase Auth. When invoked, Supabase generates a cryptographically secure 6-digit numeric code, composes the email content, and dispatches it to the provided address using the project's configured email provider.

What happens to the user interface after submitting an email address?

The Login component sets the emailSubmitted state flag to true immediately after the signInWithOtp promise resolves. This boolean toggle conditionally renders the InputOTP component, replacing the email input field and submit button with a six-digit code entry interface. The maxLength={6} property on the input component enforces the expected OTP format.

Where is the authentication session stored after successful OTP verification?

Upon successful verification via supabase.auth.verifyOtp, Supabase Auth automatically stores the session tokens—including the access token and refresh token—in the browser's local storage. The getCurrentUser function in frontend/src/services/auth/authService.ts retrieves this session by calling supabase.auth.getSession(), which reads from this storage mechanism to return the active JWT and user metadata.

How does the logout mechanism work in this implementation?

The logout process calls supabase.auth.signOut() within the signOut function defined in frontend/src/services/auth/authService.ts. This method revokes the current refresh token on Supabase’s servers and clears all authentication data from local storage. The application typically follows this call by clearing the React Query cache to remove any persisted user data from the client-side state.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →