Skip to content

feat: 0.5.0 growth analytics — PostHog events, Resend sync, waitlist - #205

Merged
ayoub3bidi merged 6 commits into
developfrom
feature/saas-analytics-infrastructure
Jul 22, 2026
Merged

feat: 0.5.0 growth analytics — PostHog events, Resend sync, waitlist#205
ayoub3bidi merged 6 commits into
developfrom
feature/saas-analytics-infrastructure

Conversation

@ayoub3bidi

@ayoub3bidi ayoub3bidi commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep conversion analytics for free SaaS + Pro waitlist: waitlist_joined, upgrade_limit_hit, SPA pageviews, sampled session replay
  • Harden sync-contacts (JWT identity only) and deploy it; fire-and-forget on SIGNED_IN
  • Remove LemonSqueezy webhook, billing/referral/session scaffolding (repo + live drop migration); anon viz limit stays hardcoded at 12

Test plan

  • Focused Vitest (AuthProvider, analytics growth events, entitlement, CSP)
  • pnpm lintformat:checktest:coveragebuild
  • Staging smoke after preview deploy: anon viz limit → event; waitlist join → event + welcome mail; sign-in → Resend contact upsert
  • Live DB: scaffolding tables/RPCs gone; waitlist/profiles/keepalive intact
  • PostHog: unused flags archived; 3 growth insights created

Ops notes

  • Resend audience segment General: 976d858b-6904-4207-809f-06453241ac96 — confirm RESEND_AUDIENCE_ID on the project matches
  • Set PostHog org billing caps at free allowances in the PostHog UI (no API for this)
  • Surveys remain unpublished; SDK keeps disable_surveys: true

Summary by CodeRabbit

  • New Features
    • Added bot protection for signup and Google sign-in via invisible Turnstile verification.
    • Signed-in users’ contact details are now synced, with a one-time welcome email sent on first sign-in.
  • Improvements
    • Waitlist and visualization upgrade/limit experiences now record clearer analytics events.
    • Account deletion confirmation is now email-based (updated UI wording and validation).
  • Documentation
    • Updated guidance to reflect the waitlist-only Pro experience and current auth/email behavior.
  • Tests
    • Improved analytics and auth-related test coverage and mocks.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ayoub3bidi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 36d75637-1807-4e63-9282-5e524bcd1b36

📥 Commits

Reviewing files that changed from the base of the PR and between 8005610 and 9728335.

📒 Files selected for processing (3)
  • src/contexts/AuthProvider.jsx
  • src/contexts/AuthProvider.test.jsx
  • src/services/authService.js
📝 Walkthrough

Walkthrough

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

Changes

Contact synchronization and transactional email

Layer / File(s) Summary
Email templates and delivery persistence
supabase/functions/_shared/transactionalEmails.ts, supabase/migrations/20260720190000_profiles_welcome_email_sent_at.sql
Adds shared welcome email templates and tracks one-time delivery on profiles.
Authenticated contact synchronization endpoint
supabase/functions/sync-contacts/*, supabase/functions/_shared/cors.ts
Validates JWT requests, derives profile plan data, synchronizes Resend contacts, and sends a guarded welcome email.
Sign-in synchronization wiring
src/contexts/AuthProvider.*, src/services/authService.js, .github/workflows/deploy-supabase-functions.yml, docs/AGENTS_REFERENCE.md
Invokes sync-contacts after sign-in, tests the payload, and deploys and documents the function.
Waitlist email delivery
supabase/functions/waitlist-welcome/index.ts
Uses shared HTML/text content and sends a Telegram notification for new waitlist entries.

Analytics and feature flags

Layer / File(s) Summary
Growth event instrumentation
src/services/analyticsEvents.js, src/pages/ProComingSoonPage.jsx, src/pages/VisualizerApp.jsx, src/services/analyticsEvents.growth.test.js
Adds and emits waitlist-joined and visualization-limit events with test coverage.
PostHog configuration and feature flags
src/services/analytics.js, src/services/featureFlags.js, src/test/setup.js, AGENTS.md
Configures SPA pageviews and recording sampling, retrieves general flag values, and adds a PostHog test mock.

Turnstile and request security

Layer / File(s) Summary
Turnstile signup verification
index.html, src/services/authService.js, supabase/functions/before-signup/index.ts
Carries invisible Turnstile tokens through Google sign-in and rejects signup requests that fail verification.
Webhook and platform access alerts
supabase/functions/_shared/webhookVerify.ts, supabase/functions/platform-access/index.ts, supabase/functions/post-signup/index.ts
Hardens shared-secret validation and adds Telegram alerts for signup, rejection, and ban events.

Account deletion confirmation

Layer / File(s) Summary
Email-based deletion confirmation
src/pages/ProfileSettingsPage.*, src/i18n/locales/*/translation.json
Requires the current user email in deletion guards, controls, tests, and localized copy.

Platform and scope maintenance

Layer / File(s) Summary
Workflow execution and secret handling
.github/workflows/*
Tightens deployment-origin checks, moves workflow inputs and secrets into environment variables, and adds explicit permissions.
Premature SaaS scaffolding removal
supabase/migrations/*, AGENTS.md, docs/AGENTS_REFERENCE.md
Removes obsolete database objects and updates product-scope documentation.
Test import maintenance
src/security/cspHeaders.test.js
Removes an unused Vitest import.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: growth analytics events, Resend contact sync, and waitlist-related updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/saas-analytics-infrastructure

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/services/authService.js 94.82% 3 Missing ⚠️
src/contexts/AuthProvider.jsx 90.90% 1 Missing ⚠️
src/services/featureFlags.js 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown

Preview for Bayan Flow Staging ready!

Name Link
🔨 Latest commit 9728335
🔍 Latest deploy log https://github.com/ayoub3bidi/bayan-flow/actions/runs/29901191173
😎 Deploy Preview https://pr-205-bayan-flow-staging.ayoub3bidi.workers.dev
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

Preview alias pr-205 on the staging worker. Updates automatically with new commits.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (1)
supabase/functions/sync-contacts/index.ts (1)

20-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unused function.

The getServiceClient function 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 createClient import 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2d6e8d3 and 3f5c127.

📒 Files selected for processing (16)
  • public/_headers
  • scripts/cspHeaders.js
  • src/contexts/AuthProvider.jsx
  • src/pages/ProComingSoonPage.jsx
  • src/pages/VisualizerApp.jsx
  • src/security/cspHeaders.test.js
  • src/services/analytics.js
  • src/services/analyticsEvents.js
  • src/services/entitlementService.js
  • src/test/setup.js
  • supabase/functions/lemonqueezy-webhook/index.ts
  • supabase/functions/sync-contacts/index.ts
  • supabase/migrations/20260718120000_user_sessions_analytics.sql
  • supabase/migrations/20260718130000_billing_subscriptions.sql
  • supabase/migrations/20260718140000_referral_system.sql
  • vite.config.js

Comment thread src/contexts/AuthProvider.jsx Outdated
Comment thread src/contexts/AuthProvider.jsx Outdated
Comment on lines +219 to +223
syncContactToResend(nextSession.user, {
displayName:
nextSession.user.user_metadata?.full_name ||
nextSession.user.user_metadata?.name,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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

Comment thread src/services/entitlementService.js Outdated
Comment on lines +34 to +37
// Exported constant for backward compatibility (reads once at module load)
export const ANONYMOUS_VISUALIZATION_LIMIT =
DEFAULT_ANONYMOUS_VISUALIZATION_LIMIT;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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: export getAnonymousVisualizationLimit instead of (or alongside) the static constant.
  • src/pages/VisualizerApp.jsx#L419-L425: call the exported getAnonymousVisualizationLimit() function to populate trackUpgradeLimitHit and openGatedFeature so 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.

Comment on lines +78 to +80
const userId = attrs?.custom_data?.user_id || attrs?.user_id || null;
const email = attrs?.customer_email || attrs?.user_email || null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 -S

Repository: 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 || true

Repository: 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:


🌐 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:


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.

Comment on lines +109 to +121
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +6 to +9
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')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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:


🏁 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.ts

Repository: 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])
PY

Repository: 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-L122
  • supabase/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.

Comment on lines +18 to +24
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()
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.ts

Repository: 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])
PY

Repository: 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("----")
PY

Repository: 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:


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.sql
  • supabase/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.

Comment on lines +58 to +72
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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

Comment on lines +24 to +29
create or replace function public.generate_referral_code(p_user_id uuid)
returns text
language plpgsql
security definer
as $$
declare

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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: Add set search_path = public to 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: Add set search_path = public to 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: Add set search_path = public to the function definition, and remove the completely unused p_user_id parameter (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-L67
  • supabase/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.

Comment thread supabase/migrations/20260718140000_referral_system.sql Outdated
@github-actions github-actions Bot added documentation Improvements or additions to documentation ci Workflows labels Jul 20, 2026
@ayoub3bidi ayoub3bidi changed the title feat: SaaS analytics infrastructure — PostHog, Supabase, LemonSqueezy, Resend feat: 0.5.0 growth analytics — PostHog events, Resend sync, waitlist Jul 20, 2026
@ayoub3bidi
ayoub3bidi force-pushed the feature/saas-analytics-infrastructure branch from 22e321c to 905c9dc Compare July 20, 2026 18:03
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.
@ayoub3bidi
ayoub3bidi force-pushed the feature/saas-analytics-infrastructure branch from 58b90e8 to 16cfcdc Compare July 21, 2026 09:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
src/pages/ProfileSettingsPage.jsx (2)

541-544: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Match 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 win

Make 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.email in 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 value

Remove unused translation keys.

Since ProfileSettingsPage.jsx now dynamically uses user.email for the input placeholder and the confirmation match logic, the newly added deleteAccountConfirmPlaceholder and the old deleteAccountConfirmWord keys 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 value

Await the reject function.

Since you'll need to make reject an async function to ensure sendTelegramAlert completes (as raised in the consolidated comments), you should also optionally await the reject() call here (and everywhere else it's called) for clarity, even though returning a Promise inside an async handler 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 win

Add 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 null ensures 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

📥 Commits

Reviewing files that changed from the base of the PR and between 22e321c and 16cfcdc.

📒 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.yml
  • AGENTS.md
  • docs/AGENTS_REFERENCE.md
  • index.html
  • src/contexts/AuthProvider.jsx
  • src/contexts/AuthProvider.test.jsx
  • src/i18n/locales/ar/translation.json
  • src/i18n/locales/en/translation.json
  • src/i18n/locales/fr/translation.json
  • src/pages/ProComingSoonPage.jsx
  • src/pages/ProfileSettingsPage.jsx
  • src/pages/ProfileSettingsPage.test.jsx
  • src/pages/VisualizerApp.jsx
  • src/security/cspHeaders.test.js
  • src/services/analytics.js
  • src/services/analyticsEvents.growth.test.js
  • src/services/analyticsEvents.js
  • src/services/authService.js
  • src/services/featureFlags.js
  • src/test/setup.js
  • supabase/functions/_shared/cors.ts
  • supabase/functions/_shared/transactionalEmails.ts
  • supabase/functions/_shared/webhookVerify.ts
  • supabase/functions/before-signup/index.ts
  • supabase/functions/platform-access/index.ts
  • supabase/functions/post-signup/index.ts
  • supabase/functions/sync-contacts/index.ts
  • supabase/functions/waitlist-welcome/index.ts
  • supabase/migrations/20260720180000_drop_premature_saas_scaffolding.sql
  • supabase/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

Comment thread src/contexts/AuthProvider.jsx Outdated
Comment thread supabase/functions/before-signup/index.ts
Comment thread supabase/functions/platform-access/index.ts Outdated
Comment thread supabase/functions/sync-contacts/index.ts
@ayoub3bidi ayoub3bidi self-assigned this Jul 22, 2026
…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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Do not report ban synchronization as successful when it failed.

When banError is present, the alert still says auth ban synced, which can mislead incident response while the Auth user remains unbanned. Use a failure-specific message or make the status text conditional on banError.

🤖 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 win

src/services/authService.js:47-65 — Dispose the Turnstile widget on every terminal path. Capture the widget ID from turnstile.render() and call turnstile.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 lift

Align the Turnstile payload with the actual signInWithIdToken contract. In src/services/authService.js:141-145, signInWithIdToken only forwards options.captchaToken; options.data is ignored here, so before-signup never receives raw_user_meta_data.cf_turnstile_response and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 16cfcdc and 8005610.

📒 Files selected for processing (9)
  • src/contexts/AuthProvider.jsx
  • src/i18n/locales/ar/translation.json
  • src/i18n/locales/en/translation.json
  • src/i18n/locales/fr/translation.json
  • src/pages/ProfileSettingsPage.jsx
  • src/services/authService.js
  • supabase/functions/before-signup/index.ts
  • supabase/functions/platform-access/index.ts
  • supabase/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

Comment thread src/contexts/AuthProvider.jsx Outdated
Comment on lines +200 to +205
authService
.syncContactToResend({
plan: null,
displayName: fallbackDisplayName,
})
.catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.jsx

Repository: 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" src

Repository: ayoub3bidi/bayan-flow

Length of output: 19426


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

sed -n '220,260p' src/services/authService.js

Repository: 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:


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

Comment thread src/services/authService.js
- 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
@ayoub3bidi
ayoub3bidi merged commit 9e35997 into develop Jul 22, 2026
16 of 18 checks passed
@ayoub3bidi
ayoub3bidi deleted the feature/saas-analytics-infrastructure branch July 22, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Workflows config documentation Improvements or additions to documentation tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant