-
[Supabase][Auth] invite 링크가 만료되거나 type 값이 예상과 다를 때 verifyOtp 오류를 어떤 순서로 분리하나기타개발지식/풀스택개발 2026. 7. 5. 20:14
IT 리서치 노트
[Supabase][Auth] invite 링크가 만료되거나 type 값이 예상과 다를 때 verifyOtp 오류를 어떤 순서로 분리하나
Supabase invite 수락 흐름을 붙였는데 메일 링크는 열리지만 verifyOtp가 실패할 때는 원인이 한 가지가 아니다. 2026년 7월 5일 기준 Supabase 공식 문서를 다시 보면 token_hash와 type을 함께 넘겨 검증해야 하고, 이메일 토큰은 기본적으로 1시간 뒤 만료되며, 403과 otp_expired 같은 메시지는 토큰 유효성 문제로 묶인다. 이 글은 invite 링크가 만료됐는지, type 값이 기대와 다른지, query 자체가 누락됐는지를 어떤 순서로 분리할지 정리한 것이다.
1. 개요
결론부터 말하면 invite verifyOtp 실패는 세 단계로 분리해야 한다. 먼저
token_hash와type이 수락 라우트에 실제로 들어왔는지 본다. 그다음 만료 또는 재사용 때문에 토큰이 더 이상 유효하지 않은지 본다. 마지막으로 type을 하드코딩했거나 query를 잃어버려 verifyOtp 입력 자체가 달라졌는지 본다.이미 token_hash 검증과 첫 비밀번호 설정 순서 글이 수락 성공 뒤 흐름을 다뤘다면, 이번 글은 그 직전의 실패 분기다. 또 redirectTo와 이메일 템플릿 글은 링크 도착지 문제를 다뤘고, 오늘은 도착 후 verifyOtp가 왜 실패하는지를 좁힌다.
2. 어디서 실제로 막히는가
실무에서 가장 자주 보이는 막힘은 세 가지다. 첫째, 초대 메일을 오래 뒤에 눌러 verifyOtp가 403이나
otp_expired로 실패한다. 둘째, 수락 라우트에서type을 무시하고 하드코딩해 invite가 아닌 다른 값으로 검증한다. 셋째, 메일 템플릿이나 중간 redirect에서token_hash또는type이 사라져 verifyOtp 입력이 불완전해진다.Supabase verifyOtp reference는
token_hash와type을 함께 넘기는 예시를 제공한다. Passwordless 문서는 이메일 토큰의 기본 만료 시간이 1시간이라고 적고, troubleshooting 문서는 403과otp_expired같은 메시지를 토큰 유효성 문제로 분류한다. Next.js tutorial은 수락 라우트에서 query의type을 읽어 verifyOtp에 그대로 전달하는 패턴을 보여 준다.이 네 문서를 같이 읽으면 invite 오류는 단순히 '링크가 안 된다'가 아니다. 링크는 열리지만 토큰이 만료됐을 수 있고, 토큰은 살아 있지만 수락 라우트가 type을 다르게 처리했을 수 있고, 둘 다 맞는데 query를 잃어버렸을 수도 있다. 이 셋을 분리하지 않으면 같은 오류 화면이 반복된다.
- 증상: 링크는 열리는데 verifyOtp가 실패한다.
- 실패: token_hash와 type을 같이 기록하지 않는다.
- 막힘: 만료와 입력값 누락을 같은 버그로 취급한다.
- 누락: 수락 라우트에서 raw type을 보존하지 않는다.
상황 먼저 볼 것 판단 기준 403 또는 otp_expired 링크 생성 시각과 재사용 여부 만료 또는 이미 사용된 링크로 분류한다 type 관련 분기 의심 query의 raw type 하드코딩 대신 입력값을 먼저 기록한다 링크는 정상인데 값이 비어 있다 메일 템플릿과 중간 redirect token_hash 또는 type이 유실됐는지 본다 3. 실무에서 적용하는 순서
점검 순서는 네 단계가 가장 짧다. 먼저 수락 라우트 입구에서
token_hash와type을 그대로 기록한다. 두 번째로 링크 생성 시각과 사용 시각을 대조해 만료 분기를 먼저 자른다. 세 번째로 verifyOtp 실패가 나면 raw type을 유지한 채 에러 코드와 메시지를 별도 로그에 남긴다. 마지막으로 만료가 아니고 입력도 있는데 실패한다면 메일 템플릿과 중간 redirect가 query를 바꾸지 않았는지 본다.- 수락 라우트에서
token_hash와type을 먼저 기록한다. - 토큰 만료와 재사용 분기를 먼저 자른다.
- verifyOtp 입력에 raw type을 그대로 사용한다.
- query 유실이 없는지 템플릿과 redirect를 다시 본다.
핵심은 invite verifyOtp 실패를 '초대 메일 문제' 하나로 묶지 않는 것이다. 이메일 토큰은 기본 유효 시간이 있고, verifyOtp는 입력 type에 민감하며, 수락 라우트는 query를 보존해야 한다. 이 해석은 Supabase verifyOtp, passwordless, troubleshooting, tutorial 문서를 이어 읽은 운영 기준이다.
이 정도만 남겨도 새 invite를 보내야 할지, 수락 라우트를 고쳐야 할지, 이메일 템플릿을 다시 봐야 할지가 빠르게 갈린다.
4. 공식 문서와 예시 화면으로 확인하기
첫 자료는 이메일 템플릿 문서의 TokenHash 변수 설명이다. Supabase는 메일 링크를 직접 구성할 때 TokenHash 변수를 사용할 수 있다고 적고 있다.
즉 invite 수락 오류를 볼 때는 메일 링크가 실제로 token_hash를 운반하고 있었는지부터 확인해야 한다. 링크가 열렸다는 사실만으로 verifyOtp 입력이 완전하다고 보면 안 된다.
두 번째 자료는 Supabase 이메일 OTP 만료 시간 설명이다. 공식 문서는 기본적으로 이메일 OTP와 magic link가 1시간 후 만료된다고 적고 있다.
invite 링크도 같은 검증 계층을 지나기 때문에, 오래된 링크를 다시 눌렀을 때는 redirect 문제보다 만료 문제를 먼저 의심해야 한다. 수락 라우트가 잘 열리더라도 verifyOtp는 403이나 만료 계열 오류를 반환할 수 있다.
세 번째 자료는 Supabase troubleshooting 문서다. 여기서는 403 응답과 otp_expired, token has expired or is invalid 같은 메시지를 같은 범주의 토큰 유효성 문제로 묶고 있다.
이 문서 덕분에 만료, 이미 사용됨, 잘못된 토큰을 하나의 분기에서 먼저 좁힐 수 있다. 이 단계는 redirectTo나 첫 비밀번호 저장 단계보다 앞선다.
네 번째 자료는 공식 Next.js tutorial의 confirm route 예시다. 여기서는 query에서 type을 읽고 verifyOtp에 그대로 넘기는 패턴을 보여 준다.
문서가 이렇게 쓰는 이유는 type을 하드코딩하기보다 메일 링크가 전달한 값을 먼저 보존하라는 뜻에 가깝다. 실제 invite 환경에서는 예상한 type이 안 오거나 빠질 수도 있어, 여기서 곧바로 분기 기록을 남기는 편이 안전하다.
실무에서는 수락 라우트에서 type, token_hash, 에러 메시지를 함께 기록하는 방어 코드를 두는 편이 좋다. 그래야 만료 문제와 잘못된 type 문제를 다른 기록 항목으로 분리할 수 있다.
이 구조가 있으면 링크는 열리지만 verifyOtp가 실패하는 상황에서도 어떤 입력이 빠졌는지 바로 좁힐 수 있다. 이미 token_hash 검증과 첫 비밀번호 설정 순서 글을 읽었다면, 이번 글은 그 앞단의 실패 분기표다.
마지막 자료는 verifyOtp 실패 분기표다. 같은 invite 오류 화면이라도 쿼리 누락, 토큰 만료, type 불일치를 같은 이유로 취급하면 다시 재현하기 어렵다.
이 표를 기준으로 보면 초대 링크가 잘못된 것인지, 사용자가 너무 늦게 눌렀는지, 아니면 서버 수락 라우트가 type을 잘못 다루는지 설명이 짧아진다. 앞선 redirectTo와 이메일 템플릿 글과 inviteUserByEmail와 createUser 비교 글을 함께 보면 초대 메일 생성과 수락 오류를 한 사다리로 묶기 쉽다.
5. 주의사항과 리스크
첫 번째 리스크는
type을 임의로 고정하는 것이다. 문서 예시는 query의 type을 읽어 verifyOtp에 전달하는데, 이를 무시하면 초대와 다른 이메일 인증 흐름이 섞였을 때 오판하기 쉽다. 두 번째 리스크는 403을 redirect 문제로만 생각하는 것이다. 세 번째 리스크는 수락 화면이 열렸다는 이유로 token_hash와 type이 끝까지 보존됐다고 가정하는 것이다.운영 전에는 같은 링크를 즉시 클릭한 경우와 1시간 뒤 클릭한 경우를 각각 테스트해 두는 편이 좋다. 그래야 만료 UX와 정상 수락 UX를 혼동하지 않는다.
- verifyOtp 실패는 만료, 누락, type 불일치로 나눠 기록한다.
- 수락 라우트는 raw type을 먼저 보존한다.
- 링크 도착 성공과 verifyOtp 성공을 같은 성공 조건으로 보지 않는다.
6. 결론
Supabase invite verifyOtp 오류를 줄이려면 링크가 열렸는지보다 token_hash와 type이 어떻게 들어왔는지, 그리고 토큰이 아직 유효한지를 먼저 나눠 봐야 한다. 만료 분기와 type 분기, query 유실 분기를 따로 기록해 두면 초대 수락 실패를 훨씬 짧게 설명할 수 있다.
만약 만료 원인이 확인된 뒤 운영 복구 순서까지 바로 이어 보고 싶다면, 이어서 invite 링크가 만료된 뒤 재발송할 때 redirectTo와 새 token_hash를 다시 붙이는 글을 같이 보는 편이 좋다. 이 글이 실패 분기를 자르는 문서라면, 후속 글은 새 링크를 다시 만드는 복구 절차를 다룬다.
- 먼저 token_hash와 type을 그대로 기록한다.
- 만료 또는 재사용 분기를 가장 먼저 자른다.
- query 유실이 없을 때만 서버 라우트 로직을 의심한다.
7. 참고 링크
- https://supabase.com/docs/reference/javascript/auth-verifyotp
- https://supabase.com/docs/guides/auth/auth-email-passwordless
- https://supabase.com/docs/guides/troubleshooting/otp-verification-failures-token-has-expired-or-otp_expired-errors-5ee4d0
- https://supabase.com/docs/guides/auth/auth-email-templates
- https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs
'기타개발지식 > 풀스택개발' 카테고리의 다른 글