Conversation
📝 WalkthroughWalkthroughThis PR modifies the email OTP verification flow to use an admin-scoped Supabase client for recording verification instead of a token-scoped client. The new approach explicitly passes the user_id to a redesigned PostgreSQL function, which now validates the input and enforces access via service_role only. Changes
Sequence DiagramsequenceDiagram
actor Client
participant VerifyFunc as Verify Function<br/>(TypeScript)
participant AdminClient as Supabase Admin<br/>Client
participant RPC as RPC Function<br/>(PostgreSQL)
participant DB as user_security<br/>Table
Client->>VerifyFunc: OTP Verification Request
VerifyFunc->>VerifyFunc: Validate OTP
Note over VerifyFunc: Create otpVerifiedAt<br/>timestamp locally
VerifyFunc->>AdminClient: Call with user_id
AdminClient->>RPC: record_email_otp_verified(user_id)
RPC->>RPC: Validate user_id NOT NULL
RPC->>DB: Upsert email_otp_verified_at
DB-->>RPC: Return current timestamp
RPC-->>AdminClient: Return timestamptz
AdminClient-->>VerifyFunc: Return result
VerifyFunc-->>Client: Return verified_at in response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 SQLFluff (4.0.4)supabase/migrations/20260224100000_revoke_record_email_otp_verified_auth_role.sqlUser Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 344bfffb69
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| REVOKE EXECUTE ON FUNCTION "public"."record_email_otp_verified"(uuid) FROM "authenticated"; | ||
|
|
||
| -- Remove the legacy zero-arg function overload now that callers are migrated. | ||
| DROP FUNCTION IF EXISTS "public"."record_email_otp_verified"(); |
There was a problem hiding this comment.
Preserve legacy RPC during staged deployment
Dropping record_email_otp_verified() immediately creates a rollout compatibility gap: in .github/workflows/build_and_deploy.yml, supabase db push runs before supabase functions deploy, so the currently deployed edge function (still calling the zero-arg RPC until the next step) will fail OTP verification during that window with missing/unauthorized function errors. Keep the legacy signature (e.g., wrapper overload) for at least one deploy cycle, then remove it after all function runtimes have switched to the new user_id form.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
supabase/functions/_backend/private/verify_email_otp.ts (1)
81-88:⚠️ Potential issue | 🔴 CriticalGuard
verifyData.userbefore dereferencingid.Line 87 dereferences
verifyData.user.idwithout a hard null check. This matches the TS18047 failure and can throw at runtime whenuseris null.🔧 Proposed fix
- if (verifyData.user?.id && verifyData.user.id !== auth.userId) { + const verifiedUserId = verifyData.user?.id + if (!verifiedUserId) { + return quickError(401, 'invalid_otp', 'Invalid or expired OTP') + } + if (verifiedUserId !== auth.userId) { return quickError(403, 'otp_user_mismatch', 'OTP does not match current user') } const otpVerifiedAt = new Date().toISOString() const { error: recordError } = await supabaseAdmin(c).rpc('record_email_otp_verified', { - user_id: verifyData.user.id, + user_id: verifiedUserId, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/_backend/private/verify_email_otp.ts` around lines 81 - 88, The code dereferences verifyData.user.id without ensuring verifyData.user exists, which can cause runtime/TS18047 errors; update the flow in verify_email_otp.ts to explicitly guard verifyData.user before accessing .id (e.g., if (!verifyData.user || !verifyData.user.id) return quickError(400, 'otp_no_user', 'No user found for OTP')), and only call supabaseAdmin(c).rpc('record_email_otp_verified', { user_id: verifyData.user.id }) after that guard so record_email_otp_verified and the otpVerifiedAt assignment never run with a null user.
🧹 Nitpick comments (1)
supabase/functions/_backend/private/verify_email_otp.ts (1)
85-95: Prefer returning the DB-persisted verification timestamp.Line 85 currently uses worker time, while the migration writes DB time. Returning the RPC value avoids drift between API response and stored
email_otp_verified_at.♻️ Suggested adjustment
- const otpVerifiedAt = new Date().toISOString() - const { error: recordError } = await supabaseAdmin(c).rpc('record_email_otp_verified', { + const { data: otpVerifiedAt, error: recordError } = await supabaseAdmin(c).rpc('record_email_otp_verified', { user_id: verifyData.user.id, }) - if (recordError) { + if (recordError || !otpVerifiedAt) { cloudlog({ requestId: c.get('requestId'), context: 'verify_email_otp - record failed', error: recordError?.message }) return quickError(500, 'record_failed', 'Failed to record OTP verification') } return c.json({ verified_at: otpVerifiedAt })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/_backend/private/verify_email_otp.ts` around lines 85 - 95, The code returns a local otpVerifiedAt (new Date().toISOString()) but the RPC record_email_otp_verified writes the canonical DB timestamp; change the flow to capture the RPC result payload from supabaseAdmin(c).rpc('record_email_otp_verified', { user_id: verifyData.user.id }) (e.g., inspect the returned data field) and return the persisted timestamp from that payload (email_otp_verified_at) in the JSON response instead of otpVerifiedAt; keep the existing error handling for recordError unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@supabase/functions/_backend/private/verify_email_otp.ts`:
- Around line 86-88: The RPC type for record_email_otp_verified is out of sync
causing a TS error at the call in verify_email_otp.ts
(supabaseAdmin(c).rpc('record_email_otp_verified', { user_id: verifyData.user.id
})). Regenerate your Supabase client types so the function signature in
supabase/functions/_backend/utils/supabase.types.ts (record_email_otp_verified:
{ Args: ... }) includes the user_id: uuid argument; then re-run type generation
(e.g., via the Supabase CLI or your project’s gen script) and commit the updated
types so the RPC call typechecks.
---
Outside diff comments:
In `@supabase/functions/_backend/private/verify_email_otp.ts`:
- Around line 81-88: The code dereferences verifyData.user.id without ensuring
verifyData.user exists, which can cause runtime/TS18047 errors; update the flow
in verify_email_otp.ts to explicitly guard verifyData.user before accessing .id
(e.g., if (!verifyData.user || !verifyData.user.id) return quickError(400,
'otp_no_user', 'No user found for OTP')), and only call
supabaseAdmin(c).rpc('record_email_otp_verified', { user_id: verifyData.user.id
}) after that guard so record_email_otp_verified and the otpVerifiedAt
assignment never run with a null user.
---
Nitpick comments:
In `@supabase/functions/_backend/private/verify_email_otp.ts`:
- Around line 85-95: The code returns a local otpVerifiedAt (new
Date().toISOString()) but the RPC record_email_otp_verified writes the canonical
DB timestamp; change the flow to capture the RPC result payload from
supabaseAdmin(c).rpc('record_email_otp_verified', { user_id: verifyData.user.id
}) (e.g., inspect the returned data field) and return the persisted timestamp
from that payload (email_otp_verified_at) in the JSON response instead of
otpVerifiedAt; keep the existing error handling for recordError unchanged.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
supabase/functions/_backend/private/verify_email_otp.tssupabase/migrations/20260224100000_revoke_record_email_otp_verified_auth_role.sql
| const { error: recordError } = await supabaseAdmin(c).rpc('record_email_otp_verified', { | ||
| user_id: verifyData.user.id, | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TYPE_FILE="$(fd 'supabase.types.ts' | head -n1)"
echo "Using type file: ${TYPE_FILE}"
echo "== Migration signature =="
rg -n -C2 "record_email_otp_verified" supabase/migrations/20260224100000_revoke_record_email_otp_verified_auth_role.sql
echo "== Generated TS signature =="
rg -n -C4 "record_email_otp_verified" "${TYPE_FILE}"
echo "== Current callsite =="
rg -n -C3 "rpc\\('record_email_otp_verified'" supabase/functions/_backend/private/verify_email_otp.tsRepository: Cap-go/capgo
Length of output: 1046
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for supabase type files
echo "=== Finding Supabase type files ==="
find . -type f -name "*supabase*.ts" -o -name "database.types.ts" | grep -v node_modules | head -20
echo ""
echo "=== Searching for record_email_otp_verified in all TypeScript files ==="
rg -t ts "record_email_otp_verified" --max-count=5
echo ""
echo "=== Current callsite code ==="
cat -n supabase/functions/_backend/private/verify_email_otp.ts | sed -n '80,95p'Repository: Cap-go/capgo
Length of output: 1281
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Full supabase.types.ts file ==="
wc -l supabase/functions/_backend/utils/supabase.types.ts
echo ""
echo "=== Search for record_email_otp_verified in types file ==="
rg -n -C10 "record_email_otp_verified" supabase/functions/_backend/utils/supabase.types.ts
echo ""
echo "=== Check if file mentions Database interface ==="
head -100 supabase/functions/_backend/utils/supabase.types.ts | tail -50Repository: Cap-go/capgo
Length of output: 2372
Regenerate Supabase RPC types for record_email_otp_verified(uuid).
The generated type definition at supabase/functions/_backend/utils/supabase.types.ts:3882 shows record_email_otp_verified: { Args: never; Returns: string }, which means no arguments are accepted. However, the migration defines CREATE OR REPLACE FUNCTION "public"."record_email_otp_verified"("user_id" uuid), and the callsite passes { user_id: verifyData.user.id }. This causes a TypeScript error because the generated types are out of sync with the actual RPC signature. Regenerate the Supabase types to reflect the RPC parameter.
🧰 Tools
🪛 GitHub Actions: Run tests
[error] 86-86: TS2345: Argument of type '{ user_id: string; }' is not assignable to parameter of type 'undefined'.
[error] 87-87: TS18047: 'verifyData.user' is possibly 'null'.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/functions/_backend/private/verify_email_otp.ts` around lines 86 -
88, The RPC type for record_email_otp_verified is out of sync causing a TS error
at the call in verify_email_otp.ts
(supabaseAdmin(c).rpc('record_email_otp_verified', { user_id: verifyData.user.id
})). Regenerate your Supabase client types so the function signature in
supabase/functions/_backend/utils/supabase.types.ts (record_email_otp_verified:
{ Args: ... }) includes the user_id: uuid argument; then re-run type generation
(e.g., via the Supabase CLI or your project’s gen script) and commit the updated
types so the RPC call typechecks.
|
* fix(security): revoke anon access to exist_app_v2 rpc * fix(build): restore TUS HEAD upload routing (#1664) * fix(build): handle tus HEAD upload route * fix(build): chain HEAD middleware correctly * chore(release): 12.116.2 * fix(security): protect replication endpoint (#1686) * fix(security): protect replication endpoint * fix(api): require replication endpoint internal api secret * fix(api): allow admin JWT access to replication endpoint * fix(frontend): use admin session only when no replication secret * fix(security): revoke PUBLIC execute on exist_app_v2 rpc * fix(security): revoke anon execute on exist_app_v2 rpc * fix(database): enforce org-scoped webhook rls (#1676) * fix: use exact app_id match for preview lookup (#1674) * fix(backend): use exact app_id match for preview lookup * fix(backend): preserve case-insensitive preview app lookup * chore(release): 12.116.3 * fix(security): revoke anon access to exist_app_v2 rpc * fix(security): revoke PUBLIC execute on exist_app_v2 rpc * fix(security): revoke anon execute on exist_app_v2 rpc * fix(frontend): require confirmation before URL login session (#1688) * fix(frontend): require confirmation for URL session login * fix(build): restore TUS HEAD upload routing (#1664) * fix(build): handle tus HEAD upload route * fix(build): chain HEAD middleware correctly * chore(release): 12.116.2 * fix(security): protect replication endpoint (#1686) * fix(security): protect replication endpoint * fix(api): require replication endpoint internal api secret * fix(api): allow admin JWT access to replication endpoint * fix(frontend): use admin session only when no replication secret * fix(frontend): retain tokens until query login succeeds * fix(database): enforce org-scoped webhook rls (#1676) * fix: use exact app_id match for preview lookup (#1674) * fix(backend): use exact app_id match for preview lookup * fix(backend): preserve case-insensitive preview app lookup * chore(release): 12.116.3 * fix(frontend): require confirmation for URL session login * fix(frontend): retain tokens until query login succeeds --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore(release): 12.116.4 * Riderx/fix email otp rpc reopen (#1693) * fix(security): restrict email otp verification rpc path * fix(security): also revoke otp rpc execute from public * fix(security): record email otp verification via service-side rpc * fix(security): harden email otp verification RPC usage * fix(db): drop legacy record_email_otp_verified overload * fix(frontend): delete replaced profile images from storage (#1683) * fix(frontend): delete replaced profile images from storage * fix(backend): clean stale unlinked user avatars * fix(build): restore TUS HEAD upload routing (#1664) * fix(build): handle tus HEAD upload route * fix(build): chain HEAD middleware correctly * chore(release): 12.116.2 * fix(security): protect replication endpoint (#1686) * fix(security): protect replication endpoint * fix(api): require replication endpoint internal api secret * fix(api): allow admin JWT access to replication endpoint * fix(frontend): use admin session only when no replication secret * fix: address sonar regex exec suggestions * fix(database): enforce org-scoped webhook rls (#1676) * fix: use exact app_id match for preview lookup (#1674) * fix(backend): use exact app_id match for preview lookup * fix(backend): preserve case-insensitive preview app lookup * chore(release): 12.116.3 * fix(frontend): delete replaced profile images from storage * fix(backend): clean stale unlinked user avatars * fix: address sonar regex exec suggestions --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix: restrict find_apikey_by_value RPC to service role (#1672) * fix(security): restrict find_apikey_by_value to service role * fix(build): restore TUS HEAD upload routing (#1664) * fix(build): handle tus HEAD upload route * fix(build): chain HEAD middleware correctly * chore(release): 12.116.2 * fix(security): protect replication endpoint (#1686) * fix(security): protect replication endpoint * fix(api): require replication endpoint internal api secret * fix(api): allow admin JWT access to replication endpoint * fix(frontend): use admin session only when no replication secret * fix(database): enforce org-scoped webhook rls (#1676) * fix: use exact app_id match for preview lookup (#1674) * fix(backend): use exact app_id match for preview lookup * fix(backend): preserve case-insensitive preview app lookup * chore(release): 12.116.3 * fix(security): restrict find_apikey_by_value to service role --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix: secure get_total_metrics rpc (#1671) * fix(db): harden get_total_metrics rpc auth * fix(db): qualify org_id and harden rpc role checks * fix(db): align get_total_metrics auth overloads * fix(db): harden get_total_metrics rpc auth * fix(db): qualify org_id and harden rpc role checks * fix(db): align get_total_metrics auth overloads * fix(db): harden get_total_metrics rpc auth * fix(db): qualify org_id and harden rpc role checks * fix(db): align get_total_metrics auth overloads * fix(backend): validate stripe redirect URLs (#1681) * fix(backend): validate stripe redirect URLs * fix(build): restore TUS HEAD upload routing (#1664) * fix(build): handle tus HEAD upload route * fix(build): chain HEAD middleware correctly * chore(release): 12.116.2 * fix(security): protect replication endpoint (#1686) * fix(security): protect replication endpoint * fix(api): require replication endpoint internal api secret * fix(api): allow admin JWT access to replication endpoint * fix(frontend): use admin session only when no replication secret * test(backend): add stripe redirect validation tests * test(backend): fix stripe redirect unit test env setup * fix(database): enforce org-scoped webhook rls (#1676) * fix: use exact app_id match for preview lookup (#1674) * fix(backend): use exact app_id match for preview lookup * fix(backend): preserve case-insensitive preview app lookup * chore(release): 12.116.3 * fix(backend): validate stripe redirect URLs * test(backend): add stripe redirect validation tests * test(backend): fix stripe redirect unit test env setup --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * feat(api): auto cleanup EXIF image metadata (#1673) * feat(api): auto cleanup image metadata on updates * fix: preserve content type when stripping image metadata * fix(security): restrict get_orgs_v6(userid uuid) access (#1677) * fix(security): restrict get_orgs_v6(uuid) execution to private roles * fix(build): restore TUS HEAD upload routing (#1664) * fix(build): handle tus HEAD upload route * fix(build): chain HEAD middleware correctly * chore(release): 12.116.2 * fix(security): protect replication endpoint (#1686) * fix(security): protect replication endpoint * fix(api): require replication endpoint internal api secret * fix(api): allow admin JWT access to replication endpoint * fix(frontend): use admin session only when no replication secret * fix(database): enforce org-scoped webhook rls (#1676) * fix: use exact app_id match for preview lookup (#1674) * fix(backend): use exact app_id match for preview lookup * fix(backend): preserve case-insensitive preview app lookup * chore(release): 12.116.3 * fix(security): restrict get_orgs_v6(uuid) execution to private roles --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(security): revoke anon access to apikey oracle RPCs (#1670) * fix(security): restrict apikey oracle rpc execution * fix(build): restore TUS HEAD upload routing (#1664) * fix(build): handle tus HEAD upload route * fix(build): chain HEAD middleware correctly * chore(release): 12.116.2 * fix(security): protect replication endpoint (#1686) * fix(security): protect replication endpoint * fix(api): require replication endpoint internal api secret * fix(api): allow admin JWT access to replication endpoint * fix(frontend): use admin session only when no replication secret * fix: remove anon-backed get_user_id calls in private apikey flows * fix(database): enforce org-scoped webhook rls (#1676) * fix: use exact app_id match for preview lookup (#1674) * fix(backend): use exact app_id match for preview lookup * fix(backend): preserve case-insensitive preview app lookup * chore(release): 12.116.3 * fix(security): restrict apikey oracle rpc execution * fix: remove anon-backed get_user_id calls in private apikey flows --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(security): require capgkey auth in exist_app_v2 * fix(api): block scoped apikey key creation (#1685) * fix(api): block scoped apikeys from creating keys * fix(build): restore TUS HEAD upload routing (#1664) * fix(build): handle tus HEAD upload route * fix(build): chain HEAD middleware correctly * chore(release): 12.116.2 * fix(security): protect replication endpoint (#1686) * fix(security): protect replication endpoint * fix(api): require replication endpoint internal api secret * fix(api): allow admin JWT access to replication endpoint * fix(frontend): use admin session only when no replication secret * fix(database): enforce org-scoped webhook rls (#1676) * test: fix apikey test lint violations * fix: use exact app_id match for preview lookup (#1674) * fix(backend): use exact app_id match for preview lookup * fix(backend): preserve case-insensitive preview app lookup * chore(release): 12.116.3 * fix(api): block scoped apikeys from creating keys * test: fix apikey test lint violations --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix: restrict webhook secret access to admin-only (#1692) * fix(security): restrict webhook secret read access * fix(rls): restrict webhook reads to admins * fix(security): keep only apikey-based exist_app_v2 check * fix(security): require capgkey auth in exist_app_v2 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>



Summary
Test plan
Screenshots
Checklist
bun run lint:backend && bun run lint.accordingly.
my tests
Summary by CodeRabbit
Bug Fixes
Chores