feat: 0.5.0 growth analytics — PostHog events, Resend sync, waitlist - #205
Conversation
|
Warning Review limit reached
Next review available in: 46 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe PR adds authenticated Resend contact synchronization and welcome emails, Turnstile verification, growth analytics, email-based account deletion confirmation, workflow hardening, and removal of premature SaaS database scaffolding. ChangesContact synchronization and transactional email
Analytics and feature flags
Turnstile and request security
Account deletion confirmation
Platform and scope maintenance
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant AuthProvider
participant syncContacts
participant Supabase
participant Resend
Browser->>AuthProvider: Complete sign-in
AuthProvider->>syncContacts: Invoke with displayName and language
syncContacts->>Supabase: Verify JWT and read profile plan
syncContacts->>Resend: Sync contact and send welcome email
Resend-->>syncContacts: Return delivery results
syncContacts-->>AuthProvider: Return sync status
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Preview for Bayan Flow Staging ready!
Preview alias |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (1)
supabase/functions/sync-contacts/index.ts (1)
20-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove unused function.
The
getServiceClientfunction is defined but never called in this file. Since this Edge Function only interacts directly with the Resend API, the Supabase client instantiation is unnecessary and should be removed.♻️ Proposed refactor
-function getServiceClient() { - return createClient( - Deno.env.get('SUPABASE_URL') ?? '', - Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' - ); -}You should also remove the
createClientimport on line 7 if it becomes unused.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/functions/sync-contacts/index.ts` around lines 20 - 25, Remove the unused getServiceClient function and delete the createClient import if no other code references it, leaving the Resend API integration unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/contexts/AuthProvider.jsx`:
- Around line 21-45: Move syncContactToResend and its getSupabaseClient
dependency out of AuthProvider into the appropriate authentication or contact
service module, export the service function, and update AuthProvider to call
that exported function instead. Keep the existing payload, fire-and-forget
behavior, and Supabase availability guard unchanged.
- Around line 219-223: Update the sign-in flow around syncContactToResend and
identifyUser to compute the user’s display name once in a shared local variable,
preserving the full_name-then-name fallback, and reuse it for both calls. Extend
AuthProvider.test.jsx to mock syncContactToResend and verify it is invoked
during sign-in with the expected user and displayName.
In `@src/services/entitlementService.js`:
- Around line 34-37: Replace or supplement the static
ANONYMOUS_VISUALIZATION_LIMIT export in src/services/entitlementService.js lines
34-37 with the dynamic getAnonymousVisualizationLimit export. In
src/pages/VisualizerApp.jsx lines 419-425, call getAnonymousVisualizationLimit()
when supplying the limit to trackUpgradeLimitHit and openGatedFeature, ensuring
analytics and UI use the feature-flagged value.
In `@supabase/functions/lemonqueezy-webhook/index.ts`:
- Around line 109-121: Update the webhook write paths around the subscription
upsert and profile plan update to throw when Supabase returns an error instead
of only logging and continuing. Apply the same failure propagation to the
payment branch’s writes, ensuring any write failure exits the handler with a
non-200 response so Lemon Squeezy retries the webhook; preserve successful
processing behavior.
- Around line 78-80: Update the webhook event handling to read userId from
event.meta.custom_data for subscription_created, subscription_updated, and
payment events, while preserving the existing fallback behavior where
applicable. Ensure the Supabase write paths receive the extracted user ID
instead of null.
In `@supabase/migrations/20260718120000_user_sessions_analytics.sql`:
- Around line 38-83: Secure the public.upsert_user_session function by enforcing
that p_user_id matches auth.uid() before performing the insert or update,
rejecting mismatches; alternatively remove security definer and ensure the
authenticated role has the required user_sessions INSERT and UPDATE grants so
RLS applies.
In `@supabase/migrations/20260718130000_billing_subscriptions.sql`:
- Around line 58-72: Update public.is_pro_user so it only returns the
authenticated caller’s own Pro status by requiring p_user_id = auth.uid() in the
security-definer query, while preserving the service-controlled subscription
checks and existing authenticated execution grant.
- Around line 18-24: Add a nullable Lemon Squeezy invoice ID column and a
uniqueness constraint for payment events in usage_events within
supabase/migrations/20260718130000_billing_subscriptions.sql. Update the
payment-event insert logic in supabase/functions/lemonqueezy-webhook/index.ts to
populate that invoice ID and use upsert/on-conflict-do-nothing behavior, while
preserving non-payment event handling.
- Around line 6-9: Align
supabase/migrations/20260718130000_billing_subscriptions.sql (lines 6-9) with
the fields written by supabase/functions/lemonqueezy-webhook/index.ts (lines
89-122), using consistent Lemon Squeezy subscription/customer and plan
identifiers so the upsert succeeds; expand the status constraint to accept the
provider’s lifecycle values. In supabase/functions/lemonqueezy-webhook/index.ts
(lines 127-140), handle subscription_deleted by retaining pro access until
ends_at rather than immediately changing the profile to free.
In `@supabase/migrations/20260718140000_referral_system.sql`:
- Around line 24-29: Update
supabase/migrations/20260718140000_referral_system.sql at lines 24-29 in
generate_referral_code to set search_path = public and reject authenticated
callers whose auth.uid() differs from p_user_id; at lines 62-67 in
get_referral_stats, set the secure search path and restrict results to the
caller’s own p_user_id for authenticated users; at lines 81-86 in the third
SECURITY DEFINER function, set search_path = public, remove the unused p_user_id
parameter, and update its calling Edge Function accordingly.
- Around line 43-47: Separate permanent user referral codes from the one-to-many
referrals log: add or reuse a per-user code store with a unique user identifier
and unique referral code, update the referral-code creation and lookup logic to
use it, and remove the invalid ON CONFLICT (referrer_id) upsert from referrals.
Keep public.referrals focused on individual referral records, including
referred_email and status, so get_referral_stats can count multiple referrals
per referrer.
---
Nitpick comments:
In `@supabase/functions/sync-contacts/index.ts`:
- Around line 20-25: Remove the unused getServiceClient function and delete the
createClient import if no other code references it, leaving the Resend API
integration unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1e05e6e6-64a6-419b-a854-5377be959794
📒 Files selected for processing (16)
public/_headersscripts/cspHeaders.jssrc/contexts/AuthProvider.jsxsrc/pages/ProComingSoonPage.jsxsrc/pages/VisualizerApp.jsxsrc/security/cspHeaders.test.jssrc/services/analytics.jssrc/services/analyticsEvents.jssrc/services/entitlementService.jssrc/test/setup.jssupabase/functions/lemonqueezy-webhook/index.tssupabase/functions/sync-contacts/index.tssupabase/migrations/20260718120000_user_sessions_analytics.sqlsupabase/migrations/20260718130000_billing_subscriptions.sqlsupabase/migrations/20260718140000_referral_system.sqlvite.config.js
| syncContactToResend(nextSession.user, { | ||
| displayName: | ||
| nextSession.user.user_metadata?.full_name || | ||
| nextSession.user.user_metadata?.name, | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add test coverage and deduplicate display name logic.
Codecov indicates that this new logic is not covered by tests. Please update AuthProvider.test.jsx to mock and verify the contact synchronization behavior during sign-in.
Additionally, to keep the code DRY, consider extracting the display name fallback logic into a shared local variable, as it is identical to the logic used in the identifyUser call just above this block.
♻️ Proposed refactor for deduplication
- identifyUser(nextSession.user, {
- email: nextSession.user.email,
- displayName:
- nextSession.user.user_metadata?.full_name ||
- nextSession.user.user_metadata?.name,
- plan: null,
- });
- trackSignInCompleted();
- syncContactToResend(nextSession.user, {
- displayName:
- nextSession.user.user_metadata?.full_name ||
- nextSession.user.user_metadata?.name,
- });
+ const fallbackDisplayName =
+ nextSession.user.user_metadata?.full_name ||
+ nextSession.user.user_metadata?.name;
+
+ identifyUser(nextSession.user, {
+ email: nextSession.user.email,
+ displayName: fallbackDisplayName,
+ plan: null,
+ });
+ trackSignInCompleted();
+ syncContactToResend(nextSession.user, {
+ displayName: fallbackDisplayName,
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| syncContactToResend(nextSession.user, { | |
| displayName: | |
| nextSession.user.user_metadata?.full_name || | |
| nextSession.user.user_metadata?.name, | |
| }); | |
| const fallbackDisplayName = | |
| nextSession.user.user_metadata?.full_name || | |
| nextSession.user.user_metadata?.name; | |
| identifyUser(nextSession.user, { | |
| email: nextSession.user.email, | |
| displayName: fallbackDisplayName, | |
| plan: null, | |
| }); | |
| trackSignInCompleted(); | |
| syncContactToResend(nextSession.user, { | |
| displayName: fallbackDisplayName, | |
| }); |
🧰 Tools
🪛 GitHub Check: codecov/patch
[warning] 222-222: src/contexts/AuthProvider.jsx#L222
Added line #L222 was not covered by tests
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/contexts/AuthProvider.jsx` around lines 219 - 223, Update the sign-in
flow around syncContactToResend and identifyUser to compute the user’s display
name once in a shared local variable, preserving the full_name-then-name
fallback, and reuse it for both calls. Extend AuthProvider.test.jsx to mock
syncContactToResend and verify it is invoked during sign-in with the expected
user and displayName.
Source: Linters/SAST tools
| // Exported constant for backward compatibility (reads once at module load) | ||
| export const ANONYMOUS_VISUALIZATION_LIMIT = | ||
| DEFAULT_ANONYMOUS_VISUALIZATION_LIMIT; | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Static default limit causes analytics and UI mismatch with dynamic feature flags.
Because the anonymous visualization limit is now dynamically determined via feature flags (getAnonymousVisualizationLimit), the static ANONYMOUS_VISUALIZATION_LIMIT export will cause consumers to track and display the wrong limit (e.g., showing 12 when the user actually hit a feature-flagged limit of 8).
src/services/entitlementService.js#L34-L37: exportgetAnonymousVisualizationLimitinstead of (or alongside) the static constant.src/pages/VisualizerApp.jsx#L419-L425: call the exportedgetAnonymousVisualizationLimit()function to populatetrackUpgradeLimitHitandopenGatedFeatureso analytics and UI messaging reflect the actual limit enforced.
📍 Affects 2 files
src/services/entitlementService.js#L34-L37(this comment)src/pages/VisualizerApp.jsx#L419-L425
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/services/entitlementService.js` around lines 34 - 37, Replace or
supplement the static ANONYMOUS_VISUALIZATION_LIMIT export in
src/services/entitlementService.js lines 34-37 with the dynamic
getAnonymousVisualizationLimit export. In src/pages/VisualizerApp.jsx lines
419-425, call getAnonymousVisualizationLimit() when supplying the limit to
trackUpgradeLimitHit and openGatedFeature, ensuring analytics and UI use the
feature-flagged value.
| const userId = attrs?.custom_data?.user_id || attrs?.user_id || null; | ||
| const email = attrs?.customer_email || attrs?.user_email || null; | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target file around the referenced lines
file='supabase/functions/lemonqueezy-webhook/index.ts'
wc -l "$file"
sed -n '1,220p' "$file"
# Find other references to Lemon Squeezy webhook fields
rg -n "custom_data|customer_email|user_email|meta\\.|subscription|payment" supabase/functions/lemonqueezy-webhook -SRepository: ayoub3bidi/bayan-flow
Length of output: 6926
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether the repo documents the webhook payload shape
rg -n "event.meta.custom_data|custom_data|Lemon Squeezy|lemonsqueezy" -S .Repository: ayoub3bidi/bayan-flow
Length of output: 1906
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for any tests or mocks that describe the webhook payload structure
rg -n "lemonqueezy-webhook|custom_data|meta" -S supabase functions test tests __tests__ . 2>/dev/null || trueRepository: ayoub3bidi/bayan-flow
Length of output: 45382
🌐 Web query:
Lemon Squeezy webhook custom_data event.meta.custom_data subscription attributes docs
💡 Result:
In Lemon Squeezy, custom data passed during checkout is accessible in webhook payloads via the meta.custom_data object [1][2][3]. This object is available for all Order, Subscription, and License Key-related events [1][2]. How it works: 1. Passing Data: You can include custom data in your checkout by adding URL parameters (e.g., checkout[custom][user_id]=123) to your checkout link [2][3] or by including a custom object within checkout_data.custom when creating a checkout via the API [2][3]. 2. Webhook Access: When a qualifying event occurs (like subscription_created or order_created), the webhook payload will include the custom_data object inside the meta field [2][4]. Example Payload Structure: { "meta": { "event_name": "subscription_created", "custom_data": { "user_id": "123", "campaign_id": "abc" } }, "data": { "type": "subscriptions",... } } This feature is designed for developers to link external identifiers (such as local database user IDs or session tokens) to Lemon Squeezy objects without exposing that data to customers [2][3].
Citations:
- 1: https://docs.lemonsqueezy.com/help/webhooks/webhook-requests
- 2: https://docs.lemonsqueezy.com/help/checkout/passing-custom-data
- 3: https://docs.lemonsqueezy.com/guides/developer-guide/taking-payments
- 4: https://docs.lemonsqueezy.com/guides/developer-guide/webhooks
🌐 Web query:
site:docs.lemonsqueezy.com webhooks custom_data meta custom_data subscriptions attributes Lemon Squeezy
💡 Result:
In Lemon Squeezy, when you pass custom data to a checkout, that information is returned within the meta field of relevant webhook payloads [1][2]. When an Order, Subscription, or License key-related event occurs, the webhook payload includes a meta object [1][2]. Your custom data will be located inside this meta object, specifically under the key custom_data [1][2]. For example, if you pass a user_id of 123 to a checkout, the incoming webhook payload will look like this: { "meta": { "event_name": "subscription_created", "custom_data": { "user_id": 123 } }, "data": { "type": "subscriptions", "attributes": {... } } } You can pass this data either via query string parameters in checkout links (e.g., checkout[custom][user_id]=123) or by including a custom object within checkout_data when using the API [3][2]. This custom_data is distinct from the Subscription object's attributes, which contain standard fields like status, user_email, and renewal information [4]. Accessing it via meta.custom_data allows you to easily associate Lemon Squeezy events with your internal user identifiers or session data [3][2].
Citations:
- 1: https://docs.lemonsqueezy.com/help/webhooks/webhook-requests
- 2: https://docs.lemonsqueezy.com/help/checkout/passing-custom-data
- 3: https://docs.lemonsqueezy.com/guides/developer-guide/taking-payments
- 4: https://docs.lemonsqueezy.com/api/subscriptions/the-subscription-object
Read userId from event.meta.custom_data. attrs.custom_data isn’t where Lemon Squeezy puts checkout metadata, so subscription_created, subscription_updated, and payment events all end up with userId = null and skip the Supabase writes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/functions/lemonqueezy-webhook/index.ts` around lines 78 - 80, Update
the webhook event handling to read userId from event.meta.custom_data for
subscription_created, subscription_updated, and payment events, while preserving
the existing fallback behavior where applicable. Ensure the Supabase write paths
receive the extracted user ID instead of null.
| if (error) { | ||
| console.error("Upsert subscription error:", error); | ||
| } | ||
|
|
||
| if (status === "active") { | ||
| const { error: profileError } = await supabase | ||
| .from("profiles") | ||
| .update({ plan: "pro" }) | ||
| .eq("id", userId); | ||
|
|
||
| if (profileError) { | ||
| console.error("Update profile plan error:", profileError); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="supabase/functions/lemonqueezy-webhook/index.ts"
echo "== file size =="
wc -l "$FILE"
echo
echo "== outline (if available) =="
ast-grep outline "$FILE" --view expanded || true
echo
echo "== relevant slices =="
sed -n '1,240p' "$FILE" | nl -ba | sed -n '1,240p'Repository: ayoub3bidi/bayan-flow
Length of output: 484
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="supabase/functions/lemonqueezy-webhook/index.ts"
echo "== lines 1-240 with numbers =="
awk '{printf "%4d %s\n", NR, $0}' "$FILE" | sed -n '1,240p'
echo
echo "== search for return/status/error handling =="
rg -n 'return new Response|Response\(|status === "active"|error:|console\.error|throw|catch' "$FILE"Repository: ayoub3bidi/bayan-flow
Length of output: 7447
Propagate Supabase write failures
The webhook logs or ignores Supabase errors in the subscription and payment branches, then still returns 200. That suppresses Lemon Squeezy retries and can drop subscription/profile updates; throw on any write error at minimum, or move the writes into a transactional RPC.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/functions/lemonqueezy-webhook/index.ts` around lines 109 - 121,
Update the webhook write paths around the subscription upsert and profile plan
update to throw when Supabase returns an error instead of only logging and
continuing. Apply the same failure propagation to the payment branch’s writes,
ensuring any write failure exits the handler with a non-200 response so Lemon
Squeezy retries the webhook; preserve successful processing behavior.
| lemonqueezy_subscription_id text unique, | ||
| lemonqueezy_customer_id text, | ||
| plan text default 'free' check (plan in ('free', 'pro')), | ||
| status text default 'active' check (status in ('active', 'cancelled', 'past_due', 'trialing')), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== File list ==\n'
git ls-files 'supabase/migrations/*.sql' 'supabase/functions/lemonqueezy-webhook/index.ts'
printf '\n== Migration snippet ==\n'
nl -ba supabase/migrations/20260718130000_billing_subscriptions.sql | sed -n '1,120p'
printf '\n== Webhook snippet 1 ==\n'
nl -ba supabase/functions/lemonqueezy-webhook/index.ts | sed -n '1,220p'
printf '\n== Search for subscription schema usage ==\n'
rg -n "lemonqueezy_(subscription|customer|id|plan_id)|create table subscriptions|table subscriptions|status text default 'active'" supabase -g '!**/node_modules/**'Repository: ayoub3bidi/bayan-flow
Length of output: 1276
🌐 Web query:
Lemon Squeezy subscription statuses active cancelled past_due trialing on_trial paused unpaid expired docs
💡 Result:
Lemon Squeezy uses the following subscription statuses to track the lifecycle of a subscription [1][2]: active The subscription is currently active and valid [1][2]. on_trial The subscription has started on a trial period and is awaiting its first scheduled payment [1][2]. paused Payment collection for the subscription has been paused [1][2]. The subscription remains active, but no payments are collected [1]. past_due A renewal payment has failed [1][2]. The system will attempt up to 4 payment retries over a 2-week period [1]. If a retry succeeds, the status returns to active [1]. unpaid All 4 renewal payment retries have failed [1][2]. Depending on store settings (dunning rules), the subscription may either remain in this state or eventually transition to expired [1]. cancelled The customer or store owner has cancelled future payments [1][2]. The subscription remains technically active and valid until the end of the current billing period (the grace period), at which point it expires [1][3]. The ends_at field indicates when the subscription is scheduled to expire [1]. expired The subscription has fully ended [1][2]. This occurs if a cancelled subscription reaches its renewal date without being resumed, or if an unpaid subscription is not reactivated during the dunning process [1][3]. Customers should no longer have access to the product [1][2].
Citations:
- 1: https://docs.lemonsqueezy.com/api/subscriptions/the-subscription-object
- 2: https://docs.lemonsqueezy.com/help/products/subscriptions
- 3: https://docs.lemonsqueezy.com/guides/developer-guide/managing-subscriptions
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Other webhook references ==\n'
rg -n "subscription_deleted|subscription_updated|subscription_created|on_trial|paused|unpaid|expired|cancelled|past_due|trialing" supabase/functions -g '!**/node_modules/**'
printf '\n== Any profiles.plan constraints ==\n'
rg -n "plan text default 'free'|check \\(plan in|update \\{ plan: \"pro\" \\}|update \\{ plan: \"free\" \\}" supabase -g '!**/node_modules/**'Repository: ayoub3bidi/bayan-flow
Length of output: 852
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Billing migration ==\n'
sed -n '1,120p' supabase/migrations/20260718130000_billing_subscriptions.sql
printf '\n== Lemon Squeezy webhook ==\n'
sed -n '1,220p' supabase/functions/lemonqueezy-webhook/index.tsRepository: ayoub3bidi/bayan-flow
Length of output: 7625
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Search for subscriptions table references ==\n'
rg -n "\bsubscriptions\b|lemonqueezy_subscription_id|lemonqueezy_customer_id|plan_id|current_period_start|current_period_end" supabase -g '!**/node_modules/**'Repository: ayoub3bidi/bayan-flow
Length of output: 1744
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('supabase/functions/lemonqueezy-webhook/index.ts')
text = p.read_text()
for needle in ["subscription_created", "subscription_updated", "subscription_deleted", "status === \"active\"", "update({ plan: \"pro\" })", "update({ plan: \"free\" })"]:
idx = text.find(needle)
print(f"\n--- {needle} @ {idx} ---")
if idx != -1:
start = max(0, text.rfind('\n', 0, idx-200))
end = text.find('\n', idx+300)
if end == -1:
end = len(text)
print(text[start:end])
PYRepository: ayoub3bidi/bayan-flow
Length of output: 3672
Align the webhook payload with public.subscriptions and defer access removal until expiry.
supabase/functions/lemonqueezy-webhook/index.ts writes lemonqueezy_id/plan_id, but supabase/migrations/20260718130000_billing_subscriptions.sql only defines lemonqueezy_subscription_id, lemonqueezy_customer_id, plan, and status, so the upsert fails while the profile can still be marked pro. The status constraint is also too narrow for Lemon Squeezy’s lifecycle, and subscription_deleted should not immediately flip the user back to free before ends_at.
📍 Affects 2 files
supabase/migrations/20260718130000_billing_subscriptions.sql#L6-L9(this comment)supabase/functions/lemonqueezy-webhook/index.ts#L89-L122supabase/functions/lemonqueezy-webhook/index.ts#L127-L140
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/migrations/20260718130000_billing_subscriptions.sql` around lines 6
- 9, Align supabase/migrations/20260718130000_billing_subscriptions.sql (lines
6-9) with the fields written by supabase/functions/lemonqueezy-webhook/index.ts
(lines 89-122), using consistent Lemon Squeezy subscription/customer and plan
identifiers so the upsert succeeds; expand the status constraint to accept the
provider’s lifecycle values. In supabase/functions/lemonqueezy-webhook/index.ts
(lines 127-140), handle subscription_deleted by retaining pro access until
ends_at rather than immediately changing the profile to free.
| create table if not exists public.usage_events ( | ||
| id bigserial primary key, | ||
| user_id uuid references auth.users(id) on delete cascade, | ||
| event_type text not null, | ||
| metadata jsonb default '{}', | ||
| created_at timestamptz default now() | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Files ==\n'
git ls-files 'supabase/migrations/*.sql' 'supabase/functions/lemonqueezy-webhook/index.ts' | sed 's#^`#-` #'
printf '\n== Search usage_events and webhook references ==\n'
rg -n "usage_events|subscription_payment|subscription_payment_failed|lemonqueezy_id|event_id|webhook" supabase -g '!**/dist/**' -g '!**/build/**' || true
printf '\n== Inspect target migration ==\n'
sed -n '1,120p' supabase/migrations/20260718130000_billing_subscriptions.sql
printf '\n== Inspect webhook handler region ==\n'
sed -n '110,220p' supabase/functions/lemonqueezy-webhook/index.tsRepository: ayoub3bidi/bayan-flow
Length of output: 9493
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('supabase/functions/lemonqueezy-webhook/index.ts')
text = p.read_text()
for needle in ['subscription_payment_success', 'subscription_payment_failed', 'data.id', 'attrs?.invoice_id', 'attrs?.id', 'attrs?.event_id', 'event.id']:
print(f"\n-- {needle} --")
idx = text.find(needle)
if idx == -1:
print("not found")
continue
start = max(0, idx - 220)
end = min(len(text), idx + 520)
print(text[start:end])
PYRepository: ayoub3bidi/bayan-flow
Length of output: 2589
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Target file sizes ==\n'
wc -l supabase/migrations/20260718130000_billing_subscriptions.sql supabase/functions/lemonqueezy-webhook/index.ts
printf '\n== Migration excerpt ==\n'
nl -ba supabase/migrations/20260718130000_billing_subscriptions.sql | sed -n '1,120p'
printf '\n== Webhook excerpt ==\n'
nl -ba supabase/functions/lemonqueezy-webhook/index.ts | sed -n '120,220p'Repository: ayoub3bidi/bayan-flow
Length of output: 379
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for path in ['supabase/migrations/20260718130000_billing_subscriptions.sql', 'supabase/functions/lemonqueezy-webhook/index.ts']:
print(f"\n## {path}")
text = Path(path).read_text().splitlines()
for i, line in enumerate(text, 1):
if 'usage_events' in line or 'subscription_payment' in line or 'lemonqueezy_id' in line or 'invoice' in line or 'event' in line:
lo = max(1, i-3)
hi = min(len(text), i+5)
for j in range(lo, hi+1):
print(f"{j:4d}: {text[j-1]}")
print("----")
PYRepository: ayoub3bidi/bayan-flow
Length of output: 8348
🏁 Script executed:
#!/bin/bash
set -euo pipefail
set +e
echo "files:"
git ls-files 'supabase/migrations/*.sql' 'supabase/functions/lemonqueezy-webhook/index.ts'
echo
echo "search:"
rg -n "const .*data|type .*Webhook|subscription_payment_success|subscription_payment_failed|usage_events|lemonqueezy_id|invoice_id|event_id" supabase/functions/lemonqueezy-webhook/index.ts supabase/migrations/20260718130000_billing_subscriptions.sql
echo
echo "migration:"
nl -ba supabase/migrations/20260718130000_billing_subscriptions.sql | sed -n '1,80p'
echo
echo "webhook:"
nl -ba supabase/functions/lemonqueezy-webhook/index.ts | sed -n '1,260p'Repository: ayoub3bidi/bayan-flow
Length of output: 3156
🌐 Web query:
Lemon Squeezy webhook payload data.id event object identifier invoice identifier docs subscription_payment_success
💡 Result:
In a Lemon Squeezy webhook payload for the subscription_payment_success event, the data.id field represents the unique identifier of the Subscription Invoice object associated with that specific payment [1]. When this event is triggered, the webhook payload contains a meta object (providing context such as the event name) and a data object, which corresponds to the Subscription Invoice resource [1][2]. Within this data object: - data.id: This is the unique identifier for the Subscription Invoice (e.g., '1') [3]. - data.attributes.subscription_id: This attribute identifies the parent Subscription to which the invoice relates [3]. Because subscription_payment_success is explicitly defined to return a Subscription Invoice object [1], the data.id does not refer to the Subscription object itself, but to the specific invoice record generated for that successful payment. You can use this invoice ID to retrieve full details about the transaction via the /v1/subscription-invoices/:id API endpoint [4].
Citations:
- 1: https://docs.lemonsqueezy.com/help/webhooks/event-types
- 2: https://gist.github.com/amosbastian/e403e1d8ccf4f7153f7840dd11a85a69
- 3: https://docs.lemonsqueezy.com/api/subscription-invoices/list-all-subscription-invoices
- 4: https://docs.lemonsqueezy.com/api/subscription-invoices/retrieve-subscription-invoice
Make payment-event inserts idempotent.
data.id is the Lemon Squeezy subscription-invoice ID for payment webhooks, but usage_events has no unique constraint on it, so retries/manual resends can create duplicate rows. Add a dedicated invoice-id column or constrain (event_type, lemonqueezy_invoice_id), then write with upsert/on conflict do nothing.
supabase/migrations/20260718130000_billing_subscriptions.sqlsupabase/functions/lemonqueezy-webhook/index.ts
📍 Affects 2 files
supabase/migrations/20260718130000_billing_subscriptions.sql#L18-L24(this comment)supabase/functions/lemonqueezy-webhook/index.ts#L143-L172
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/migrations/20260718130000_billing_subscriptions.sql` around lines 18
- 24, Add a nullable Lemon Squeezy invoice ID column and a uniqueness constraint
for payment events in usage_events within
supabase/migrations/20260718130000_billing_subscriptions.sql. Update the
payment-event insert logic in supabase/functions/lemonqueezy-webhook/index.ts to
populate that invoice ID and use upsert/on-conflict-do-nothing behavior, while
preserving non-payment event handling.
| create or replace function public.is_pro_user(p_user_id uuid) | ||
| returns boolean | ||
| language sql | ||
| security definer | ||
| stable | ||
| as $$ | ||
| select exists ( | ||
| select 1 from public.subscriptions | ||
| where user_id = p_user_id | ||
| and plan = 'pro' | ||
| and status = 'active' | ||
| ); | ||
| $$; | ||
|
|
||
| grant execute on function public.is_pro_user(uuid) to authenticated; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Preserve ownership checks inside the security-definer RPC.
is_pro_user() bypasses RLS and accepts an arbitrary user ID. Restrict execution and require p_user_id = auth.uid() so callers cannot inspect another account’s billing status.
Proposed hardening
language sql
security definer
+set search_path = ''
stable
as $$
- select exists (
+ select p_user_id = auth.uid() and exists (
select 1 from public.subscriptions
where user_id = p_user_id
and plan = 'pro'
and status = 'active'
);
$$;
+revoke execute on function public.is_pro_user(uuid) from public, anon;
grant execute on function public.is_pro_user(uuid) to authenticated;As per coding guidelines, “Apply RLS to public tables” and preserve the service-controlled Pro security boundary. <coding_guidelines>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| create or replace function public.is_pro_user(p_user_id uuid) | |
| returns boolean | |
| language sql | |
| security definer | |
| stable | |
| as $$ | |
| select exists ( | |
| select 1 from public.subscriptions | |
| where user_id = p_user_id | |
| and plan = 'pro' | |
| and status = 'active' | |
| ); | |
| $$; | |
| grant execute on function public.is_pro_user(uuid) to authenticated; | |
| create or replace function public.is_pro_user(p_user_id uuid) | |
| returns boolean | |
| language sql | |
| security definer | |
| set search_path = '' | |
| stable | |
| as $$ | |
| select p_user_id = auth.uid() and exists ( | |
| select 1 from public.subscriptions | |
| where user_id = p_user_id | |
| and plan = 'pro' | |
| and status = 'active' | |
| ); | |
| $$; | |
| revoke execute on function public.is_pro_user(uuid) from public, anon; | |
| grant execute on function public.is_pro_user(uuid) to authenticated; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/migrations/20260718130000_billing_subscriptions.sql` around lines 58
- 72, Update public.is_pro_user so it only returns the authenticated caller’s
own Pro status by requiring p_user_id = auth.uid() in the security-definer
query, while preserving the service-controlled subscription checks and existing
authenticated execution grant.
Source: Coding guidelines
| create or replace function public.generate_referral_code(p_user_id uuid) | ||
| returns text | ||
| language plpgsql | ||
| security definer | ||
| as $$ | ||
| declare |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Insecure SECURITY DEFINER functions: missing search_path and authorization.
All three SECURITY DEFINER functions fail to set a secure search_path, which exposes them to search path injection attacks. Additionally, generate_referral_code and get_referral_stats are vulnerable to Insecure Direct Object Reference (IDOR) because they accept p_user_id without verifying it matches the caller's auth.uid(), allowing any authenticated client to view or generate codes for other users.
supabase/migrations/20260718140000_referral_system.sql#L24-L29: Addset search_path = publicto the function definition, and add an authorization check to prevent IDOR (e.g.,if auth.role() = 'authenticated' and auth.uid() != p_user_id then raise exception 'Unauthorized'; end if;).supabase/migrations/20260718140000_referral_system.sql#L62-L67: Addset search_path = publicto the function definition, and constrain the query to prevent IDOR (e.g.,where referrer_id = p_user_id and (auth.uid() = p_user_id or auth.role() != 'authenticated')).supabase/migrations/20260718140000_referral_system.sql#L81-L86: Addset search_path = publicto the function definition, and remove the completely unusedp_user_idparameter (ensure the calling Edge Function is updated as well).
📍 Affects 1 file
supabase/migrations/20260718140000_referral_system.sql#L24-L29(this comment)supabase/migrations/20260718140000_referral_system.sql#L62-L67supabase/migrations/20260718140000_referral_system.sql#L81-L86
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/migrations/20260718140000_referral_system.sql` around lines 24 - 29,
Update supabase/migrations/20260718140000_referral_system.sql at lines 24-29 in
generate_referral_code to set search_path = public and reject authenticated
callers whose auth.uid() differs from p_user_id; at lines 62-67 in
get_referral_stats, set the secure search path and restrict results to the
caller’s own p_user_id for authenticated users; at lines 81-86 in the third
SECURITY DEFINER function, set search_path = public, remove the unused p_user_id
parameter, and update its calling Edge Function accordingly.
22e321c to
905c9dc
Compare
Track waitlist joins and anonymous viz-limit hits, enable SPA pageviews via history_change, and sample session replay at 20%.
Add sync-contacts with atomic welcome_email_sent_at claim, styled Resend templates, and Resend segment-based contact upsert. Drop premature billing/referral scaffolding that was out of scope for 0.5.0.
Verify Cloudflare Turnstile in before-signup, send Telegram alerts on signup rejects and new registrations, require email confirmation for account deletion, and tighten edge-function CORS for production.
Block fork workflow_run deploys, move secrets into step env blocks, and keep Supabase keepalive credentials out of inline shell expansion.
58b90e8 to
16cfcdc
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
src/pages/ProfileSettingsPage.jsx (2)
541-544: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMatch case-insensitive logic in the disabled state.
Ensure the button's disabled state uses the same case-insensitive comparison as the submit handler.
💡 Proposed fix
disabled={ isDeletingAccount || - deleteConfirmText.trim() !== user.email + deleteConfirmText.trim().toLowerCase() !== user.email?.toLowerCase() }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/ProfileSettingsPage.jsx` around lines 541 - 544, Update the disabled condition for the account deletion button in ProfileSettingsPage to compare deleteConfirmText and user.email case-insensitively, matching the submit handler’s logic while preserving the existing isDeletingAccount check.
191-194: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMake the email confirmation case-insensitive.
Mobile keyboards often auto-capitalize the first letter of an input. Comparing the emails case-insensitively prevents user frustration during this critical flow. Also, consider safely handling
user.emailin case it is undefined.💡 Proposed fix
const handleDeleteAccount = async () => { - if (!user || isDeletingAccount || deleteConfirmText.trim() !== user.email) { + if (!user || isDeletingAccount || deleteConfirmText.trim().toLowerCase() !== user.email?.toLowerCase()) { return; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/ProfileSettingsPage.jsx` around lines 191 - 194, Update handleDeleteAccount so deleteConfirmText is compared to user.email case-insensitively after trimming, while safely handling an undefined user.email without throwing. Preserve the existing early-return behavior for invalid confirmation, missing users, or an active deletion.src/i18n/locales/en/translation.json (1)
55-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove unused translation keys.
Since
ProfileSettingsPage.jsxnow dynamically usesuser.emailfor the input placeholder and the confirmation match logic, the newly addeddeleteAccountConfirmPlaceholderand the olddeleteAccountConfirmWordkeys are unused and can be safely removed to keep the translation files clean.
src/i18n/locales/en/translation.json#L55-L58: Remove"deleteAccountConfirmPlaceholder"and"deleteAccountConfirmWord".src/i18n/locales/ar/translation.json#L55-L58: Remove"deleteAccountConfirmPlaceholder"and"deleteAccountConfirmWord".src/i18n/locales/fr/translation.json#L55-L58: Remove"deleteAccountConfirmPlaceholder"and"deleteAccountConfirmWord".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/locales/en/translation.json` around lines 55 - 58, Remove the unused deleteAccountConfirmPlaceholder and deleteAccountConfirmWord translation keys from the English, Arabic, and French translation files at the specified sites, preserving deleteAccountConfirmLabel and deleteAccountInProgress.supabase/functions/before-signup/index.ts (1)
122-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAwait the
rejectfunction.Since you'll need to make
rejectanasyncfunction to ensuresendTelegramAlertcompletes (as raised in the consolidated comments), you should also optionallyawaitthereject()call here (and everywhere else it's called) for clarity, even though returning a Promise inside anasynchandler automatically unwraps it.🛠️ Proposed fix to await `reject`
if (!turnstile.success) { - return reject('turnstile_failed', { ip }); + return await reject('turnstile_failed', { ip }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/functions/before-signup/index.ts` around lines 122 - 130, Update the turnstile failure branch in the signup handler to await the async reject function before returning its result, and apply the same awaited call consistently at every other reject invocation in this handler.src/services/authService.js (1)
24-61: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a timeout to prevent the sign-in flow from hanging.
If the Turnstile script is loaded but network issues or ad-blockers prevent it from initializing or firing callbacks, the promise will never resolve and the sign-in flow will hang indefinitely. Adding a short timeout (e.g., 10 seconds) that resolves to
nullensures the flow gracefully proceeds to the backend for evaluation.⏱️ Proposed fix to add a timeout
function getTurnstileToken() { return new Promise(resolve => { const siteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY; if ( !siteKey || typeof (/** `@type` {any} */ (globalThis).turnstile) === 'undefined' ) { resolve(null); return; } + const timeoutId = setTimeout(() => { + container.remove(); + resolve(null); + }, 10000); + const turnstile = /** `@type` {any} */ (globalThis).turnstile; const container = document.createElement('div'); container.style.cssText = 'position:fixed;bottom:0;left:0;width:1px;height:1px;overflow:hidden;opacity:0.01;pointer-events:none;'; document.body.appendChild(container); turnstile.render(container, { sitekey: siteKey, appearance: 'execute', callback: (/** `@type` {string} */ token) => { + clearTimeout(timeoutId); container.remove(); resolve(token); }, 'error-callback': () => { + clearTimeout(timeoutId); container.remove(); resolve(null); }, }); }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/authService.js` around lines 24 - 61, Update getTurnstileToken to add a short timeout (for example, 10 seconds) that resolves the promise with null and removes the rendered container if Turnstile never initializes or invokes a callback. Clear the timeout when either callback resolves first, preserving the existing token and error-callback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/contexts/AuthProvider.jsx`:
- Around line 34-41: Update the sync-contacts invocation in AuthProvider to
attach a catch handler to the returned promise, handling rejected network or
Edge Function calls without awaiting or blocking the surrounding flow. Preserve
the existing request payload and fire-and-forget behavior.
In `@supabase/functions/before-signup/index.ts`:
- Around line 27-34: Update the reject function in
supabase/functions/before-signup/index.ts (lines 27-34) to be async and await
its sendTelegramAlert call; also add await to sendTelegramAlert in
supabase/functions/platform-access/index.ts (lines 87-90), preserving the
existing alert messages.
In `@supabase/functions/platform-access/index.ts`:
- Around line 49-56: Update the userError handling in the platform-access
authorization flow to remove the special fail-closed response for
userError?.code. Ensure malformed JWTs and all other authentication errors
continue through the existing fail-open path, reserving allowed: false responses
only for the account_banned reason.
In `@supabase/functions/sync-contacts/index.ts`:
- Around line 300-310: Update the welcome-email flow around
claimWelcomeEmailSlot and sendWelcomeEmail so rollbackWelcomeEmailClaim always
runs whenever sending fails, including rejected fetch or response.text
operations. Move the rollback into a finally block scoped to the claimed slot,
while preserving the existing behavior of setting welcomeSent from successful
sends and allowing successful claims to remain marked.
---
Nitpick comments:
In `@src/i18n/locales/en/translation.json`:
- Around line 55-58: Remove the unused deleteAccountConfirmPlaceholder and
deleteAccountConfirmWord translation keys from the English, Arabic, and French
translation files at the specified sites, preserving deleteAccountConfirmLabel
and deleteAccountInProgress.
In `@src/pages/ProfileSettingsPage.jsx`:
- Around line 541-544: Update the disabled condition for the account deletion
button in ProfileSettingsPage to compare deleteConfirmText and user.email
case-insensitively, matching the submit handler’s logic while preserving the
existing isDeletingAccount check.
- Around line 191-194: Update handleDeleteAccount so deleteConfirmText is
compared to user.email case-insensitively after trimming, while safely handling
an undefined user.email without throwing. Preserve the existing early-return
behavior for invalid confirmation, missing users, or an active deletion.
In `@src/services/authService.js`:
- Around line 24-61: Update getTurnstileToken to add a short timeout (for
example, 10 seconds) that resolves the promise with null and removes the
rendered container if Turnstile never initializes or invokes a callback. Clear
the timeout when either callback resolves first, preserving the existing token
and error-callback behavior.
In `@supabase/functions/before-signup/index.ts`:
- Around line 122-130: Update the turnstile failure branch in the signup handler
to await the async reject function before returning its result, and apply the
same awaited call consistently at every other reject invocation in this handler.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6954274a-e91e-4772-92b1-f6e449833e50
📒 Files selected for processing (33)
.github/workflows/deploy-cloudflare.yml.github/workflows/deploy-supabase-functions.yml.github/workflows/ensure-pr-source-develop.yml.github/workflows/keep-supabase-alive.ymlAGENTS.mddocs/AGENTS_REFERENCE.mdindex.htmlsrc/contexts/AuthProvider.jsxsrc/contexts/AuthProvider.test.jsxsrc/i18n/locales/ar/translation.jsonsrc/i18n/locales/en/translation.jsonsrc/i18n/locales/fr/translation.jsonsrc/pages/ProComingSoonPage.jsxsrc/pages/ProfileSettingsPage.jsxsrc/pages/ProfileSettingsPage.test.jsxsrc/pages/VisualizerApp.jsxsrc/security/cspHeaders.test.jssrc/services/analytics.jssrc/services/analyticsEvents.growth.test.jssrc/services/analyticsEvents.jssrc/services/authService.jssrc/services/featureFlags.jssrc/test/setup.jssupabase/functions/_shared/cors.tssupabase/functions/_shared/transactionalEmails.tssupabase/functions/_shared/webhookVerify.tssupabase/functions/before-signup/index.tssupabase/functions/platform-access/index.tssupabase/functions/post-signup/index.tssupabase/functions/sync-contacts/index.tssupabase/functions/waitlist-welcome/index.tssupabase/migrations/20260720180000_drop_premature_saas_scaffolding.sqlsupabase/migrations/20260720190000_profiles_welcome_email_sent_at.sql
🚧 Files skipped from review as they are similar to previous changes (10)
- .github/workflows/deploy-supabase-functions.yml
- src/pages/ProComingSoonPage.jsx
- src/pages/VisualizerApp.jsx
- src/services/analyticsEvents.js
- src/test/setup.js
- src/services/analyticsEvents.growth.test.js
- src/contexts/AuthProvider.test.jsx
- src/services/analytics.js
- AGENTS.md
- docs/AGENTS_REFERENCE.md
…c alerts, UX hardening - Move syncContactToResend from AuthProvider to authService (service layer contract) - Deduplicate displayName fallback in AuthProvider sign-in flow - Add .catch() to fire-and-forget syncContactToResend call - Wrap sendWelcomeEmail in try/finally so rollback always runs on failure - Remove invalid_token fail-closed branch in platform-access (fail-open per contract) - Make before-signup reject() async and await sendTelegramAlert in both edge functions - Add 10s timeout to getTurnstileToken to prevent sign-in hanging - Case-insensitive email confirmation in ProfileSettingsPage - Remove unused deleteAccountConfirmPlaceholder/Word translation keys
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
supabase/functions/platform-access/index.ts (1)
75-81: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDo not report ban synchronization as successful when it failed.
When
banErroris present, the alert still saysauth ban synced, which can mislead incident response while the Auth user remains unbanned. Use a failure-specific message or make the status text conditional onbanError.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/functions/platform-access/index.ts` around lines 75 - 81, Update the sendTelegramAlert call in the banError handling flow so its status text reflects whether auth ban synchronization succeeded or failed. When banError is present, report the failure instead of “auth ban synced”; preserve the success wording only when synchronization completes without an error.src/services/authService.js (2)
47-65: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winsrc/services/authService.js:47-65 — Dispose the Turnstile widget on every terminal path. Capture the widget ID from
turnstile.render()and callturnstile.remove(widgetId)in the timeout, success, and error handlers; removing only the container can leave stale widget state behind.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/authService.js` around lines 47 - 65, Update the Turnstile flow around turnstile.render to capture its widget ID, then call turnstile.remove(widgetId) in the timeout, callback success, and error-callback terminal paths before removing the container and resolving. Preserve the existing timeout and token/null resolution behavior.Source: MCP tools
141-145: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftAlign the Turnstile payload with the actual
signInWithIdTokencontract. Insrc/services/authService.js:141-145,signInWithIdTokenonly forwardsoptions.captchaToken;options.datais ignored here, sobefore-signupnever receivesraw_user_meta_data.cf_turnstile_responseand new Google signups will be rejected. Make the client and hook use the same field end-to-end.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/authService.js` around lines 141 - 145, Update the Turnstile payload construction before signInWithIdToken so it uses the contract’s options.captchaToken field instead of options.data, and ensure the before-signup hook reads that same field while preserving the cf_turnstile_response value for new Google signups.Source: MCP tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/contexts/AuthProvider.jsx`:
- Around line 200-205: Move the post-authentication calls from onAuthStateChange
into a separate effect or deferred task that runs after session state updates
and the auth callback has returned. Ensure both syncContactToResend and
evaluateAccess execute there, preserving their existing inputs and error
handling while preventing Supabase work during the auth callback.
In `@src/services/authService.js`:
- Around line 247-252: Update the sync-contacts request in the
syncContactToResend helper so an unknown profile.plan remains absent or null
instead of being converted to “free”; preserve “free” only when it is an
explicitly known plan, and ensure SIGNED_IN calls cannot downgrade an existing
Pro contact before the real profile is loaded.
---
Outside diff comments:
In `@src/services/authService.js`:
- Around line 47-65: Update the Turnstile flow around turnstile.render to
capture its widget ID, then call turnstile.remove(widgetId) in the timeout,
callback success, and error-callback terminal paths before removing the
container and resolving. Preserve the existing timeout and token/null resolution
behavior.
- Around line 141-145: Update the Turnstile payload construction before
signInWithIdToken so it uses the contract’s options.captchaToken field instead
of options.data, and ensure the before-signup hook reads that same field while
preserving the cf_turnstile_response value for new Google signups.
In `@supabase/functions/platform-access/index.ts`:
- Around line 75-81: Update the sendTelegramAlert call in the banError handling
flow so its status text reflects whether auth ban synchronization succeeded or
failed. When banError is present, report the failure instead of “auth ban
synced”; preserve the success wording only when synchronization completes
without an error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ef7eee31-e7f0-4483-9676-153af97217b8
📒 Files selected for processing (9)
src/contexts/AuthProvider.jsxsrc/i18n/locales/ar/translation.jsonsrc/i18n/locales/en/translation.jsonsrc/i18n/locales/fr/translation.jsonsrc/pages/ProfileSettingsPage.jsxsrc/services/authService.jssupabase/functions/before-signup/index.tssupabase/functions/platform-access/index.tssupabase/functions/sync-contacts/index.ts
💤 Files with no reviewable changes (3)
- src/i18n/locales/ar/translation.json
- src/i18n/locales/fr/translation.json
- src/i18n/locales/en/translation.json
🚧 Files skipped from review as they are similar to previous changes (3)
- src/pages/ProfileSettingsPage.jsx
- supabase/functions/sync-contacts/index.ts
- supabase/functions/before-signup/index.ts
| authService | ||
| .syncContactToResend({ | ||
| plan: null, | ||
| displayName: fallbackDisplayName, | ||
| }) | ||
| .catch(() => {}); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
rg -n -U 'onAuthStateChange\([\s\S]{0,1600}(syncContactToResend|evaluateAccess)' \
src/contexts/AuthProvider.jsxRepository: ayoub3bidi/bayan-flow
Length of output: 1966
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Inspect the auth provider around the callback and the referenced service methods.
sed -n '1,280p' src/contexts/AuthProvider.jsx
printf '\n--- auth service references ---\n'
rg -n "syncContactToResend|evaluateAccess|onAuthStateChange|supabase" src/services src/contexts -g '!**/*.test.*'Repository: ayoub3bidi/bayan-flow
Length of output: 14089
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '\n--- AuthProvider excerpt ---\n'
sed -n '150,240p' src/contexts/AuthProvider.jsx
printf '\n--- syncContactToResend definition ---\n'
rg -n -A 40 -B 10 "syncContactToResend" src
printf '\n--- evaluateAccess definition ---\n'
rg -n -A 60 -B 20 "evaluateAccess" srcRepository: ayoub3bidi/bayan-flow
Length of output: 19426
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
sed -n '220,260p' src/services/authService.jsRepository: ayoub3bidi/bayan-flow
Length of output: 1166
🌐 Web query:
Supabase onAuthStateChange asynchronous client calls deadlock subsequent calls troubleshooting why is my supabase api call not returning
💡 Result:
The issue where Supabase API calls fail to return after being triggered within an onAuthStateChange callback is caused by a deadlock within the Supabase JS client [1][2]. Specifically, when an async function is used as a callback in onAuthStateChange, and that callback attempts to make another Supabase API call (e.g., to postgREST, storage, or auth), the client attempts to acquire an internal lock that is already held by the ongoing auth process [3][4]. This creates a recursive locking condition where the initial operation waits for the callback to finish, while the callback waits for the new API call to acquire the same lock, leading to a permanent hang [2][4]. To troubleshoot and fix this, apply the following strategies: 1. Avoid async calls inside onAuthStateChange: The most direct fix is to ensure that the callback function passed to onAuthStateChange does not contain any awaited Supabase client calls [1][2][4]. 2. Decouple your logic: If you need to perform data fetching based on authentication state changes, move that logic outside of the onAuthStateChange listener [5]. For example, in a React application, you can update a local state variable (e.g., user session or profile) inside the listener and trigger the necessary API calls in a separate useEffect hook that depends on that state [5][6]. 3. Ensure you are on the latest version: Supabase continues to release updates to address locking and concurrency issues [4][7]. Ensure your @supabase/supabase-js package is updated to the latest version, as recent patches have been specifically implemented to mitigate these deadlock scenarios [7]. If you are experiencing hangs not related to onAuthStateChange, check for database-level blocking queries using a lock monitor view, as sometimes performance issues can be mistaken for deadlocks [8].
Citations:
- 1: https://supabase.com/docs/guides/troubleshooting/why-is-my-supabase-api-call-not-returning-PGzXw0
- 2: Supabase operations in onAuthStateChange will cause the next call to supabase anywhere else in the code to not return. supabase/auth-js#762
- 3: feat(auth): add deprecation notice to
onAuthStateChangewith async function supabase/supabase-js#1580 - 4: fix(auth): make _notifyAllSubscribers non-blocking to prevent callback deadlocks supabase/supabase-js#2016
- 5: Supabase JS Client Hangs During onAuthStateChange Fetch After Refresh (Chrome/Expo Web) supabase/supabase-js#1401
- 6: https://supabase.com/docs/reference/javascript/auth-onauthstatechange
- 7: Lock "lock:jabe-auth" was not released within 5000ms causes app to freeze when switching browser tabs supabase/supabase-js#2426
- 8: https://supabase.com/docs/guides/troubleshooting/how-to-check-if-my-queries-are-being-blocked-by-other-queries-NSKtR1
Move the post-auth Supabase work out of onAuthStateChange. This callback starts syncContactToResend() and then evaluateAccess(), and both hit Supabase before the auth lock is released. Defer them to a separate effect/task after session state updates; .catch() doesn’t avoid the lock.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/contexts/AuthProvider.jsx` around lines 200 - 205, Move the
post-authentication calls from onAuthStateChange into a separate effect or
deferred task that runs after session state updates and the auth callback has
returned. Ensure both syncContactToResend and evaluateAccess execute there,
preserving their existing inputs and error handling while preventing Supabase
work during the auth callback.
Source: MCP tools
- Defer syncContactToResend via setTimeout(0) to avoid Supabase auth lock contention - Don't default unknown plan to 'free' — omit it and let edge function read from DB - Wrap sync invocation in .catch() for unhandled rejection safety - Update test to await async sync invocation via waitFor
Summary
waitlist_joined,upgrade_limit_hit, SPA pageviews, sampled session replaysync-contacts(JWT identity only) and deploy it; fire-and-forget onSIGNED_INTest plan
pnpm lint→format:check→test:coverage→buildwaitlist/profiles/keepaliveintactOps notes
976d858b-6904-4207-809f-06453241ac96— confirmRESEND_AUDIENCE_IDon the project matchesdisable_surveys: trueSummary by CodeRabbit