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

> Learn the Supabase Auth OTP email authentication flow for user registration and login. Discover how the TCG Pocket Collection Tracker uses OTP to secure your JWT session.

- Repository: [Marcel Panse/tcg-pocket-collection-tracker](https://github.com/marcelpanse/tcg-pocket-collection-tracker)
- Tags: internals
- Published: 2026-03-06

---

**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.

```typescript
// 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.

```tsx
// 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.

```typescript
// 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.

```typescript
// 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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/authService.ts) invokes the Supabase sign-out method, which revokes the refresh token and clears local storage credentials.

```typescript
// 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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/Login.tsx)), mutation hooks ([`useAuth.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/useAuth.ts)), and session services ([`authService.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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.