beta access flow - #12
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 16 minutes and 50 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (15)
WalkthroughAdds beta-access gating across frontend routes, new early-access UI components, configurable auth forms, backend waitlist/admin invite workflows, email invite plumbing, and shared beta-access utilities (email normalization, admin allowlist, access checks, and enforcement helpers). Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Client as Client App
participant Server as Backend
participant DB as Database
User->>Client: Submit early access form
Client->>Client: Normalize email (trim + normalizeEmail)
Client->>Server: addToWaitlist({ email, name? })
Server->>DB: Query waitlist by normalized email (by_email)
alt already exists
DB-->>Server: existing record (position, status)
Server-->>Client: alreadyExists + position
else new entry
DB->>DB: Insert waitlist record (status: pending)
Server-->>Client: Success with new position
end
Client->>Client: Update UI (success/error, reset inputs)
sequenceDiagram
participant Client as Client App
participant Server as Backend
participant DB as Database
participant Email as Email Service
participant Admin as Admin User
Admin->>Client: Trigger inviteNextPendingUsers(count)
Client->>Server: inviteNextPendingUsers(count, auth)
Server->>Server: requireAdminUser(ctx)
Server->>DB: Query next pending waitlist entries (limit)
DB-->>Server: pending records
loop per pending user
Server->>DB: Patch waitlist entry (inviteCode, invitedAt, status=invited)
Server->>Email: sendInviteEmail(waitlistId, inviteCode, email)
Email-->>Server: send success
Server->>DB: markEmailSent(waitlistId)
end
Server-->>Client: invited emails + counts
sequenceDiagram
participant Client as Client App
participant Server as Backend
participant DB as Database
Client->>Server: getCurrentUserAccess()
Server->>Server: safeGetAuthUser()
alt unauthenticated
Server-->>Client: unauthenticated access payload
else authenticated
Server->>DB: getWaitlistRecordByEmail(normalizeEmail(authUser.email))
DB-->>Server: waitlist record or null
Server->>Server: compute isAdmin / hasBetaAccess
Server-->>Client: access payload (hasBetaAccess, waitlist metadata)
end
alt hasBetaAccess
Client->>Client: Render protected content
else no access
Client->>Client: Render EarlyAccessRequired -> shows apply card or sign-in
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/backend/convex/events.ts (2)
89-138: 🧹 Nitpick | 🔵 TrivialExtract the shared event-enrichment logic.
The
eventTopicsmap build +articles/sourcesenrichment ingetPublishedEvents(lines 89–132) is now duplicated almost verbatim ingetPublicPublishedEventsPreview(lines 153–187). Pulling it into a helper likeenrichEventsWithTopicsAndSources(ctx, events)keeps the two queries from drifting and makes any future change (e.g. field projection per the comment above) a one-liner.Also applies to: 153-187
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/convex/events.ts` around lines 89 - 138, Extract the duplicated enrichment logic in getPublishedEvents and getPublicPublishedEventsPreview into a shared helper enrichEventsWithTopicsAndSources(ctx, events): move the eventTopics preloading (building topicsByEventId from eventIds and allEventTopicRows) and per-event article/source/topic enrichment (loading articles, computing articleCount, deduplicating sourceIds and loading sources, mapping topicIds from topicsByEventId, filtering null sources) into that helper; update both functions to call enrichEventsWithTopicsAndSources(ctx, events) and return the same shape ({ ...events, page: enrichedPage }) so future changes (e.g. projection) are centralized.
191-200:⚠️ Potential issue | 🟠 MajorBeta-gating
getEventBySlugregresses SSR meta tags for every event detail page.
apps/web/src/routes/event.$slug.tsxcalls this query throughserverHttpClientfrom the routeloaderto populate dynamic OG/Twitter/canonical meta inhead(). WithrequireBetaAccess(ctx)enforced here, SSR contexts without authentication (search crawlers, link unfurlers) will throwConvexError. The loader'stry/catchsilently catches it and returnsnull, causing the page to ship genericEvent — Biviantmeta to all anonymous clients.Additionally,
getPublicPublishedEventsPreviewspreads...event, exposing every column from the events table to anonymous callers — including fields that may not be intended for public consumption.Fix options:
- Create a public SEO-safe slice (e.g.,
getEventBySlugPreviewreturning{ title, perspectiveSummaries.center, imageUrl, imageAlt }) for the SSR loader, keeping the gatedgetEventBySlugfor authenticated detail UI.- If beta is intentionally private, emit
noindexfrom the loader and accept generic meta.- Audit and limit fields returned from
getPublicPublishedEventsPreviewto prevent unintended data exposure.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/convex/events.ts` around lines 191 - 200, The beta check in getEventBySlug causes SSR callers to get blocked; implement a public, SEO-safe slice (e.g., getEventBySlugPreview) that does NOT call requireBetaAccess and returns only allowed fields (title, perspectiveSummaries.center, imageUrl, imageAlt, canonical slug, and any minimal SEO fields) for use by the route loader/serverHttpClient; keep the existing getEventBySlug with requireBetaAccess for the authenticated detail UI. Also audit and restrict getPublicPublishedEventsPreview so it no longer spreads ...event but explicitly selects only safe public columns to avoid leaking sensitive fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/src/components/early-access-required.tsx`:
- Line 42: The unauthenticated copy for the early access message is awkwardly
phrased; update the string that uses surfaceName (the message template
referencing surfaceName in the EarlyAccessRequired component) to replace "from
your beta email" with "from your invite email" so it reads: "<surfaceName> is
currently available to invited beta users. Sign in with the invited email from
your invite email, or apply for access below." — adjust only the phrasing around
surfaceName in that message template.
In `@apps/web/src/components/sign-up-form.tsx`:
- Around line 184-229: Extract the duplicated Google sign-in divider +
button+SVG into two reusable components: AuthDivider (renders the horizontal
divider with "Or continue with") and GoogleSignInButton which accepts a
callbackURL prop and calls authClient.signIn.social({ provider: "google",
callbackURL }) on click and contains the inline SVG and button styling; replace
the duplicated blocks in both SignUpForm and SignInForm with <AuthDivider /> and
<GoogleSignInButton callbackURL={redirectTo} /> respectively so the SVG and
click logic live in one place.
In `@apps/web/src/routes/dashboard.tsx`:
- Around line 638-689: The UI uses a single isInvitingWaitlist flag so every row
and batch button shows "Sending..." and is disabled simultaneously; change to a
per-row in-flight id: add state like invitingWaitlistId (string | null) and set
it in handleInviteSingleUser before calling the mutation and clear it in finally
(or use the mutation's onMutate/onSettled hooks), then update the per-row button
disabled/label logic to disabled={isInvitingWaitlist && invitingWaitlistId !==
entry._id ? true : (invitingWaitlistId === entry._id)} and label to
(invitingWaitlistId === entry._id ? "Sending..." : "Send invite"). Keep existing
isInvitingWaitlist (or a separate isBatchInviting) for the batch handlers
handleInviteNextPendingUsers so batch buttons still disable all rows during
batch operations.
- Around line 287-295: Replace direct calls to convex's useMutation for
inviteWaitlistUser and inviteNextPendingUsers by first wrapping the Convex
mutation with useConvexMutation (e.g., const convexInviteWaitlistUser =
useConvexMutation(api.waitlist.inviteWaitlistUser)) and then pass that into
TanStack's useMutation (e.g., useMutation({ mutationFn: convexInviteWaitlistUser
})). Remove the manual isInvitingWaitlist state and any manual pending/error
handling around inviteNextPendingUsers/inviteWaitlistUser, and update UI/buttons
to use the TanStack mutation state flags (inviteNextPendingUsers.isPending /
inviteWaitlistUser.isPending, isError, error, reset) instead. Ensure you update
references to the original functions (inviteWaitlistUser,
inviteNextPendingUsers) to the new TanStack-wrapped mutation objects so existing
call sites use mutation.mutate(...) and state flags rather than custom useState
flags.
In `@apps/web/src/routes/feed.tsx`:
- Around line 224-234: The loading placeholder that renders when the variable
access is undefined should be an accessible ARIA live region so screen readers
announce the status; update the inner placeholder element (the div with class
"rounded-[1.2rem] border border-border/70 bg-card/70 px-5 py-8 text-sm
text-muted-foreground" in apps/web/src/routes/feed.tsx) to include role="status"
and aria-live="polite" so the "Loading…" message is announced.
In `@packages/backend/convex/emails.ts`:
- Around line 128-129: The current fallback uses process.env.SITE_URL ??
DEFAULT_SITE_URL which does not handle empty-string env values; change the
SITE_URL resolution to treat empty or whitespace-only values as missing (e.g.,
use process.env.SITE_URL?.trim() ? process.env.SITE_URL.trim() :
DEFAULT_SITE_URL or the || pattern) when setting siteUrl and constructing
inviteUrl (variable inviteUrl) to ensure a valid base URL; also stop reading
process.env inside the email template by passing the resolved siteUrl into
getInviteEmailHTML (or whichever function builds the template) so both the
invite link and footer href use the same validated siteUrl.
In `@packages/backend/convex/lib/betaAccess.ts`:
- Around line 13-22: getAdminEmails currently reparses process.env.ADMIN_EMAILS
on every call; introduce a module-level lazy cache (e.g., let cachedAdminEmails:
Set<string> | null = null) and populate it the first time getAdminEmails or
isAdminEmail is called by splitting, normalizing (via normalizeEmail), trimming
and storing in a Set<string>, then have getAdminEmails return
Array.from(cachedAdminEmails) or isAdminEmail consult
cachedAdminEmails.has(normalizeEmail(email)); ensure the cache is only populated
once and handles an empty env string safely.
- Around line 91-102: Replace the duplicated admin-check implementations by
using the centralized functions in betaAccess.ts: import and call
requireAdminUser (and/or getAdminEmails if needed) instead of the private
requireAdmin() in config.ts and the private requireAdminUser() in clustering.ts;
remove the inline ADMIN_EMAILS parsing blocks from config.ts and clustering.ts,
update all callers (the listed call sites) to use the imported requireAdminUser,
and in clustering.ts ensure any thrown error uses ConvexError rather than Error
to match Convex conventions.
In `@packages/backend/convex/waitlist.ts`:
- Around line 281-287: The current query in inviteNextPendingUsers loads all
pending rows via ctx.db.query("waitlist").withIndex("by_status", (q) =>
q.eq("status", "pending")).order("asc").collect() and then slices to count,
which wastes reads and can hit limits; change it to use .take(count) directly on
that query to return only the first count rows (i.e. replace .collect() +
slice(0, count) with .take(count)) so the function retrieves just the needed
documents without loading the entire pending pool.
- Around line 187-216: getWaitlistAdminOverview currently calls
ctx.db.query("waitlist").collect() and then filters/sorts in JS; change it to
use index-bounded queries: fetch nextPending with
ctx.db.query("waitlist").by_status("pending").take(safeLimit) (or equivalent
index API) and fetch recentInvites with a new index by_status_invitedAt and
.by_status_invitedAt("invited").take(safeLimit) so you never load the whole
table into memory; for stats, stop scanning the full table—introduce a small
waitlistStats counters table (waitlistStats) updated by addToWaitlist,
inviteWaitlistUser, inviteNextPendingUsers, and unsubscribe to return O(1)
counts in getWaitlistAdminOverview (or as an interim improvement, perform three
targeted index-limited queries per status instead of collect()).
---
Outside diff comments:
In `@packages/backend/convex/events.ts`:
- Around line 89-138: Extract the duplicated enrichment logic in
getPublishedEvents and getPublicPublishedEventsPreview into a shared helper
enrichEventsWithTopicsAndSources(ctx, events): move the eventTopics preloading
(building topicsByEventId from eventIds and allEventTopicRows) and per-event
article/source/topic enrichment (loading articles, computing articleCount,
deduplicating sourceIds and loading sources, mapping topicIds from
topicsByEventId, filtering null sources) into that helper; update both functions
to call enrichEventsWithTopicsAndSources(ctx, events) and return the same shape
({ ...events, page: enrichedPage }) so future changes (e.g. projection) are
centralized.
- Around line 191-200: The beta check in getEventBySlug causes SSR callers to
get blocked; implement a public, SEO-safe slice (e.g., getEventBySlugPreview)
that does NOT call requireBetaAccess and returns only allowed fields (title,
perspectiveSummaries.center, imageUrl, imageAlt, canonical slug, and any minimal
SEO fields) for use by the route loader/serverHttpClient; keep the existing
getEventBySlug with requireBetaAccess for the authenticated detail UI. Also
audit and restrict getPublicPublishedEventsPreview so it no longer spreads
...event but explicitly selects only safe public columns to avoid leaking
sensitive fields.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: e916df8b-616f-407b-a774-b7b9277dd83f
⛔ Files ignored due to path filters (1)
packages/backend/convex/_generated/api.d.tsis excluded by!**/_generated/**,!**/_generated/**
📒 Files selected for processing (14)
apps/web/src/components/early-access-apply-card.tsxapps/web/src/components/early-access-required.tsxapps/web/src/components/sign-in-form.tsxapps/web/src/components/sign-up-form.tsxapps/web/src/routes/dashboard.tsxapps/web/src/routes/event.$slug.tsxapps/web/src/routes/feed.tsxapps/web/src/routes/index.tsxpackages/backend/convex/auth.tspackages/backend/convex/emails.tspackages/backend/convex/events.tspackages/backend/convex/lib/betaAccess.tspackages/backend/convex/user.tspackages/backend/convex/waitlist.ts
There was a problem hiding this comment.
Actionable comments posted: 15
♻️ Duplicate comments (1)
packages/backend/convex/waitlist.ts (1)
122-167: 🧹 Nitpick | 🔵 TrivialStats still scan every waitlist row across all five statuses on every admin render.
getWaitlistStatsand thestatsblock ingetWaitlistAdminOverviewboth use.withIndex("by_status", ...).collect()for each of the 5 statuses just to read.length. That's index-bounded but still O(N) per status — i.e., the entire table is read on every invocation, and because these are reactiveuseQuerys, they re-run on every waitlist mutation.For an interim improvement, replace
.collect()with a Convex pagination/count primitive that doesn't materialize rows (or, longer-term, maintain awaitlistStatscounters doc updated byaddToWaitlist/inviteWaitlistUser/inviteNextPendingUsers/unsubscribeso this becomes O(1)). ThenextPending/recentInvites.take(safeLimit)changes on Lines 244–253 already follow this pattern correctly.As per coding guidelines for
packages/backend/**: "Focus on Convex best practices" and "Review database schema changes carefully".Also applies to: 215-254
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/convex/waitlist.ts` around lines 122 - 167, getWaitlistStats is materializing all rows by calling .collect() for each status; replace those five .collect() calls with a non-materializing count/pagination primitive (e.g., use the Convex query count API) so you get pending/invited/converted/bounced/unsubscribed counts without fetching rows. Update variable names (e.g., pendingCount, invitedCount) and the total computation to sum the counts, and apply the same change to the analogous stats block in getWaitlistAdminOverview (the stats section referenced in the comment) so both use the count primitive instead of .collect(); keep the safeLimit/.take(safeLimit) pattern used for nextPending/recentInvites as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/src/components/auth-social.tsx`:
- Around line 36-53: The inline SVG used for the Google logo in the auth-social
component is decorative and should be removed from the accessibility tree;
update the <svg className="mr-2 h-4 w-4"> element in the auth-social component
to include aria-hidden="true" and focusable="false" (so screen readers ignore
it) while keeping the visible styling and the existing button label "Continue
with Google".
- Around line 29-34: authClient.signIn.social(...) is returning a Promise that
is not handled; update the click handler for the social sign-in (the onClick
that calls authClient.signIn.social with provider "google" and callbackURL) to
handle the Promise outcome — either await it in an async handler or attach
.then/.catch, and surface failures using the same onError/toast pattern used in
sign-in-form.tsx (show a user-friendly toast on error and optionally log the
error), so network/OAuth failures no longer silently fail.
In `@apps/web/src/components/early-access-required.tsx`:
- Around line 38-42: The copy in the description string is redundant; update the
unauthenticated branch of the description constant (used with
access.authenticated, access.email, surfaceName) to replace "Sign in with the
invited email from your invite email" with a tightened phrase such as "Sign in
with the email address from your invite, or apply for access below." Keep the
surrounding sentence structure and interpolation unchanged so only the phrase is
swapped.
- Around line 7-12: AccessState.waitlistStatus is too permissive as string|null;
tighten it to the backend-known literal union (e.g. "pending" | "unsubscribed" |
"bounced" | null) or, better, derive the type from the Convex query result (e.g.
use FunctionReturnType<typeof api.user.getCurrentUserAccess> or the return type
of getCurrentUserBetaAccess) so the component's AccessState (referenced in
early-access-required.tsx) stays in sync with the backend; update the
AccessState type definition accordingly and import/alias the derived type where
AccessState is used.
In `@apps/web/src/components/sign-in-form.tsx`:
- Around line 32-34: The redirectTo prop is currently typed as string which
breaks TanStack Router's type-safe navigate({ to }) usage in
useNavigate/navigate; update the prop type for redirectTo in SignInForm (and
similarly in SignUpForm) to the union of valid routes (e.g., '/' | '/bookmarks'
| '/dashboard' | '/feed' | '/unsubscribe' | '/event/$slug' | '/api/auth/$') so
it matches the navigate({ to }) expected type, or add a runtime validation step
that ensures redirectTo is one of those route values and then cast (e.g.,
validatedPath as never) before calling navigate({ to: validatedPath }); ensure
references to useNavigate, navigate, and the redirectTo prop name are updated
accordingly.
In `@apps/web/src/components/sign-up-form.tsx`:
- Around line 54-59: Remove the manual onError type annotation in the
sign-up-form component and replace the current raw-error-to-UI behavior with the
same pattern used in sign-in-form: let TypeScript infer the better-auth callback
signature by omitting the explicit type on the onError handler, call
console.error(error) to log the raw error for diagnostics, and set/show a
generic user-facing message (e.g., "An unexpected error occurred. Please try
again.") instead of rendering error.error.message or error.error.statusText
directly in the UI.
In `@apps/web/src/routes/dashboard.tsx`:
- Around line 60-66: The effect that sets showSignIn based on invitePreview
currently runs on every invitePreview change and can override a user toggle;
modify the effect (the useEffect that reads invitePreview and calls
setShowSignIn) to stop re-applying the invite-driven default after the user has
manually toggled sign-in: add a userToggled ref (e.g., userToggledRef) that is
set to true inside the user action handler (onSwitchToSignIn / any handler that
calls setShowSignIn), and in the effect early-return if userToggledRef.current
is true; alternatively only apply the invitePreview logic once on mount by
checking a initialized ref so invitePreview no longer snaps the UI back after
user interaction.
- Around line 44-47: The redirect validation currently accepts any value where
search.redirect.startsWith("/"), which allows protocol-relative URLs like
"//evil.com"; update the check for redirectTo (and any use of search.redirect)
to explicitly reject values that begin with "//" by requiring a single leading
slash (e.g., ensure search.redirect is truthy and startsWith("/") AND does not
startWith("//") or use a regex like /^\/(?!\/)/) so only path-style redirects
are allowed.
In `@packages/backend/convex/emails.ts`:
- Around line 458-462: Update the stale HTML comment "<!-- Expiry note -->" in
the email template: either remove it or replace it with a comment that
accurately describes the current content (e.g. "Use same email reminder") so the
block containing the td with text "Use the same email address when you create
your account." is self-documenting; locate the comment immediately above that td
in the emails.ts template and change or delete it accordingly.
In `@packages/backend/convex/events.ts`:
- Around line 35-78: The current hot-feed path issues N×M reads because inside
the events.map loop you call ctx.db.get(sourceId) per-event per-source; instead,
collect all unique sourceIds across events (derive from articles for all
events), perform a single batch fetch via Promise.all(uniqueSourceIds.map((id)
=> ctx.db.get(id))) to build a lookup map, then in the events mapping (the async
events.map block) replace per-event Promise.all source fetches with a simple
lookup of sources by ID (filtering nulls) to attach sources and keep topic
lookup via topicsByEventId unchanged; update symbols: eventIds,
allEventTopicRows, topicsByEventId, articles, sourceIds, ctx.db.get, and the
final returned event object to read sources from the shared lookup.
- Around line 160-174: getPublicPublishedEventsPreview currently returns full
perspectiveSummaries (left/right/center) and globalImpact via
enrichEventsWithTopicsAndSources, while getEventBySlugPreview redacts to
perspectiveSummaries.center; make them consistent by redacting
getPublicPublishedEventsPreview to the same public shape: after calling
enrichEventsWithTopicsAndSources in getPublicPublishedEventsPreview, map each
event to replace perspectiveSummaries with an object containing only center
(perspectiveSummaries.center) and remove or null-out globalImpact (and any other
non-public fields), or reuse the same redaction helper used by
getEventBySlugPreview if one exists, ensuring both unauthenticated preview
endpoints expose the identical limited fields.
In `@packages/backend/convex/lib/betaAccess.ts`:
- Around line 27-33: isAdminEmail currently uses an unnecessary optional chain
and nullish fallback because getAdminEmails() always initializes
cachedAdminEmails; remove the dead-code by either asserting non-null after
population or adding a small accessor. Fix by updating isAdminEmail to call
getAdminEmails() as before and then use a non-null assertion:
cachedAdminEmails!.has(normalizeEmail(email)), or implement a private
getAdminEmailSet() that calls getAdminEmails(), returns a Set<string>, and use
getAdminEmailSet().has(normalizeEmail(email)); reference isAdminEmail,
getAdminEmails, cachedAdminEmails, and normalizeEmail when making the change.
- Around line 35-43: In getWaitlistRecordByEmail, replace the call to .unique()
on ctx.db.query("waitlist").withIndex("by_email", ...) with .first() so the
query returns the first matching document instead of throwing on duplicate
normalized emails; update the method to return the .first() result and ensure
callers (requireBetaAccess, getPublishedEvents, getEventBySlug, dashboard, feed)
continue to handle a nullable Doc<"waitlist"> as before, and plan a backfill to
deduplicate legacy mixed-case waitlist rows.
In `@packages/backend/convex/schema.ts`:
- Line 321: The schema index by_status_invitedAt is fine, but Convex lacks
native uniqueness for waitlist.email, so update the waitlist insert mutation to
enforce uniqueness at the application level: in the mutation (the function that
creates waitlist records — the counterpart to getWaitlistRecordByEmail in
betaAccess.ts) perform a transactional read-then-insert using Convex
transactions (read existing record by email via getWaitlistRecordByEmail inside
the same transaction and only insert if none exists), and add a one-time dedupe
migration to remove duplicate waitlist.email entries before deployment.
In `@packages/backend/convex/waitlist.ts`:
- Around line 179-205: getInvitePreview currently returns invitee PII
(email/name) for any caller; change it to avoid exposing PII to unauthenticated
callers by either enforcing authentication or redacting fields: in the
getInvitePreview handler, check the request context auth (e.g., ctx.auth /
ctx.user) and if unauthenticated return the same
isValid/status/position/timestamps but set email to null or a masked value and
name to null; alternatively, require an authenticated user and return a
401/unauthorized when ctx lacks credentials. Update references to the email/name
fields in callers (e.g., dashboard UI) to handle null/masked values accordingly.
---
Duplicate comments:
In `@packages/backend/convex/waitlist.ts`:
- Around line 122-167: getWaitlistStats is materializing all rows by calling
.collect() for each status; replace those five .collect() calls with a
non-materializing count/pagination primitive (e.g., use the Convex query count
API) so you get pending/invited/converted/bounced/unsubscribed counts without
fetching rows. Update variable names (e.g., pendingCount, invitedCount) and the
total computation to sum the counts, and apply the same change to the analogous
stats block in getWaitlistAdminOverview (the stats section referenced in the
comment) so both use the count primitive instead of .collect(); keep the
safeLimit/.take(safeLimit) pattern used for nextPending/recentInvites as-is.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b0f02885-3cc0-48d6-aea1-c3743b167c94
📒 Files selected for processing (14)
apps/web/src/components/auth-social.tsxapps/web/src/components/early-access-required.tsxapps/web/src/components/sign-in-form.tsxapps/web/src/components/sign-up-form.tsxapps/web/src/routes/dashboard.tsxapps/web/src/routes/event.$slug.tsxapps/web/src/routes/feed.tsxpackages/backend/convex/clustering.tspackages/backend/convex/config.tspackages/backend/convex/emails.tspackages/backend/convex/events.tspackages/backend/convex/lib/betaAccess.tspackages/backend/convex/schema.tspackages/backend/convex/waitlist.ts
| }) | ||
| .index("by_email", ["email"]) | ||
| .index("by_status", ["status", "createdAt"]) | ||
| .index("by_status_invitedAt", ["status", "invitedAt"]) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
LGTM — index supports the new admin invite flows.
by_status_invitedAt is well-shaped for range queries like "invited users after timestamp T" or "most recently invited" used by the new admin/email plumbing. Be aware the index will trigger a backfill on first deploy, which is fine at current waitlist scale.
A small operational note: if you later need to enforce uniqueness on waitlist.email (relevant to the .unique() lookup in betaAccess.ts's getWaitlistRecordByEmail), Convex doesn't provide native unique constraints — you'll need an application-level check inside the insert mutation (read-then-insert under transactional semantics) plus a one-time dedupe migration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/backend/convex/schema.ts` at line 321, The schema index
by_status_invitedAt is fine, but Convex lacks native uniqueness for
waitlist.email, so update the waitlist insert mutation to enforce uniqueness at
the application level: in the mutation (the function that creates waitlist
records — the counterpart to getWaitlistRecordByEmail in betaAccess.ts) perform
a transactional read-then-insert using Convex transactions (read existing record
by email via getWaitlistRecordByEmail inside the same transaction and only
insert if none exists), and add a one-time dedupe migration to remove duplicate
waitlist.email entries before deployment.
Summary by CodeRabbit
New Features
Improvements