Skip to content

Conversation

@jakejarvis
Copy link
Owner

@jakejarvis jakejarvis commented Dec 4, 2025

Summary by CodeRabbit

  • New Features
    • Full dashboard experience: add & verify domains (DNS/HTML/meta), grid/table views, selectable cards, bulk actions, archived domains, upgrade prompts, announcement pill, and improved login/dashboard flows.
  • Notifications & Emails
    • Automated domain/certificate/verification/subscription emails and alerts with per-user and per-domain preferences.
  • Documentation
    • Added Privacy Policy and Terms pages; updated README.
  • Tests
    • Expanded unit/integration tests across verification, notifications, repos, edge-config, and subscription handlers.
  • Chores
    • Added environment placeholders for auth, GitHub, Polar, and email integrations.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Dec 4, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
domainstack Ready Ready Preview Comment Dec 6, 2025 2:51pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 4, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Added server-side auth and billing (Better‑Auth + Polar), a full domain‑tracking system (DB schema, repos, TRPC router, verification service), scheduled Inngest jobs, Resend-backed email templates, many dashboard UI components/hooks, and accompanying tests and migrations.

Changes

Cohort / File(s) Change Summary
Environment & Config
\.env.example
Added placeholders for auth, GitHub, Polar, and Resend env vars.
Server auth & client auth
lib/auth.ts, lib/auth-client.ts, app/api/auth/[...all]/route.ts, trpc/init.ts
Introduced Better‑Auth server config (Drizzle adapter, GitHub provider, optional Polar plugins), exported auth and Session; extended client auth (authClient, checkout, customerPortal); Next.js auth route exposes GET/POST; added protectedProcedure and session-aware Context.
DB schema & migrations
lib/db/schema.ts, drizzle/..., drizzle/0004_mean_daredevil.sql, drizzle/meta/*
Added enums and tables for users, sessions, accounts, user_tracked_domains, subscriptions, notifications, providers, certificates, dns_records, etc.; included SQL migration and Drizzle meta snapshot/journal.
Repositories & DB helpers
lib/db/repos/*
New repo modules and helpers for tracked domains, notifications, certificates, user subscriptions, user notification preferences, users (CRUD/query functions, logging).
Verification service & tests
server/services/verification.ts, server/services/verification.test.ts
Added domain verification (DNS TXT, HTML file, meta tag), token generation, instruction builders, orchestration to try all methods, and unit tests.
TRPC router
server/routers/tracking.ts, server/routers/_app.ts
Added trackingRouter with add/verify/list/archive/remove, limits and notification preference endpoints; integrated into appRouter.
Inngest jobs & API route
lib/inngest/functions/*, app/api/inngest/route.ts
Added scheduled/background jobs: domain/certificate/subscription expiry checks, reverification, cleanup, auto‑verify pending; API route updated to serve them.
Email templates & Resend
lib/resend.ts, emails/*, lib/polar/emails.ts
Conditional Resend client init, RESEND_FROM_EMAIL; many React Email templates for expiry, verification, pro/subscription flows; Polar email utilities.
Polar billing & webhooks
lib/polar/*
Product definitions, checkout helpers, webhook handlers (created/active/canceled/revoked), handleDowngrade archival logic, email utilities and tests.
Notifications & helpers
lib/constants/notifications.ts, lib/notifications.ts, lib/notifications.test.ts
Notification categories, thresholds, idempotency key generator, helpers to pick notification types with tests.
Edge Config & tier limits
lib/constants/tier-limits.ts, lib/edge-config.ts, lib/edge-config.test.ts
Added DEFAULT_TIER_LIMITS and TierLimits; getTierLimits/getMaxDomainsForTier reading Edge Config with fallbacks and tests.
Schemas & types
lib/schemas/internal/*, lib/schemas/index.ts
Zod schemas/types for user tier, notifications, verification instructions; index re-exports added.
Dashboard pages & layout
app/dashboard/*, app/login/page.tsx, app/(static)/*, app/layout.tsx
Server‑side auth-guarded dashboard layout and pages, login redirect logic, static Terms/Privacy pages, Toaster added to root layout, loading placeholders.
Dashboard components & UI
components/dashboard/*
Many UI components: headers, badges, TrackedDomain cards, grid/table/list views, add‑domain multi‑step dialog (resume capable), banners, subscription UI, settings (global & per‑domain), filters, selection, bulk actions, archived view, skeletons, errors, and tests.
Auth & layout UI
components/auth/*, components/layout/*, components/brand-icons.tsx
New/updated auth UI: AuthButton, SignInButton, UserMenu, LoginDialog; controlled BookmarkletDialog; MobileMenu; ThemeToggle hooking into useTheme; GitHubIcon, AnnouncementPill, header/footer updates.
Client hooks & utilities
hooks/*
Added hooks: useViewPreference, useSortPreference, useDomainFilters, useSelection, useTheme (useTheme), useCustomerPortal, useUpgradeCheckout, useDomainVerification (resume capable), use-domain-mutations, and related utilities.
Dashboard orchestration
components/dashboard/dashboard-content.tsx, components/dashboard/dashboard-skeleton.tsx, components/dashboard/dashboard-error.tsx
DashboardContent orchestrates TRPC queries, optimistic mutations, filtering/sorting, selection/bulk actions, add‑domain dialog/resume flows; skeleton and error components added.
Domain UI API renames & context
components/domain/*, components/layout/home-search-context.tsx, tests
Prop rename: HomeSearchProvider onSuggestionClick (was onSuggestionClickAction); ExportButton prop onExport (was onExportAction); provider/context and tests updated.
Selection, multi-select & utilities UI
components/ui/multi-select.tsx, hooks/use-selection.ts
New MultiSelect component and useSelection hook for multi‑select state management.
Packaging, deps & docs
package.json, README.md, AGENTS.md
Added dependencies (Better‑Auth, Polar SDK, React Email, TanStack Table, Resend, nuqs, etc.), README/AGENTS updates, and numerous tests added across modules.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant Next as Next.js Server
    participant TRPC
    participant Auth
    participant DB as Postgres
    participant Verif as VerificationService
    participant Inngest
    participant Resend

    User->>Browser: Visit /dashboard or interact (add/verify domain)
    Browser->>Next: SSR request (include cookies/headers)
    Next->>Auth: auth.api.getSession(headers)
    Auth->>DB: validate session, load user/subscription
    alt authenticated
        Next-->>Browser: render dashboard (server)
    else
        Next-->>Browser: redirect /login
    end

    User->>Browser: Add domain (UI)
    Browser->>TRPC: tracking.addDomain(domain)
    TRPC->>DB: create domain/tracked_domain (limit checks, transactional)
    TRPC-->>Browser: return verification token & instructions

    User->>Browser: Trigger verify action
    Browser->>TRPC: tracking.verifyDomain(trackedId, method)
    TRPC->>Verif: verifyDomainOwnership(domain, token, method)
    Verif-->>TRPC: verification result
    TRPC->>DB: update tracked_domain (verified/status)
    TRPC-->>Browser: verification outcome

    par Scheduled background jobs (Inngest)
        Inngest->>Next: run job (reverify/check-expiry/cleanup)
        Next->>DB: query eligible domains/subscriptions/certificates
        Next->>Resend: render email template and send (idempotency)
        Resend-->>User: deliver email
        Next->>DB: record notification + resendId
    end
Loading

Poem

🐰 I hopped through code with whiskers bright,

Tokens, crons, and webhooks through the night.
Domains now watched with careful art,
Emails like carrots, delivered to the heart.
Migrations, tests, and dashboards — off I dart!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.63% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the three main features: user accounts, domain verification flow, and expiry notifications. It clearly highlights the primary changes without being vague.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e766d95 and 111fcf8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • components/dashboard/dashboard-content.tsx (1 hunks)
  • components/dashboard/tracked-domains-table.tsx (1 hunks)
  • package.json (5 hunks)

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 and usage tips.

@jakejarvis

This comment was marked as outdated.

@coderabbitai

This comment was marked as outdated.

@codecov
Copy link

codecov bot commented Dec 4, 2025

Codecov Report

❌ Patch coverage is 77.59104% with 80 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.85%. Comparing base (2f7b0d5) to head (111fcf8).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/db/repos/tracked-domains.ts 60.30% 41 Missing and 11 partials ⚠️
lib/db/repos/notifications.ts 71.05% 10 Missing and 1 partial ⚠️
lib/db/repos/certificates.ts 0.00% 7 Missing ⚠️
lib/db/schema.ts 87.09% 4 Missing ⚠️
components/layout/bookmarklet-dialog.tsx 66.66% 1 Missing and 1 partial ⚠️
lib/edge-config.ts 85.71% 1 Missing and 1 partial ⚠️
components/brand-icons.tsx 50.00% 1 Missing ⚠️
components/domain/domain-suggestions-client.tsx 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #212      +/-   ##
==========================================
+ Coverage   71.66%   72.85%   +1.19%     
==========================================
  Files         111      128      +17     
  Lines        3014     3463     +449     
  Branches      891      972      +81     
==========================================
+ Hits         2160     2523     +363     
- Misses        508      578      +70     
- Partials      346      362      +16     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (24)
package.json (1)

77-77: Consider moving prettier to devDependencies.

Prettier is a code formatter typically used during development and CI. Having it in production dependencies unnecessarily increases the deployment bundle size.

Apply this diff to move prettier to devDependencies:

     "posthog-js": "^1.300.0",
     "posthog-node": "^5.17.0",
-    "prettier": "^3.7.4",
     "puppeteer-core": "24.26.1",

Then add it to devDependencies (around line 119):

     "puppeteer": "24.26.1",
+    "prettier": "^3.7.4",
     "react-email": "5.0.5",
components/auth/user-menu.tsx (1)

33-41: Consider logging sign-out errors.

While sign-out failures are less critical than sign-in failures, logging errors would improve observability when debugging session issues.

+"use client";
+
 import { LogOut, Settings, User } from "lucide-react";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 import {
   DropdownMenu,
   DropdownMenuContent,
   DropdownMenuItem,
   DropdownMenuLabel,
   DropdownMenuSeparator,
   DropdownMenuTrigger,
 } from "@/components/ui/dropdown-menu";
 import { signOut, useSession } from "@/lib/auth-client";
+import { logger } from "@/lib/logger/client";

 export function UserMenu() {
   const { data: session } = useSession();
   const router = useRouter();

   // ... rest of component

   const handleSignOut = async () => {
-    await signOut({
+    try {
+      await signOut({
+        fetchOptions: {
+          onSuccess: () => {
+            router.push("/");
+          },
+        },
+      });
+    } catch (error) {
+      logger.error("Sign-out failed", { error });
-      fetchOptions: {
-        onSuccess: () => {
-          router.push("/");
-        },
-      },
-    });
+    }
   };

Based on learnings, client-side logging should use the singleton logger.

lib/resend.ts (1)

7-11: Use structured server-side logging.

Console methods should be replaced with the structured logger for consistency and better observability in production.

As per coding guidelines, use the server logger:

 import "server-only";

 import { Resend } from "resend";
+import { logger } from "@/lib/logger/server";

 const apiKey = process.env.RESEND_API_KEY;

 if (!apiKey) {
-  console.warn(
-    "RESEND_API_KEY is not set. Email notifications will not be sent.",
-  );
+  logger.warn("RESEND_API_KEY is not set. Email notifications will not be sent.");
 }

Based on learnings, server-side logging should use the server logger singleton.

components/dashboard/tracked-domains-grid.tsx (1)

7-19: Clarify or remove unused onAddDomain prop

onAddDomain is accepted but not used (_onAddDomain). If there’s no near‑term plan to render an “add domain” affordance in this grid, consider dropping the prop from TrackedDomainsGridProps to keep the surface minimal; otherwise, wiring it into an empty‑state tile or header action would make its purpose clearer.

components/dashboard/verification-badge.test.tsx (1)

6-84: Solid coverage; consider aligning test names with assertions

The suite exercises all key branches (verified, pending, failing, status overrides, and className passthrough) using Vitest + RTL as expected. A small nit: tests like “includes check icon / warning icon” actually assert on class names rather than icon presence, so renaming those tests (or asserting on the SVG/icon element) would better reflect what’s being verified.

Based on learnings, this matches the Vitest + RTL guidance.

drizzle/0004_remarkable_zzzax.sql (1)

1-98: Schema aligns with auth/tracking needs; confirm password storage strategy

The new enums, FK relationships, uniqueness constraints (e.g., u_tracked_domain_user, u_notification_unique), and indexes look consistent with the intended user/accounts/sessions + tracked_domains model. One thing to double‑check: accounts.password is a plain text column—please ensure that, if it’s used, only strong, salted password hashes (never plaintext or reversible values) are stored there, or consider removing the column entirely if all auth is OAuth‑only.

components/dashboard/add-domain-dialog.tsx (3)

1-24: Consider adding client-side logging for error tracking.

Per coding guidelines, client components should import the logger for error tracking: import { logger } from "@/lib/logger/client" or use useLogger({ component: "AddDomainDialog" }). This would help track verification failures and mutation errors in PostHog.


167-169: Log the caught error instead of silently discarding it.

The error is caught but not logged, making debugging difficult when verification fails unexpectedly.

-    } catch (_err) {
-      toast.error("Verification failed. Please try again.");
+    } catch (err) {
+      logger.error("Domain verification failed", { error: err, trackedDomainId, method });
+      toast.error("Verification failed. Please try again.");

321-325: Consider triggering form submission on Enter key.

Currently, Enter key is prevented but doesn't advance the wizard. Users might expect pressing Enter to submit the domain.

          onKeyDown={(e) => {
            if (e.key === "Enter") {
              e.preventDefault();
+             // Could trigger handleNext() here if desired
            }
          }}
lib/constants/tier-limits.ts (1)

15-23: LGTM! Consider deriving the type from the constant for DRY.

The constants are well-documented and follow the lib/constants/ organization pattern per coding guidelines.

Optional: derive the type from the constant to keep them in sync:

 export const DEFAULT_TIER_LIMITS = {
   free: 5,
   pro: 50,
 } as const;

-export type TierLimits = {
-  free: number;
-  pro: number;
-};
+export type TierLimits = {
+  [K in keyof typeof DEFAULT_TIER_LIMITS]: number;
+};
app/dashboard/settings/page.tsx (2)

46-49: Remove unnecessary type assertions.

The type assertion as TrackedDomainWithDetails[] on line 46 and the explicit type annotation on line 48 are redundant since the TRPC query should already provide the correct type.

Apply this diff:

-  const domains = (domainsQuery.data ?? []) as TrackedDomainWithDetails[];
-  const verifiedDomains = domains.filter(
-    (d: TrackedDomainWithDetails) => d.verified,
-  );
+  const domains = domainsQuery.data ?? [];
+  const verifiedDomains = domains.filter((d) => d.verified);

37-38: Consider logging the caught error.

The empty catch block swallows error details that could be useful for debugging. Consider logging the error before showing the toast.

Apply this diff:

-    } catch {
+    } catch (error) {
+      console.error("Failed to update notification settings:", error);
       toast.error("Failed to update settings");
     }
app/dashboard/page.tsx (2)

41-45: Consider awaiting refetch calls to ensure data freshness.

The refetch calls are not awaited, which may result in the UI not reflecting the latest data immediately. Consider using await Promise.all() to ensure both queries complete before the function returns.

Apply this diff:

   const handleAddSuccess = useCallback(() => {
     setResumeDomain(null);
-    limitsQuery.refetch();
-    domainsQuery.refetch();
+    void Promise.all([limitsQuery.refetch(), domainsQuery.refetch()]);
   }, [limitsQuery, domainsQuery]);

64-76: Add error logging to the remove handler.

Similar to the settings page, the catch block swallows error details. Consider logging the error for debugging.

Apply this diff:

   const handleRemove = useCallback(
     async (id: string) => {
       try {
         await removeMutation.mutateAsync({ trackedDomainId: id });
         toast.success("Domain removed");
-        limitsQuery.refetch();
-        domainsQuery.refetch();
-      } catch {
+        void Promise.all([limitsQuery.refetch(), domainsQuery.refetch()]);
+      } catch (error) {
+        console.error("Failed to remove domain:", error);
         toast.error("Failed to remove domain");
       }
     },
     [removeMutation, limitsQuery, domainsQuery],
   );
components/dashboard/tracked-domains-table.tsx (1)

96-248: Consider memoizing individual cell renderers for large datasets.

The column definitions are already memoized, but the inline cell renderers create new function references on each render. For tables with many rows, this can cause unnecessary re-renders.

This is a minor optimization and likely acceptable for typical domain counts.

lib/inngest/functions/check-domain-expiry.ts (1)

20-29: Unused constant: _NOTIFICATION_THRESHOLDS.

The underscore-prefixed constant suggests it's intentionally unused, but it appears to be documentation for the thresholds used in getNotificationType. Consider either removing it or using it to derive the logic in getNotificationType for a single source of truth.

-// Notification thresholds in days
-const _NOTIFICATION_THRESHOLDS = [30, 14, 7, 1] as const;
+// Notification thresholds in days (ordered from most to least urgent)
+const NOTIFICATION_THRESHOLDS = [1, 7, 14, 30] as const;
+type ThresholdDays = (typeof NOTIFICATION_THRESHOLDS)[number];
+
+const THRESHOLD_TO_TYPE: Record<ThresholdDays, NotificationType> = {
+  1: "domain_expiry_1d",
+  7: "domain_expiry_7d",
+  14: "domain_expiry_14d",
+  30: "domain_expiry_30d",
+};

 function getNotificationType(daysRemaining: number): NotificationType | null {
-  if (daysRemaining <= 1) return "domain_expiry_1d";
-  if (daysRemaining <= 7) return "domain_expiry_7d";
-  if (daysRemaining <= 14) return "domain_expiry_14d";
-  if (daysRemaining <= 30) return "domain_expiry_30d";
-  return null;
+  for (const threshold of NOTIFICATION_THRESHOLDS) {
+    if (daysRemaining <= threshold) {
+      return THRESHOLD_TO_TYPE[threshold];
+    }
+  }
+  return null;
 }
emails/domain-expiry.tsx (1)

99-106: Preview props have a static date that may appear stale.

expirationDate: "January 15, 2025" will look outdated during development. Consider using a dynamic preview date.

// Preview props for email development
const getPreviewProps = (): DomainExpiryEmailProps => {
  const futureDate = new Date();
  futureDate.setDate(futureDate.getDate() + 7);
  return {
    userName: "Jake",
    domainName: "example.com",
    expirationDate: futureDate.toLocaleDateString("en-US", { 
      month: "long", 
      day: "numeric", 
      year: "numeric" 
    }),
    daysRemaining: 7,
    dashboardUrl: "https://domainstack.io/dashboard",
  };
};

DomainExpiryEmail.PreviewProps = getPreviewProps();
lib/db/repos/tracked-domains.test.ts (1)

207-221: Remove unnecessary setTimeout in failure-timestamp test

In the "does not overwrite existing failure time on subsequent calls" test, the setTimeout(10) delay doesn’t influence the assertion (you compare the two stored timestamps for equality). It only slows the suite and could introduce flakiness on very slow CI.

You can safely drop the delay and keep the semantics identical.

server/routers/tracking.ts (1)

173-239: Avoid full list scan to resolve domainName for verification

verifyDomain currently calls getTrackedDomainsForUser(ctx.user.id) and then does a .find((d) => d.id === trackedDomainId) to get the domain name. This works (and limits are small), but it adds an extra query plus an O(n) scan on every verification.

Consider adding a repo helper that fetches a single tracked domain with its joined domains row by ID (e.g. getTrackedDomainWithDetailsById(id)), so you can resolve { domainName, verificationToken } in one targeted query and avoid the extra list call and scan.

server/services/verification.ts (3)

71-98: Reconsider wrapping tryAllVerificationMethods in cache

tryAllVerificationMethods is wrapped in React’s cache, so repeated calls with the same (domain, token) will return the first result without re-running the DNS/HTML/meta checks. For domain verification this can be surprising:

  • If a user fixes DNS/HTML/meta config and retries verification with the same token, the process may keep returning the old (failed) result.
  • In tests, different scenarios using the same domain and token can accidentally share cached results and bypass the mocked network behavior.

Domain verification is relatively infrequent and inherently time-varying, so correctness is usually more important than memoization here. Consider dropping the cache wrapper and exporting a plain async function:

-import { cache } from "react";
...
-export const tryAllVerificationMethods = cache(
-  async function tryAllVerificationMethods(
-    domain: string,
-    token: string,
-  ): Promise<VerificationResult> {
+export async function tryAllVerificationMethods(
+  domain: string,
+  token: string,
+): Promise<VerificationResult> {
   logger.debug("trying all verification methods", { domain });
   ...
-  },
-);
+}

This avoids stale results and makes behavior easier to reason about in both runtime and tests.


100-165: Update stale “hoot-verify” comments to match current constants

The doc comments for DNS and HTML verification still reference hoot-verify:

  • DNS: Expected record: _hoot-verify.example.com TXT "hoot-verify=<token>"
  • HTML: Expected file: https://example.com/.well-known/hoot-verify.txt

But the actual constants use _domainstack-verify and domainstack-verify. To avoid confusion for future readers, updating these comments to match DNS_VERIFICATION_HOST, DNS_VERIFICATION_PREFIX, and HTML_FILE_PATH would be helpful.


117-126: Let fetchWithTimeoutAndRetry own the User-Agent header

In both verifyDnsTxt, verifyHtmlFile, and verifyMetaTag, you explicitly pass a lowercased "user-agent" header even though fetchWithTimeoutAndRetry already injects "User-Agent": USER_AGENT by default. This can lead to redundant headers (User-Agent and user-agent) on the same request.

You can simplify the header objects to only include what’s unique to each call (e.g. accept, redirect) and rely on the helper to set the User-Agent:

-    const res = await fetchWithTimeoutAndRetry(
-      url,
-      {
-        headers: {
-          accept: "application/dns-json",
-          "user-agent": USER_AGENT,
-        },
-      },
-      { timeoutMs: 5000, retries: 2, backoffMs: 200 },
-    );
+    const res = await fetchWithTimeoutAndRetry(
+      url,
+      {
+        headers: {
+          accept: "application/dns-json",
+        },
+      },
+      { timeoutMs: 5000, retries: 2, backoffMs: 200 },
+    );

(and similarly for the HTML and meta-tag paths).

lib/db/repos/tracked-domains.ts (2)

192-199: Consider using COUNT(*) instead of selecting all rows for user domain counts

countTrackedDomainsForUser currently does a full select of all columns and then returns rows.length. For small per-user limits this is fine, but a COUNT(*) query would be more efficient and communicate intent more clearly:

export async function countTrackedDomainsForUser(userId: string) {
  const [{ count }] = await db
    .select({ count: sql<number>`count(*)` })
    .from(trackedDomains)
    .where(eq(trackedDomains.userId, userId));
  return count;
}

Not urgent given expected cardinalities, but a nice improvement if this call ends up in a hot path.


61-75: Handle potential unique-constraint races when creating tracked domains

createTrackedDomain assumes the caller has already ensured there isn’t an existing (userId, domainId) row. With the unique constraint in the schema this is usually fine, but two concurrent addDomain requests for the same domain/user could still race and cause a constraint violation.

If you expect concurrent adds, consider guarding this with onConflictDoNothing().returning() (and returning null when nothing was inserted), or pushing the “find-or-create” logic into this repo function so the behavior is atomic at the database level.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f7b0d5 and 0bc0b51.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (50)
  • .env.example (1 hunks)
  • app/api/auth/[...all]/route.ts (1 hunks)
  • app/api/inngest/route.ts (1 hunks)
  • app/dashboard/layout.tsx (1 hunks)
  • app/dashboard/loading.tsx (1 hunks)
  • app/dashboard/page.tsx (1 hunks)
  • app/dashboard/settings/page.tsx (1 hunks)
  • app/login/page.tsx (1 hunks)
  • components/auth/auth-button.tsx (1 hunks)
  • components/auth/sign-in-button.tsx (1 hunks)
  • components/auth/user-menu.tsx (1 hunks)
  • components/brand-icons.tsx (1 hunks)
  • components/dashboard/add-domain-dialog.tsx (1 hunks)
  • components/dashboard/dashboard-header.tsx (1 hunks)
  • components/dashboard/domain-health-badge.tsx (1 hunks)
  • components/dashboard/tracked-domain-card.tsx (1 hunks)
  • components/dashboard/tracked-domains-grid.tsx (1 hunks)
  • components/dashboard/tracked-domains-table.tsx (1 hunks)
  • components/dashboard/tracked-domains-view.tsx (1 hunks)
  • components/dashboard/verification-badge.test.tsx (1 hunks)
  • components/dashboard/verification-badge.tsx (1 hunks)
  • components/layout/app-header.tsx (2 hunks)
  • drizzle/0004_remarkable_zzzax.sql (1 hunks)
  • drizzle/meta/0004_snapshot.json (1 hunks)
  • drizzle/meta/_journal.json (1 hunks)
  • emails/domain-expiry.tsx (1 hunks)
  • emails/verification-failing.tsx (1 hunks)
  • emails/verification-revoked.tsx (1 hunks)
  • hooks/use-view-preference.ts (1 hunks)
  • lib/auth-client.ts (1 hunks)
  • lib/auth.ts (1 hunks)
  • lib/constants/index.ts (1 hunks)
  • lib/constants/tier-limits.ts (1 hunks)
  • lib/db/repos/notifications.ts (1 hunks)
  • lib/db/repos/tracked-domains.test.ts (1 hunks)
  • lib/db/repos/tracked-domains.ts (1 hunks)
  • lib/db/repos/user-limits.test.ts (1 hunks)
  • lib/db/repos/user-limits.ts (1 hunks)
  • lib/db/schema.ts (1 hunks)
  • lib/edge-config.test.ts (1 hunks)
  • lib/edge-config.ts (2 hunks)
  • lib/inngest/functions/check-domain-expiry.ts (1 hunks)
  • lib/inngest/functions/reverify-domains.ts (1 hunks)
  • lib/resend.ts (1 hunks)
  • package.json (4 hunks)
  • server/routers/_app.ts (1 hunks)
  • server/routers/tracking.ts (1 hunks)
  • server/services/verification.test.ts (1 hunks)
  • server/services/verification.ts (1 hunks)
  • trpc/init.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (17)
lib/constants/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Organize constants by domain in lib/constants/ submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via lib/constants/index.ts

Files:

  • lib/constants/tier-limits.ts
  • lib/constants/index.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • lib/constants/tier-limits.ts
  • app/dashboard/page.tsx
  • components/dashboard/verification-badge.tsx
  • lib/inngest/functions/reverify-domains.ts
  • app/api/inngest/route.ts
  • components/auth/sign-in-button.tsx
  • components/auth/user-menu.tsx
  • components/dashboard/domain-health-badge.tsx
  • server/routers/tracking.ts
  • components/dashboard/dashboard-header.tsx
  • lib/db/repos/user-limits.ts
  • app/dashboard/loading.tsx
  • components/dashboard/add-domain-dialog.tsx
  • components/layout/app-header.tsx
  • emails/domain-expiry.tsx
  • app/login/page.tsx
  • app/dashboard/settings/page.tsx
  • lib/auth-client.ts
  • emails/verification-failing.tsx
  • components/dashboard/tracked-domain-card.tsx
  • components/dashboard/tracked-domains-table.tsx
  • components/dashboard/tracked-domains-grid.tsx
  • lib/db/repos/user-limits.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • components/dashboard/tracked-domains-view.tsx
  • lib/db/repos/notifications.ts
  • components/dashboard/verification-badge.test.tsx
  • lib/inngest/functions/check-domain-expiry.ts
  • lib/db/repos/tracked-domains.ts
  • lib/auth.ts
  • app/dashboard/layout.tsx
  • server/services/verification.ts
  • components/auth/auth-button.tsx
  • lib/resend.ts
  • emails/verification-revoked.tsx
  • lib/edge-config.test.ts
  • components/brand-icons.tsx
  • server/routers/_app.ts
  • hooks/use-view-preference.ts
  • app/api/auth/[...all]/route.ts
  • lib/edge-config.ts
  • server/services/verification.test.ts
  • lib/constants/index.ts
  • trpc/init.ts
  • lib/db/schema.ts
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • lib/constants/tier-limits.ts
  • app/dashboard/page.tsx
  • components/dashboard/verification-badge.tsx
  • lib/inngest/functions/reverify-domains.ts
  • app/api/inngest/route.ts
  • components/auth/sign-in-button.tsx
  • components/auth/user-menu.tsx
  • drizzle/meta/_journal.json
  • components/dashboard/domain-health-badge.tsx
  • package.json
  • server/routers/tracking.ts
  • components/dashboard/dashboard-header.tsx
  • lib/db/repos/user-limits.ts
  • app/dashboard/loading.tsx
  • components/dashboard/add-domain-dialog.tsx
  • components/layout/app-header.tsx
  • emails/domain-expiry.tsx
  • app/login/page.tsx
  • app/dashboard/settings/page.tsx
  • lib/auth-client.ts
  • emails/verification-failing.tsx
  • components/dashboard/tracked-domain-card.tsx
  • components/dashboard/tracked-domains-table.tsx
  • components/dashboard/tracked-domains-grid.tsx
  • lib/db/repos/user-limits.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • components/dashboard/tracked-domains-view.tsx
  • lib/db/repos/notifications.ts
  • components/dashboard/verification-badge.test.tsx
  • lib/inngest/functions/check-domain-expiry.ts
  • lib/db/repos/tracked-domains.ts
  • lib/auth.ts
  • app/dashboard/layout.tsx
  • server/services/verification.ts
  • components/auth/auth-button.tsx
  • lib/resend.ts
  • emails/verification-revoked.tsx
  • lib/edge-config.test.ts
  • components/brand-icons.tsx
  • server/routers/_app.ts
  • hooks/use-view-preference.ts
  • app/api/auth/[...all]/route.ts
  • lib/edge-config.ts
  • server/services/verification.test.ts
  • lib/constants/index.ts
  • drizzle/meta/0004_snapshot.json
  • trpc/init.ts
  • lib/db/schema.ts
app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

app/**/*.tsx: Next.js App Router with default server components; keep app/page.tsx and app/api/* thin and delegate to server/ or lib/
Client components must begin with "use client" directive

Files:

  • app/dashboard/page.tsx
  • app/dashboard/loading.tsx
  • app/login/page.tsx
  • app/dashboard/settings/page.tsx
  • app/dashboard/layout.tsx
{app/**,server/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use distinct ID sourced from PostHog cookie via cache()-wrapped getDistinctId() to comply with Next.js restrictions

Files:

  • app/dashboard/page.tsx
  • app/api/inngest/route.ts
  • server/routers/tracking.ts
  • app/dashboard/loading.tsx
  • app/login/page.tsx
  • app/dashboard/settings/page.tsx
  • app/dashboard/layout.tsx
  • server/services/verification.ts
  • server/routers/_app.ts
  • app/api/auth/[...all]/route.ts
  • server/services/verification.test.ts
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/dashboard/verification-badge.tsx
  • components/auth/sign-in-button.tsx
  • components/auth/user-menu.tsx
  • components/dashboard/domain-health-badge.tsx
  • components/dashboard/dashboard-header.tsx
  • components/dashboard/add-domain-dialog.tsx
  • components/layout/app-header.tsx
  • components/dashboard/tracked-domain-card.tsx
  • components/dashboard/tracked-domains-table.tsx
  • components/dashboard/tracked-domains-grid.tsx
  • components/dashboard/tracked-domains-view.tsx
  • components/dashboard/verification-badge.test.tsx
  • components/auth/auth-button.tsx
  • components/brand-icons.tsx
lib/inngest/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Inngest client and functions for event-driven background section revalidation should be organized in lib/inngest/ with proper structure

Files:

  • lib/inngest/functions/reverify-domains.ts
  • lib/inngest/functions/check-domain-expiry.ts
{server/**,app/api/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

{server/**,app/api/**}/*.{ts,tsx}: Use drizzle-zod for DB boundary validation with *Select schemas from lib/db/zod.ts for reads and *Insert/*Update for writes, with dates coerced in write schemas
Server-side logging: import singleton import { logger } from "@/lib/logger/server" or create service logger with createLogger({ source: "..." })
Server logging automatically injects OpenTelemetry trace/span IDs and correlation IDs via AsyncLocalStorage for request tracing
Server logging automatically tracks critical errors in PostHog via after()
Server logging supports levels: trace, debug, info, warn, error, fatal

Files:

  • app/api/inngest/route.ts
  • server/routers/tracking.ts
  • server/services/verification.ts
  • server/routers/_app.ts
  • app/api/auth/[...all]/route.ts
  • server/services/verification.test.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Isolate DNS, RDAP/WHOIS, TLS, and header probing services in server/services/ with tRPC routers in server/routers/

Files:

  • server/routers/tracking.ts
  • server/services/verification.ts
  • server/routers/_app.ts
  • server/services/verification.test.ts
server/routers/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Review trpc/init.ts when extending procedures to ensure auth/context remain intact

Files:

  • server/routers/tracking.ts
  • server/routers/_app.ts
lib/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

lib/db/**/*.{ts,tsx}: Drizzle ORM schema, migrations, and repository layer should be organized in lib/db/ with repos in lib/db/repos/ for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)
Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Files:

  • lib/db/repos/user-limits.ts
  • lib/db/repos/user-limits.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • lib/db/repos/notifications.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/schema.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx}: Use Vitest with React Testing Library; configure in vitest.config.ts with threads pool for sandboxed environment compatibility
Mock @vercel/blob (put and del functions) in tests; set BLOB_READ_WRITE_TOKEN via vi.stubEnv in suites that touch uploads/deletes
Mock URL.createObjectURL/revokeObjectURL with vi.fn() in tests that need them
Logger is mocked in tests via vitest.setup.ts; use vi.mocked(logger.info) to assert log calls in tests

Files:

  • lib/db/repos/user-limits.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • components/dashboard/verification-badge.test.tsx
  • lib/edge-config.test.ts
  • server/services/verification.test.ts
lib/db/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Replace @/lib/db/client with a PGlite-backed instance (@/lib/db/pglite) for database tests

Files:

  • lib/db/repos/user-limits.test.ts
  • lib/db/repos/tracked-domains.test.ts
components/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.test.{ts,tsx}: Do not add direct tests for components/ui/* (shadcn components); mock Radix primitives (Accordion, Tooltip) when testing domain sections
Mock tRPC/React Query for components like Favicon and Screenshot in tests

Files:

  • components/dashboard/verification-badge.test.tsx
hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use camelCase named exports for shared stateful helpers in hooks/ directory

Files:

  • hooks/use-view-preference.ts
server/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer vi.hoisted for ESM module mocks in server tests (e.g., node:tls)

Files:

  • server/services/verification.test.ts
trpc/init.ts

📄 CodeRabbit inference engine (AGENTS.md)

tRPC middleware in trpc/init.ts automatically logs all procedures with correlation IDs and OpenTelemetry context

Files:

  • trpc/init.ts
🧠 Learnings (33)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/constants/**/*.{ts,tsx} : Organize constants by domain in `lib/constants/` submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via `lib/constants/index.ts`

Applied to files:

  • lib/constants/tier-limits.ts
  • lib/inngest/functions/reverify-domains.ts
  • lib/db/repos/user-limits.ts
  • emails/domain-expiry.tsx
  • lib/auth-client.ts
  • lib/inngest/functions/check-domain-expiry.ts
  • lib/db/repos/tracked-domains.ts
  • server/services/verification.ts
  • lib/edge-config.test.ts
  • lib/edge-config.ts
  • lib/constants/index.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use Vercel Edge Config for dynamic, low-latency configuration (e.g., `domain_suggestions` array) without redeployment

Applied to files:

  • lib/constants/tier-limits.ts
  • lib/edge-config.test.ts
  • lib/edge-config.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/inngest/**/*.{ts,tsx} : Inngest client and functions for event-driven background section revalidation should be organized in `lib/inngest/` with proper structure

Applied to files:

  • lib/inngest/functions/reverify-domains.ts
  • app/api/inngest/route.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use event-driven background revalidation via Inngest functions in `lib/inngest/functions/` with built-in concurrency control

Applied to files:

  • lib/inngest/functions/reverify-domains.ts
  • app/api/inngest/route.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Keep secrets in `.env.local`; see `.env.example` for required variables

Applied to files:

  • .env.example
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Requires Node.js >= 22 as specified in `package.json` engines

Applied to files:

  • package.json
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/services/screenshot.ts : Prefer `puppeteer-core` + `sparticuz/chromium` on Vercel for screenshots

Applied to files:

  • package.json
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/**/*.{ts,tsx} : Isolate DNS, RDAP/WHOIS, TLS, and header probing services in `server/services/` with tRPC routers in `server/routers/`

Applied to files:

  • server/routers/tracking.ts
  • server/services/verification.ts
  • server/routers/_app.ts
  • server/services/verification.test.ts
  • trpc/init.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/routers/**/*.{ts,tsx} : Review `trpc/init.ts` when extending procedures to ensure auth/context remain intact

Applied to files:

  • server/routers/tracking.ts
  • lib/auth.ts
  • server/routers/_app.ts
  • app/api/auth/[...all]/route.ts
  • trpc/init.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Drizzle ORM schema, migrations, and repository layer should be organized in `lib/db/` with repos in `lib/db/repos/` for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)

Applied to files:

  • lib/db/repos/user-limits.ts
  • lib/db/repos/notifications.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/schema.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to app/**/*.tsx : Next.js App Router with default server components; keep `app/page.tsx` and `app/api/*` thin and delegate to `server/` or `lib/`

Applied to files:

  • app/login/page.tsx
  • app/dashboard/layout.tsx
  • server/routers/_app.ts
  • app/api/auth/[...all]/route.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {app/**,server/**}/*.{ts,tsx} : Use distinct ID sourced from PostHog cookie via `cache()`-wrapped `getDistinctId()` to comply with Next.js restrictions

Applied to files:

  • app/login/page.tsx
  • app/dashboard/layout.tsx
  • server/routers/_app.ts
  • app/api/auth/[...all]/route.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to instrumentation-client.ts : Update `instrumentation-client.ts` when adding analytics

Applied to files:

  • lib/auth-client.ts
  • server/routers/_app.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to app/**/*.tsx : Client components must begin with `"use client"` directive

Applied to files:

  • lib/auth-client.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to instrumentation-client.ts : Client-side analytics captured via `posthog-js` initialized in `instrumentation-client.ts`

Applied to files:

  • lib/auth-client.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.{ts,tsx} : Client-side logging: import singleton `import { logger } from "@/lib/logger/client"` or use hook `useLogger({ component: "..." })`

Applied to files:

  • lib/auth-client.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.test.{ts,tsx} : Replace `@/lib/db/client` with a PGlite-backed instance (`@/lib/db/pglite`) for database tests

Applied to files:

  • lib/db/repos/user-limits.test.ts
  • lib/db/repos/tracked-domains.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to vitest.setup.ts : Global test setup in `vitest.setup.ts` mocks analytics clients/servers (`@/lib/analytics/server` and `@/lib/analytics/client`), logger clients/servers (`@/lib/logger/server` and `@/lib/logger/client`), and `server-only` module

Applied to files:

  • lib/db/repos/user-limits.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • lib/edge-config.test.ts
  • server/services/verification.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.test.{ts,tsx} : Use **Vitest** with React Testing Library; configure in `vitest.config.ts` with `threads` pool for sandboxed environment compatibility

Applied to files:

  • lib/db/repos/user-limits.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • components/dashboard/verification-badge.test.tsx
  • lib/edge-config.test.ts
  • server/services/verification.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.test.{ts,tsx} : Mock `vercel/blob` (`put` and `del` functions) in tests; set `BLOB_READ_WRITE_TOKEN` via `vi.stubEnv` in suites that touch uploads/deletes

Applied to files:

  • lib/db/repos/user-limits.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • lib/edge-config.test.ts
  • server/services/verification.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/**/*.test.{ts,tsx} : Prefer `vi.hoisted` for ESM module mocks in server tests (e.g., `node:tls`)

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
  • lib/edge-config.test.ts
  • server/services/verification.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
  • lib/db/repos/tracked-domains.ts
  • lib/auth.ts
  • drizzle/meta/0004_snapshot.json
  • lib/db/schema.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.test.{ts,tsx} : Do not add direct tests for `components/ui/*` (shadcn components); mock Radix primitives (Accordion, Tooltip) when testing domain sections

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
  • components/dashboard/verification-badge.test.tsx
  • lib/edge-config.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.test.{ts,tsx} : Logger is mocked in tests via `vitest.setup.ts`; use `vi.mocked(logger.info)` to assert log calls in tests

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
  • server/services/verification.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Server-side logging: import singleton `import { logger } from "@/lib/logger/server"` or create service logger with `createLogger({ source: "..." })`

Applied to files:

  • lib/db/repos/notifications.ts
  • lib/auth.ts
  • server/routers/_app.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.test.{ts,tsx} : Mock tRPC/React Query for components like `Favicon` and `Screenshot` in tests

Applied to files:

  • components/dashboard/verification-badge.test.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Use `drizzle-zod` for DB boundary validation with *Select schemas from `lib/db/zod.ts` for reads and *Insert/*Update for writes, with dates coerced in write schemas

Applied to files:

  • lib/auth.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Server logging automatically injects OpenTelemetry trace/span IDs and correlation IDs via AsyncLocalStorage for request tracing

Applied to files:

  • server/routers/_app.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Server logging automatically tracks critical errors in PostHog via `after()`

Applied to files:

  • server/routers/_app.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.{ts,tsx} : Client logging propagates correlation IDs from server via header/cookie/localStorage

Applied to files:

  • server/routers/_app.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/services/screenshot.test.ts : Screenshot service (`server/services/screenshot.ts`) uses hoisted mocks for `puppeteer`/`puppeteer-core` and `sparticuz/chromium` in tests

Applied to files:

  • server/services/verification.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.test.{ts,tsx} : Mock `URL.createObjectURL`/`revokeObjectURL` with `vi.fn()` in tests that need them

Applied to files:

  • server/services/verification.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to trpc/init.ts : tRPC middleware in `trpc/init.ts` automatically logs all procedures with correlation IDs and OpenTelemetry context

Applied to files:

  • trpc/init.ts
🧬 Code graph analysis (29)
app/dashboard/page.tsx (7)
hooks/use-view-preference.ts (1)
  • useViewPreference (14-43)
trpc/server.ts (1)
  • trpc (15-19)
lib/db/repos/tracked-domains.ts (1)
  • TrackedDomainWithDetails (36-56)
lib/db/schema.ts (1)
  • domains (265-285)
components/dashboard/dashboard-header.tsx (1)
  • DashboardHeader (18-67)
components/dashboard/tracked-domains-view.tsx (1)
  • TrackedDomainsView (26-75)
components/dashboard/add-domain-dialog.tsx (1)
  • AddDomainDialog (57-297)
components/dashboard/verification-badge.tsx (4)
lib/db/repos/tracked-domains.ts (1)
  • VerificationStatusType (21-22)
lib/db/schema.ts (1)
  • verificationStatus (55-59)
components/ui/badge.tsx (1)
  • Badge (46-46)
lib/utils.ts (1)
  • cn (4-6)
lib/inngest/functions/reverify-domains.ts (8)
lib/inngest/client.ts (1)
  • inngest (10-13)
lib/db/repos/tracked-domains.ts (7)
  • getPendingDomainsForAutoVerification (357-376)
  • verifyTrackedDomain (204-223)
  • getVerifiedDomainsForReverification (315-341)
  • markVerificationSuccessful (382-394)
  • TrackedDomainForReverification (266-277)
  • markVerificationFailing (400-416)
  • revokeVerification (422-435)
server/services/verification.ts (2)
  • tryAllVerificationMethods (71-98)
  • verifyDomainOwnership (40-66)
lib/resend.ts (2)
  • resend (13-13)
  • RESEND_FROM_EMAIL (15-16)
lib/db/repos/notifications.ts (2)
  • hasNotificationBeenSent (47-63)
  • createNotification (20-42)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
emails/verification-failing.tsx (1)
  • VerificationFailingEmail (30-94)
emails/verification-revoked.tsx (1)
  • VerificationRevokedEmail (21-80)
app/api/inngest/route.ts (4)
lib/inngest/client.ts (1)
  • inngest (10-13)
lib/inngest/functions/section-revalidate.ts (1)
  • sectionRevalidate (47-85)
lib/inngest/functions/check-domain-expiry.ts (1)
  • checkDomainExpiry (35-126)
lib/inngest/functions/reverify-domains.ts (1)
  • reverifyDomains (47-182)
components/auth/sign-in-button.tsx (3)
components/ui/button.tsx (1)
  • Button (60-60)
components/ui/spinner.tsx (1)
  • Spinner (16-16)
components/brand-icons.tsx (1)
  • GitHubIcon (5-15)
components/dashboard/domain-health-badge.tsx (1)
lib/utils.ts (1)
  • cn (4-6)
server/routers/tracking.ts (6)
lib/domain-server.ts (1)
  • toRegistrableDomain (10-27)
lib/db/repos/user-limits.ts (2)
  • getOrCreateUserLimits (28-99)
  • canUserAddDomain (147-153)
lib/db/repos/tracked-domains.ts (8)
  • countTrackedDomainsForUser (192-199)
  • getTrackedDomainsForUser (111-187)
  • findTrackedDomain (80-93)
  • createTrackedDomain (61-75)
  • findTrackedDomainById (98-106)
  • verifyTrackedDomain (204-223)
  • updateTrackedDomainNotifications (228-239)
  • deleteTrackedDomain (244-252)
lib/db/schema.ts (1)
  • domains (265-285)
lib/db/repos/domains.ts (1)
  • ensureDomainRecord (65-84)
server/services/verification.ts (4)
  • getVerificationInstructions (273-308)
  • generateVerificationToken (263-268)
  • verifyDomainOwnership (40-66)
  • tryAllVerificationMethods (71-98)
components/dashboard/dashboard-header.tsx (4)
hooks/use-view-preference.ts (1)
  • ViewMode (5-5)
components/ui/progress.tsx (1)
  • Progress (31-31)
components/ui/toggle-group.tsx (2)
  • ToggleGroup (72-72)
  • ToggleGroupItem (72-72)
components/ui/button.tsx (1)
  • Button (60-60)
lib/db/repos/user-limits.ts (3)
lib/db/schema.ts (2)
  • userTier (68-68)
  • userLimits (217-230)
lib/db/client.ts (1)
  • db (27-27)
lib/edge-config.ts (1)
  • getMaxDomainsForTier (111-116)
app/dashboard/loading.tsx (1)
components/ui/skeleton.tsx (1)
  • Skeleton (13-13)
components/layout/app-header.tsx (2)
components/ui/separator.tsx (1)
  • Separator (28-28)
components/auth/auth-button.tsx (1)
  • AuthButton (8-31)
emails/domain-expiry.tsx (1)
components/ui/button.tsx (1)
  • Button (60-60)
app/dashboard/settings/page.tsx (6)
trpc/server.ts (1)
  • trpc (15-19)
lib/db/schema.ts (1)
  • domains (265-285)
lib/db/repos/tracked-domains.ts (1)
  • TrackedDomainWithDetails (36-56)
components/ui/card.tsx (5)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardDescription (90-90)
  • CardContent (91-91)
components/ui/switch.tsx (1)
  • Switch (31-31)
components/ui/skeleton.tsx (1)
  • Skeleton (13-13)
lib/auth-client.ts (1)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
emails/verification-failing.tsx (3)
lib/db/repos/tracked-domains.ts (1)
  • VerificationMethod (20-20)
lib/db/schema.ts (1)
  • verificationMethod (50-54)
components/ui/button.tsx (1)
  • Button (60-60)
components/dashboard/tracked-domains-grid.tsx (2)
lib/db/repos/tracked-domains.ts (1)
  • TrackedDomainWithDetails (36-56)
components/dashboard/tracked-domain-card.tsx (1)
  • TrackedDomainCard (42-174)
lib/db/repos/tracked-domains.test.ts (1)
lib/db/repos/tracked-domains.ts (12)
  • createTrackedDomain (61-75)
  • findTrackedDomain (80-93)
  • findTrackedDomainById (98-106)
  • verifyTrackedDomain (204-223)
  • markVerificationFailing (400-416)
  • markVerificationSuccessful (382-394)
  • revokeVerification (422-435)
  • countTrackedDomainsForUser (192-199)
  • deleteTrackedDomain (244-252)
  • getTrackedDomainsForUser (111-187)
  • getVerifiedDomainsForReverification (315-341)
  • getPendingDomainsForAutoVerification (357-376)
components/dashboard/tracked-domains-view.tsx (5)
hooks/use-view-preference.ts (1)
  • ViewMode (5-5)
lib/db/repos/tracked-domains.ts (1)
  • TrackedDomainWithDetails (36-56)
lib/db/schema.ts (1)
  • domains (265-285)
components/dashboard/tracked-domains-table.tsx (1)
  • TrackedDomainsTable (89-328)
components/dashboard/tracked-domains-grid.tsx (1)
  • TrackedDomainsGrid (14-45)
lib/db/repos/notifications.ts (2)
lib/db/schema.ts (2)
  • notificationType (60-67)
  • notifications (198-212)
lib/db/client.ts (1)
  • db (27-27)
components/dashboard/verification-badge.test.tsx (1)
components/dashboard/verification-badge.tsx (1)
  • VerificationBadge (14-60)
lib/auth.ts (2)
lib/db/client.ts (1)
  • db (27-27)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
server/services/verification.ts (3)
lib/db/repos/tracked-domains.ts (1)
  • VerificationMethod (20-20)
lib/fetch.ts (1)
  • fetchWithTimeoutAndRetry (10-55)
lib/constants/app.ts (1)
  • USER_AGENT (5-7)
components/auth/auth-button.tsx (2)
components/auth/user-menu.tsx (1)
  • UserMenu (17-92)
components/ui/button.tsx (1)
  • Button (60-60)
emails/verification-revoked.tsx (1)
components/ui/button.tsx (1)
  • Button (60-60)
lib/edge-config.test.ts (1)
lib/edge-config.ts (3)
  • getDefaultSuggestions (30-57)
  • getTierLimits (78-105)
  • getMaxDomainsForTier (111-116)
app/api/auth/[...all]/route.ts (1)
lib/auth.ts (1)
  • auth (8-40)
lib/edge-config.ts (2)
lib/constants/tier-limits.ts (2)
  • TierLimits (20-23)
  • DEFAULT_TIER_LIMITS (15-18)
lib/logger/server.ts (1)
  • logger (266-266)
server/services/verification.test.ts (2)
server/services/verification.ts (3)
  • getVerificationInstructions (273-308)
  • verifyDomainOwnership (40-66)
  • tryAllVerificationMethods (71-98)
lib/db/repos/tracked-domains.ts (1)
  • VerificationMethod (20-20)
trpc/init.ts (1)
lib/auth.ts (1)
  • auth (8-40)
🪛 ast-grep (0.40.0)
server/services/verification.ts

[warning] 239-242: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
<meta[^>]*name=["']${META_TAG_NAME}["'][^>]*content=["']([^"']+)["'][^>]*/?>|<meta[^>]*content=["']([^"']+)["'][^>]*name=["']${META_TAG_NAME}["'][^>]*/?>,
"i",
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 Gitleaks (8.30.0)
server/services/verification.test.ts

[high] 48-48: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

@jakejarvis jakejarvis marked this pull request as ready for review December 4, 2025 20:02
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
lib/db/schema.ts (2)

105-133: Verify the password field in the OAuth accounts table.

The accounts table includes a password field (Line 124) alongside OAuth tokens. This duplicates the concern raised in the SQL migration review. Mixing authentication methods in a single table can introduce security risks and violates separation of concerns.


191-207: Reconsider the unique constraint on notifications.

The unique constraint on Lines 204-205 prevents maintaining notification history, as discussed in the SQL migration review. This is a duplicate of the earlier concern.

🧹 Nitpick comments (11)
app/layout.tsx (1)

55-60: Placement of {auth} content is reasonable within Providers

Rendering {auth} inside <Providers> and after the main app shell makes sense for auth-related overlays or parallel routes that should sit outside the primary children tree but still share context. If you later need toast notifications to visually appear above any auth UI, you might consider moving <Toaster /> after any overlay layers, but as-is there’s no correctness problem.

app/@auth/(.)login/page.tsx (1)

13-27: Consider guarding router.back() for direct visits

Right now any close action calls router.back(). If a user lands directly on this URL (no in‑app history), closing the modal may navigate them away from the site. Consider adding a small guard/fallback (e.g., checking history length and pushing a safe route instead) if that case matters for your flow.

components/dashboard/settings-content.tsx (3)

99-121: Defensively handle possibly missing notificationOverrides in optimistic update

In the domain overrides mutation, the optimistic update assumes d.notificationOverrides is always a defined object:

notificationOverrides: {
  ...d.notificationOverrides,
  ...overrides,
},

If the backend ever returns notificationOverrides as null or undefined, this spread would throw at runtime. It’s safer to guard:

-                  notificationOverrides: {
-                    ...d.notificationOverrides,
-                    ...overrides,
-                  },
+                  notificationOverrides: {
+                    ...(d.notificationOverrides ?? {}),
+                    ...overrides,
+                  },

This keeps the optimistic path robust against schema or data shape changes.


194-205: Surface query errors instead of silently falling back to empty/default data

When either query fails, isLoading is false and data is undefined, so the UI quietly:

  • Treats domains as [] and shows “Verify your domains…”
  • Treats globalPrefs as the hardcoded all‑true default.

That can mask backend or network issues and mislead users. Consider explicitly handling error states, e.g.:

if (domainsQuery.isError || globalPrefsQuery.isError) {
  // Show an inline error message and/or toast, possibly with a retry button.
}

before deriving domains/globalPrefs, so users see that loading failed instead of being shown synthetic defaults.

Also applies to: 293-297


374-421: Confirm three‑state checkbox UX (inherit → opposite of global → other → inherit)

The three‑state cycle is:

  • Inherited: value === undefined → first click sets !globalValue (explicit opposite of global).
  • Next click toggles true ↔ false.
  • Final click returns to inherit.

This makes the first override always differ from the global default, which may be exactly what you want, but it’s a bit non‑obvious. Please double‑check that this cycle matches the intended product behavior; if not, you might want a simpler sequence like inherit → on → off → inherit regardless of the global value.

Also applies to: 424-472, 474-521

lib/db/repos/certificates.ts (2)

80-82: Potentially redundant isNotNull check.

According to the schema definition, certificates.validTo is defined with .notNull(), so the isNotNull(certificates.validTo) condition should always be true for all rows. This check can be removed unless there's a specific reason to guard against unexpected nulls.

     .where(
-      and(eq(trackedDomains.verified, true), isNotNull(certificates.validTo)),
+      eq(trackedDomains.verified, true),
     );

84-92: Consider SQL-level aggregation for better scalability.

The current client-side grouping works correctly but fetches all certificates before filtering. For improved efficiency with larger datasets, you could use a SQL subquery or window function to select only the earliest-expiring certificate per domain directly in the query.

This is fine for moderate data volumes but worth considering if the tracked domains scale significantly.

lib/inngest/functions/check-certificate-expiry.ts (1)

26-45: Consider extracting shared utilities to reduce duplication.

Both getCertificateExpiryNotificationType and generateIdempotencyKey are duplicated between this file and check-domain-expiry.ts. Consider:

  1. Adding getCertificateExpiryNotificationType to lib/constants/notifications.ts alongside getDomainExpiryNotificationType
  2. Extracting generateIdempotencyKey to a shared location (e.g., lib/inngest/utils.ts or within the notifications module)

This would improve maintainability and ensure consistent behavior.

// lib/constants/notifications.ts - add alongside getDomainExpiryNotificationType
export function getCertificateExpiryNotificationType(
  daysRemaining: number,
): NotificationType | null {
  for (const threshold of CERTIFICATE_EXPIRY_THRESHOLDS) {
    if (daysRemaining <= threshold) {
      return CERTIFICATE_THRESHOLD_TO_TYPE[threshold];
    }
  }
  return null;
}
server/routers/tracking.ts (1)

287-324: Use targeted query for better performance.

getVerificationInstructions currently fetches all tracked domains for the user via getTrackedDomainsForUser (which includes joins to registrations, hosting, and multiple provider tables), then filters in memory to find the requested domain. For users with many tracked domains, this is inefficient.

Use a targeted query similar to findTrackedDomainWithDomainName:

  getVerificationInstructions: protectedProcedure
    .input(
      z.object({
        trackedDomainId: z.string().uuid(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const { trackedDomainId } = input;

-     // Get tracked domain
-     const domains = await getTrackedDomainsForUser(ctx.user.id);
-     const tracked = domains.find((d) => d.id === trackedDomainId);
+     // Get tracked domain with domain name in a single targeted query
+     const tracked = await findTrackedDomainWithDomainName(trackedDomainId);

      if (!tracked) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "Tracked domain not found",
        });
      }

+     // Ensure user owns this tracked domain
+     if (tracked.userId !== ctx.user.id) {
+       throw new TRPCError({
+         code: "FORBIDDEN",
+         message: "You do not have access to this domain",
+       });
+     }

      return {
        dns_txt: getVerificationInstructions(
          tracked.domainName,
          tracked.verificationToken,
          "dns_txt",
        ),
        html_file: getVerificationInstructions(
          tracked.domainName,
          tracked.verificationToken,
          "html_file",
        ),
        meta_tag: getVerificationInstructions(
          tracked.domainName,
          tracked.verificationToken,
          "meta_tag",
        ),
      };
    }),
lib/db/repos/notifications.ts (1)

22-44: Consider returning existing notification on conflict for clearer API semantics.

createNotification uses onConflictDoNothing() and returns null when a notification already exists. This makes it difficult for callers to distinguish between an error and a successful idempotency check.

Consider fetching and returning the existing record on conflict:

 export async function createNotification(params: CreateNotificationParams) {
   const { trackedDomainId, type } = params;

   try {
     const inserted = await db
       .insert(notifications)
       .values({
         trackedDomainId,
         type,
         sentAt: new Date(),
       })
       .onConflictDoNothing()
       .returning();

-    return inserted[0] ?? null;
+    if (inserted.length > 0) {
+      return inserted[0];
+    }
+
+    // Notification already exists, fetch and return it
+    const existing = await db
+      .select()
+      .from(notifications)
+      .where(
+        and(
+          eq(notifications.trackedDomainId, trackedDomainId),
+          eq(notifications.type, type),
+        ),
+      )
+      .limit(1);
+
+    return existing[0] ?? null;
   } catch (err) {
     logger.error("failed to create notification record", err, {
       trackedDomainId,
       type,
     });
     return null;
   }
 }

This way, callers always receive the notification record on success (new or existing), and null clearly indicates an error.

lib/db/schema.ts (1)

164-166: Simplify the field name for clarity.

The field is named verificationStatusEnum but the SQL column is verification_status. The "Enum" suffix is redundant and confusing—the field stores a value of enum type, not an enum itself.

Apply this diff to simplify the naming:

-    verificationStatusEnum: verificationStatus("verification_status")
+    verificationStatus: verificationStatus("verification_status")

Then update any references in related files (repos, services) from verificationStatusEnum to verificationStatus.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0bc0b51 and 8d1852c.

📒 Files selected for processing (45)
  • AGENTS.md (3 hunks)
  • README.md (1 hunks)
  • app/@auth/(.)login/page.tsx (1 hunks)
  • app/@auth/[...catchAll]/page.tsx (1 hunks)
  • app/@auth/default.tsx (1 hunks)
  • app/api/inngest/route.ts (1 hunks)
  • app/dashboard/@settings/(.)settings/page.tsx (1 hunks)
  • app/dashboard/@settings/[...catchAll]/page.tsx (1 hunks)
  • app/dashboard/@settings/default.tsx (1 hunks)
  • app/dashboard/layout.tsx (1 hunks)
  • app/dashboard/page.tsx (1 hunks)
  • app/dashboard/settings/page.tsx (1 hunks)
  • app/layout.tsx (2 hunks)
  • app/login/page.tsx (1 hunks)
  • components/auth/login-content.tsx (1 hunks)
  • components/auth/sign-in-button.tsx (1 hunks)
  • components/auth/user-menu.tsx (1 hunks)
  • components/dashboard/add-domain-dialog.tsx (1 hunks)
  • components/dashboard/dashboard-header.tsx (1 hunks)
  • components/dashboard/settings-content.tsx (1 hunks)
  • components/dashboard/tracked-domain-card.tsx (1 hunks)
  • components/dashboard/tracked-domains-table.tsx (1 hunks)
  • components/dashboard/verification-badge.test.tsx (1 hunks)
  • drizzle/0004_talented_the_call.sql (1 hunks)
  • drizzle/meta/0004_snapshot.json (1 hunks)
  • drizzle/meta/_journal.json (1 hunks)
  • emails/certificate-expiry.tsx (1 hunks)
  • lib/constants/index.ts (1 hunks)
  • lib/constants/notifications.ts (1 hunks)
  • lib/constants/tier-limits.ts (1 hunks)
  • lib/db/repos/certificates.ts (2 hunks)
  • lib/db/repos/notifications.ts (1 hunks)
  • lib/db/repos/tracked-domains.test.ts (1 hunks)
  • lib/db/repos/tracked-domains.ts (1 hunks)
  • lib/db/repos/user-limits.ts (1 hunks)
  • lib/db/repos/user-notification-preferences.ts (1 hunks)
  • lib/db/schema.ts (2 hunks)
  • lib/inngest/functions/check-certificate-expiry.ts (1 hunks)
  • lib/inngest/functions/check-domain-expiry.ts (1 hunks)
  • lib/inngest/functions/reverify-domains.ts (1 hunks)
  • lib/resend.ts (1 hunks)
  • lib/schemas/index.ts (1 hunks)
  • lib/schemas/internal/notifications.ts (1 hunks)
  • server/routers/tracking.ts (1 hunks)
  • server/services/verification.ts (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • app/dashboard/@settings/[...catchAll]/page.tsx
  • app/@auth/default.tsx
  • drizzle/meta/0004_snapshot.json
🚧 Files skipped from review as they are similar to previous changes (11)
  • lib/constants/index.ts
  • app/dashboard/page.tsx
  • app/dashboard/layout.tsx
  • lib/db/repos/tracked-domains.test.ts
  • app/dashboard/settings/page.tsx
  • drizzle/meta/_journal.json
  • lib/constants/tier-limits.ts
  • lib/resend.ts
  • components/dashboard/tracked-domain-card.tsx
  • app/login/page.tsx
  • components/auth/user-menu.tsx
🧰 Additional context used
📓 Path-based instructions (13)
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/dashboard/tracked-domains-table.tsx
  • components/dashboard/verification-badge.test.tsx
  • components/auth/sign-in-button.tsx
  • components/dashboard/add-domain-dialog.tsx
  • components/dashboard/settings-content.tsx
  • components/dashboard/dashboard-header.tsx
  • components/auth/login-content.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • components/dashboard/tracked-domains-table.tsx
  • lib/schemas/internal/notifications.ts
  • components/dashboard/verification-badge.test.tsx
  • lib/schemas/index.ts
  • lib/db/repos/certificates.ts
  • lib/inngest/functions/check-certificate-expiry.ts
  • app/@auth/[...catchAll]/page.tsx
  • components/auth/sign-in-button.tsx
  • app/@auth/(.)login/page.tsx
  • components/dashboard/add-domain-dialog.tsx
  • server/routers/tracking.ts
  • components/dashboard/settings-content.tsx
  • lib/db/repos/user-notification-preferences.ts
  • app/layout.tsx
  • lib/constants/notifications.ts
  • components/dashboard/dashboard-header.tsx
  • lib/inngest/functions/check-domain-expiry.ts
  • app/api/inngest/route.ts
  • app/dashboard/@settings/(.)settings/page.tsx
  • lib/db/repos/user-limits.ts
  • emails/certificate-expiry.tsx
  • app/dashboard/@settings/default.tsx
  • lib/inngest/functions/reverify-domains.ts
  • components/auth/login-content.tsx
  • lib/db/repos/notifications.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/schema.ts
  • server/services/verification.ts
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • components/dashboard/tracked-domains-table.tsx
  • lib/schemas/internal/notifications.ts
  • components/dashboard/verification-badge.test.tsx
  • lib/schemas/index.ts
  • lib/db/repos/certificates.ts
  • lib/inngest/functions/check-certificate-expiry.ts
  • app/@auth/[...catchAll]/page.tsx
  • components/auth/sign-in-button.tsx
  • app/@auth/(.)login/page.tsx
  • components/dashboard/add-domain-dialog.tsx
  • server/routers/tracking.ts
  • components/dashboard/settings-content.tsx
  • lib/db/repos/user-notification-preferences.ts
  • app/layout.tsx
  • lib/constants/notifications.ts
  • components/dashboard/dashboard-header.tsx
  • lib/inngest/functions/check-domain-expiry.ts
  • app/api/inngest/route.ts
  • app/dashboard/@settings/(.)settings/page.tsx
  • lib/db/repos/user-limits.ts
  • emails/certificate-expiry.tsx
  • app/dashboard/@settings/default.tsx
  • lib/inngest/functions/reverify-domains.ts
  • components/auth/login-content.tsx
  • lib/db/repos/notifications.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/schema.ts
  • server/services/verification.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx}: Use Vitest with React Testing Library; configure in vitest.config.ts with threads pool for sandboxed environment compatibility
Mock @vercel/blob (put and del functions) in tests; set BLOB_READ_WRITE_TOKEN via vi.stubEnv in suites that touch uploads/deletes
Mock URL.createObjectURL/revokeObjectURL with vi.fn() in tests that need them
Logger is mocked in tests via vitest.setup.ts; use vi.mocked(logger.info) to assert log calls in tests

Files:

  • components/dashboard/verification-badge.test.tsx
components/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.test.{ts,tsx}: Do not add direct tests for components/ui/* (shadcn components); mock Radix primitives (Accordion, Tooltip) when testing domain sections
Mock tRPC/React Query for components like Favicon and Screenshot in tests

Files:

  • components/dashboard/verification-badge.test.tsx
lib/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

lib/db/**/*.{ts,tsx}: Drizzle ORM schema, migrations, and repository layer should be organized in lib/db/ with repos in lib/db/repos/ for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)
Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Files:

  • lib/db/repos/certificates.ts
  • lib/db/repos/user-notification-preferences.ts
  • lib/db/repos/user-limits.ts
  • lib/db/repos/notifications.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/schema.ts
lib/inngest/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Inngest client and functions for event-driven background section revalidation should be organized in lib/inngest/ with proper structure

Files:

  • lib/inngest/functions/check-certificate-expiry.ts
  • lib/inngest/functions/check-domain-expiry.ts
  • lib/inngest/functions/reverify-domains.ts
app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

app/**/*.tsx: Next.js App Router with default server components; keep app/page.tsx and app/api/* thin and delegate to server/ or lib/
Client components must begin with "use client" directive

Files:

  • app/@auth/[...catchAll]/page.tsx
  • app/@auth/(.)login/page.tsx
  • app/layout.tsx
  • app/dashboard/@settings/(.)settings/page.tsx
  • app/dashboard/@settings/default.tsx
{app/**,server/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use distinct ID sourced from PostHog cookie via cache()-wrapped getDistinctId() to comply with Next.js restrictions

Files:

  • app/@auth/[...catchAll]/page.tsx
  • app/@auth/(.)login/page.tsx
  • server/routers/tracking.ts
  • app/layout.tsx
  • app/api/inngest/route.ts
  • app/dashboard/@settings/(.)settings/page.tsx
  • app/dashboard/@settings/default.tsx
  • server/services/verification.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Isolate DNS, RDAP/WHOIS, TLS, and header probing services in server/services/ with tRPC routers in server/routers/

Files:

  • server/routers/tracking.ts
  • server/services/verification.ts
{server/**,app/api/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

{server/**,app/api/**}/*.{ts,tsx}: Use drizzle-zod for DB boundary validation with *Select schemas from lib/db/zod.ts for reads and *Insert/*Update for writes, with dates coerced in write schemas
Server-side logging: import singleton import { logger } from "@/lib/logger/server" or create service logger with createLogger({ source: "..." })
Server logging automatically injects OpenTelemetry trace/span IDs and correlation IDs via AsyncLocalStorage for request tracing
Server logging automatically tracks critical errors in PostHog via after()
Server logging supports levels: trace, debug, info, warn, error, fatal

Files:

  • server/routers/tracking.ts
  • app/api/inngest/route.ts
  • server/services/verification.ts
server/routers/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Review trpc/init.ts when extending procedures to ensure auth/context remain intact

Files:

  • server/routers/tracking.ts
lib/constants/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Organize constants by domain in lib/constants/ submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via lib/constants/index.ts

Files:

  • lib/constants/notifications.ts
🧠 Learnings (29)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Applied to files:

  • lib/schemas/internal/notifications.ts
  • lib/db/repos/certificates.ts
  • lib/db/repos/notifications.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/schema.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Use `drizzle-zod` for DB boundary validation with *Select schemas from `lib/db/zod.ts` for reads and *Insert/*Update for writes, with dates coerced in write schemas

Applied to files:

  • lib/schemas/internal/notifications.ts
  • lib/db/repos/certificates.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.test.{ts,tsx} : Do not add direct tests for `components/ui/*` (shadcn components); mock Radix primitives (Accordion, Tooltip) when testing domain sections

Applied to files:

  • components/dashboard/verification-badge.test.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.test.{ts,tsx} : Use **Vitest** with React Testing Library; configure in `vitest.config.ts` with `threads` pool for sandboxed environment compatibility

Applied to files:

  • components/dashboard/verification-badge.test.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.test.{ts,tsx} : Mock tRPC/React Query for components like `Favicon` and `Screenshot` in tests

Applied to files:

  • components/dashboard/verification-badge.test.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/constants/**/*.{ts,tsx} : Organize constants by domain in `lib/constants/` submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via `lib/constants/index.ts`

Applied to files:

  • lib/schemas/index.ts
  • lib/constants/notifications.ts
  • lib/db/repos/user-limits.ts
  • AGENTS.md
  • lib/inngest/functions/reverify-domains.ts
  • lib/db/repos/tracked-domains.ts
  • server/services/verification.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Drizzle ORM schema, migrations, and repository layer should be organized in `lib/db/` with repos in `lib/db/repos/` for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)

Applied to files:

  • lib/db/repos/certificates.ts
  • lib/db/repos/user-notification-preferences.ts
  • lib/db/repos/user-limits.ts
  • AGENTS.md
  • lib/db/repos/notifications.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/schema.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to app/**/*.tsx : Next.js App Router with default server components; keep `app/page.tsx` and `app/api/*` thin and delegate to `server/` or `lib/`

Applied to files:

  • app/@auth/[...catchAll]/page.tsx
  • app/@auth/(.)login/page.tsx
  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {app/**,server/**}/*.{ts,tsx} : Use distinct ID sourced from PostHog cookie via `cache()`-wrapped `getDistinctId()` to comply with Next.js restrictions

Applied to files:

  • app/@auth/[...catchAll]/page.tsx
  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.{ts,tsx} : Client-side logging: import singleton `import { logger } from "@/lib/logger/client"` or use hook `useLogger({ component: "..." })`

Applied to files:

  • components/auth/sign-in-button.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.{ts,tsx} : Client logging errors automatically tracked in PostHog with console output in development and always for errors

Applied to files:

  • components/auth/sign-in-button.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Server logging automatically tracks critical errors in PostHog via `after()`

Applied to files:

  • components/auth/sign-in-button.tsx
  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Server-side logging: import singleton `import { logger } from "@/lib/logger/server"` or create service logger with `createLogger({ source: "..." })`

Applied to files:

  • components/auth/sign-in-button.tsx
  • AGENTS.md
  • lib/db/repos/notifications.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to app/**/*.tsx : Client components must begin with `"use client"` directive

Applied to files:

  • app/@auth/(.)login/page.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/**/*.{ts,tsx} : Isolate DNS, RDAP/WHOIS, TLS, and header probing services in `server/services/` with tRPC routers in `server/routers/`

Applied to files:

  • server/routers/tracking.ts
  • AGENTS.md
  • server/services/verification.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/routers/**/*.{ts,tsx} : Review `trpc/init.ts` when extending procedures to ensure auth/context remain intact

Applied to files:

  • server/routers/tracking.ts
  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use Vercel Blob for favicon/screenshot storage with automatic public URLs; metadata cached in Postgres

Applied to files:

  • README.md
  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use Vercel Edge Config for dynamic, low-latency configuration (e.g., `domain_suggestions` array) without redeployment

Applied to files:

  • README.md
  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/inngest/**/*.{ts,tsx} : Inngest client and functions for event-driven background section revalidation should be organized in `lib/inngest/` with proper structure

Applied to files:

  • app/api/inngest/route.ts
  • AGENTS.md
  • lib/inngest/functions/reverify-domains.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use event-driven background revalidation via Inngest functions in `lib/inngest/functions/` with built-in concurrency control

Applied to files:

  • app/api/inngest/route.ts
  • lib/inngest/functions/reverify-domains.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.{ts,tsx} : TypeScript only with `strict` enabled; prefer small, pure modules (≈≤300 LOC)

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to vitest.setup.ts : Global test setup in `vitest.setup.ts` mocks analytics clients/servers (`@/lib/analytics/server` and `@/lib/analytics/client`), logger clients/servers (`@/lib/logger/server` and `@/lib/logger/client`), and `server-only` module

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Keep secrets in `.env.local`; see `.env.example` for required variables

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/services/screenshot.ts : Prefer `puppeteer-core` + `sparticuz/chromium` on Vercel for screenshots

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/analytics/server.ts : Server-side analytics captured via `posthog-node` in `lib/analytics/server.ts` using `analytics.track()` and `analytics.trackException()` with Next.js 16 `after()` for background event capture

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use Next.js 16 `after()` for fire-and-forget background operations (analytics, domain access tracking) with graceful degradation

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to instrumentation-client.ts : Client-side analytics captured via `posthog-js` initialized in `instrumentation-client.ts`

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use **PostHog** for analytics and error tracking with reverse proxy via `/_proxy/ingest/*`

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Server logging automatically injects OpenTelemetry trace/span IDs and correlation IDs via AsyncLocalStorage for request tracing

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (18)
lib/schemas/internal/notifications.ts (1)
lib/db/repos/user-notification-preferences.ts (1)
  • UserNotificationPreferences (12-12)
components/dashboard/verification-badge.test.tsx (1)
components/dashboard/verification-badge.tsx (1)
  • VerificationBadge (14-60)
lib/db/repos/certificates.ts (2)
lib/schemas/internal/notifications.ts (1)
  • NotificationOverrides (28-28)
lib/db/schema.ts (4)
  • trackedDomains (150-188)
  • domains (280-300)
  • certificates (379-406)
  • users (68-80)
lib/inngest/functions/check-certificate-expiry.ts (8)
lib/constants/notifications.ts (4)
  • getCertificateExpiryNotificationType (89-98)
  • NotificationType (43-47)
  • CERTIFICATE_EXPIRY_THRESHOLDS (36-36)
  • CERTIFICATE_THRESHOLD_TO_TYPE (60-68)
lib/db/repos/notifications.ts (4)
  • NotificationType (12-12)
  • hasNotificationBeenSent (79-95)
  • createNotification (22-44)
  • updateNotificationResendId (50-74)
lib/inngest/client.ts (1)
  • inngest (10-13)
lib/db/repos/certificates.ts (1)
  • getVerifiedTrackedDomainsCertificates (61-95)
lib/db/repos/user-notification-preferences.ts (1)
  • getOrCreateUserNotificationPreferences (17-52)
lib/resend.ts (2)
  • resend (14-14)
  • RESEND_FROM_EMAIL (16-17)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
emails/certificate-expiry.tsx (1)
  • CertificateExpiryEmail (24-117)
components/dashboard/settings-content.tsx (4)
lib/constants/notifications.ts (3)
  • NotificationCategory (13-13)
  • NOTIFICATION_CATEGORIES (7-11)
  • NOTIFICATION_CATEGORY_INFO (16-32)
lib/utils.ts (1)
  • cn (4-6)
lib/schemas/internal/notifications.ts (1)
  • NotificationOverrides (28-28)
components/domain/favicon.tsx (1)
  • Favicon (35-135)
lib/db/repos/user-notification-preferences.ts (2)
lib/db/client.ts (1)
  • db (27-27)
lib/db/schema.ts (1)
  • userNotificationPreferences (228-245)
app/layout.tsx (1)
lib/auth.ts (1)
  • auth (8-40)
lib/constants/notifications.ts (1)
lib/db/repos/notifications.ts (1)
  • NotificationType (12-12)
components/dashboard/dashboard-header.tsx (4)
hooks/use-view-preference.ts (1)
  • ViewMode (5-5)
components/ui/progress.tsx (1)
  • Progress (31-31)
components/ui/toggle-group.tsx (2)
  • ToggleGroup (72-72)
  • ToggleGroupItem (72-72)
components/ui/button.tsx (1)
  • Button (60-60)
lib/inngest/functions/check-domain-expiry.ts (9)
lib/constants/notifications.ts (2)
  • NotificationType (43-47)
  • getDomainExpiryNotificationType (74-83)
lib/db/repos/notifications.ts (4)
  • NotificationType (12-12)
  • hasNotificationBeenSent (79-95)
  • createNotification (22-44)
  • updateNotificationResendId (50-74)
lib/inngest/client.ts (1)
  • inngest (10-13)
lib/db/schema.ts (1)
  • trackedDomains (150-188)
lib/db/repos/tracked-domains.ts (1)
  • getVerifiedTrackedDomainsWithExpiry (378-399)
lib/db/repos/user-notification-preferences.ts (1)
  • getOrCreateUserNotificationPreferences (17-52)
lib/resend.ts (2)
  • resend (14-14)
  • RESEND_FROM_EMAIL (16-17)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
emails/domain-expiry.tsx (1)
  • DomainExpiryEmail (23-97)
lib/db/repos/user-limits.ts (3)
lib/db/schema.ts (2)
  • userTier (61-61)
  • userLimits (212-225)
lib/db/client.ts (1)
  • db (27-27)
lib/edge-config.ts (1)
  • getMaxDomainsForTier (111-116)
emails/certificate-expiry.tsx (1)
components/ui/button.tsx (1)
  • Button (60-60)
app/dashboard/@settings/default.tsx (1)
app/@auth/default.tsx (1)
  • Default (1-3)
components/auth/login-content.tsx (3)
components/ui/card.tsx (6)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardDescription (90-90)
  • CardContent (91-91)
  • CardFooter (87-87)
  • Card (85-85)
components/logo.tsx (1)
  • Logo (5-19)
components/auth/sign-in-button.tsx (1)
  • SignInButton (10-47)
lib/db/repos/notifications.ts (3)
lib/constants/notifications.ts (1)
  • NotificationType (43-47)
lib/db/client.ts (1)
  • db (27-27)
lib/db/schema.ts (1)
  • notifications (191-207)
lib/db/repos/tracked-domains.ts (3)
lib/db/schema.ts (8)
  • verificationMethod (51-55)
  • verificationStatus (56-60)
  • trackedDomains (150-188)
  • domains (280-300)
  • providers (252-277)
  • registrations (303-348)
  • hosting (427-454)
  • users (68-80)
lib/schemas/internal/notifications.ts (1)
  • NotificationOverrides (28-28)
lib/db/client.ts (1)
  • db (27-27)
lib/db/schema.ts (2)
trpc/init.ts (1)
  • t (70-75)
lib/schemas/internal/notifications.ts (1)
  • NotificationOverrides (28-28)
server/services/verification.ts (2)
lib/db/repos/tracked-domains.ts (1)
  • VerificationMethod (21-21)
lib/fetch.ts (1)
  • fetchWithTimeoutAndRetry (10-55)
🪛 ast-grep (0.40.0)
server/services/verification.ts

[warning] 230-233: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
<meta[^>]*name=["']${META_TAG_NAME}["'][^>]*content=["']([^"']+)["'][^>]*/?>|<meta[^>]*content=["']([^"']+)["'][^>]*name=["']${META_TAG_NAME}["'][^>]*/?>,
"i",
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 LanguageTool
AGENTS.md

[grammar] ~122-~122: Ensure spelling is correct
Context: ...(). ### Re-verification & Grace Period - Inngest function reverifyDomains` runs daily a...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~145-~145: Ensure spelling is correct
Context: ...nOverrides: Per-domain overrides. ### Inngest Background Jobs - check-domain-expiry`...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (32)
app/dashboard/@settings/default.tsx (1)

1-3: LGTM! Standard Next.js parallel route fallback.

This correctly implements a default.tsx fallback for the @settings parallel route slot. Returning null is appropriate when the settings route is not active.

app/layout.tsx (1)

26-32: Auth slot prop addition looks correct and type-safe

Extending RootLayout to accept an auth: React.ReactNode prop via Readonly<{ children: React.ReactNode; auth: React.ReactNode; }> is consistent with slot-style/parallel route usage and keeps the public layout signature explicit. No issues here.

app/@auth/[...catchAll]/page.tsx (1)

1-3: Null catch‑all route looks appropriate

Using a no‑op component here is fine for the intercepted auth parallel route and keeps the segment lightweight.

app/dashboard/@settings/(.)settings/page.tsx (1)

13-27: Modal wiring and composition look good

The intercepted modal wiring and reuse of SettingsContent with showCard={false} are consistent and keep this route thin, as desired.

components/auth/login-content.tsx (1)

14-49: Reusable login content with showCard toggle looks solid

Good use of a shared content fragment and the showCard prop to support both full-page and modal contexts without duplication.

components/dashboard/verification-badge.test.tsx (1)

1-82: Test coverage for badge states looks comprehensive

The suite exercises all key combinations of verified, verificationStatus, and className, and matches the component’s intended behavior, so this looks in good shape.

lib/schemas/index.ts (1)

10-10: Notifications schemas re‑export fits existing barrel pattern

Re‑exporting ./internal/notifications here keeps consumer imports (@/lib/schemas) consistent with other schema modules and matches the existing barrel style.

components/auth/sign-in-button.tsx (1)

13-24: No changes required—logger usage is correct

The current logger.error() call matches the intended API. The logger supports both 2-argument error(message, context) and 3-argument error(message, error, context) patterns (documented overloads in lib/logger/client.ts lines 162–163). The code correctly uses the 3-argument form:

logger.error("GitHub sign-in failed", err, { provider: "github" });

This is the standard pattern used throughout the codebase (e.g., server/services/verification.ts, trpc/init.ts) and is explicitly shown in the logger's documentation example at line 192.

Likely an incorrect or invalid review comment.

app/api/inngest/route.ts (1)

3-15: LGTM!

The new Inngest functions are properly imported and registered with the serve() function. The organization follows the established pattern with functions in lib/inngest/functions/. Based on learnings, this aligns with the expected Inngest structure.

README.md (1)

12-28: LGTM!

The README updates accurately document the new features (domain tracking, GitHub auth, email notifications) and the expanded tech stack. The documentation is comprehensive and aligns with the implementation.

lib/inngest/functions/check-domain-expiry.ts (2)

40-146: LGTM! Previous review concerns have been addressed.

The implementation properly handles:

  • Incrementing results.skipped when domains are outside notification thresholds (lines 80-83)
  • Using an idempotency key with Resend to prevent duplicate emails on retry (lines 170-175, 223-226)
  • Creating the notification record before sending to act as a lock (lines 184-198)

The overall structure follows the Inngest patterns and logging guidelines.


148-168: LGTM!

The sendExpiryNotification helper is well-structured with proper null checks for the Resend client and clear documentation. The idempotency approach with record-before-send is a solid pattern for preventing duplicate emails.

components/dashboard/dashboard-header.tsx (2)

31-33: Previous review concern addressed.

The greeting now safely handles falsy userName values with a conditional check before attempting to split, preventing potential runtime errors.


44-58: LGTM!

The view toggle implementation is clean with proper ARIA labels for accessibility. The if (value) guard correctly handles the case when the user clicks the already-selected item (which returns empty string in single-select mode).

lib/inngest/functions/check-certificate-expiry.ts (2)

51-160: LGTM!

The main function follows the same robust pattern as checkDomainExpiry:

  • Proper concurrency limiting and retry configuration
  • Consistent skip counting for all exit paths
  • Step-based execution for durability and retry safety

The 15-minute offset from domain expiry check (9:15 vs 9:00 UTC) is a nice touch to spread load.


162-265: LGTM!

The notification helper properly implements the idempotency pattern with:

  • Record creation before email send
  • Resend idempotency key for duplicate prevention
  • Error propagation for Inngest retry handling
  • Resend ID storage for troubleshooting
components/dashboard/tracked-domains-table.tsx (1)

1-345: LGTM!

Well-structured table component following project conventions. Good use of memoized columns, proper sorting state management, and clean helper components. The file naming and export conventions are correct per coding guidelines.

lib/db/repos/user-limits.ts (2)

106-138: Past review concerns have been addressed.

The function now correctly handles the case where no user limits record exists by using a transaction with an insert fallback. The previous issue of silently returning undefined is resolved.


145-185: Past review concerns have been addressed.

Same improvement as updateUserTier - now uses a transaction pattern with insert fallback to ensure the function never silently fails.

lib/schemas/internal/notifications.ts (1)

1-39: LGTM!

Well-organized Zod schemas with clear documentation. The separation between required global preferences and optional per-domain overrides follows good design patterns. As per coding guidelines, these schemas can be reused for JSON column validation.

emails/certificate-expiry.tsx (1)

1-238: LGTM!

Well-implemented email template with appropriate urgency handling, Let's Encrypt-aware messaging, and consistent styling. The PreviewProps enable easy development testing.

lib/inngest/functions/reverify-domains.ts (1)

67-78: LGTM on the Inngest function configuration.

Good use of concurrency limit to ensure only one instance runs at a time, and the retry count of 3 is appropriate for a cron job. The function structure with step.run for each domain operation enables proper checkpointing and resumability.

drizzle/0004_talented_the_call.sql (3)

1-3: LGTM!

The enum definitions are appropriate for the domain verification and user tier system.


94-107: LGTM!

Foreign key constraints with ON DELETE cascade are appropriate for this user-centric data model, and the indexes are well-placed for common query patterns (user lookups, domain tracking queries, and status filtering).


4-18: The password field in the accounts table is part of the better-auth library's standard schema and is intentional, not a security concern. Better-auth supports multiple authentication methods (OAuth and credential-based) for the same provider connection, which is why both OAuth tokens and a password field coexist in this table. The password field is nullable and managed securely by the better-auth framework. No changes needed.

lib/db/schema.ts (6)

16-26: LGTM!

The import of NotificationOverrides follows the coding guideline to reuse domain Zod types for JSON columns, preventing drift between database and validation schemas.


51-61: LGTM!

The enum definitions correctly match the SQL migration and provide appropriate types for domain verification and user tier management.


136-143: LGTM!

The verifications table follows standard patterns for email verification token management.


172-175: LGTM!

The notificationOverrides field correctly uses the imported NotificationOverrides type with $type<>(), following the coding guideline to reuse domain Zod types for JSON columns.

Based on learnings, this pattern prevents drift between database and validation schemas.


212-225: LGTM!

The userLimits table design is flexible and well-documented. Using Edge Config for tier limits with optional per-user overrides is a good pattern for handling special cases like beta testers without hardcoding limits.


228-245: LGTM!

The userNotificationPreferences table provides sensible defaults (all notification types enabled) and clear structure for global user preferences.

lib/constants/notifications.ts (1)

1-68: LGTM! Well-structured notification constants.

The constant definitions, types, and mappings are clean and type-safe. The use of template literal types for NotificationType is elegant, and the metadata structure will work well for UI rendering.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
components/auth/user-menu.tsx (1)

36-42: Add defensive checks for user.name.

This issue was previously identified: the code will throw if user.name is undefined, null, or contains empty string parts after splitting.

Apply the suggested fix from the previous review to add null safety.

lib/inngest/functions/reverify-domains.ts (2)

38-41: Excellent fix: Action-based counter increment now works correctly.

The previous issue with the revoked counter has been resolved. The code now:

  1. Defines a clear VerificationFailureAction type
  2. Returns the specific action from handleVerificationFailure
  3. Increments the correct counter (revoked or failing) based on the returned action

This ensures accurate observability metrics in the job results.

Also applies to: 186-199


333-344: Excellent fix: PII no longer logged.

The previous issue with logging user email addresses (PII) has been resolved. The code now consistently logs userId instead of userEmail in both the failing and revoked email notification handlers, which aligns with privacy best practices and compliance requirements.

Also applies to: 410-422

🧹 Nitpick comments (5)
app/login/page.tsx (1)

1-15: Minor Next.js typings and headers() usage nit

headers() from next/headers is synchronous, so the await is unnecessary, and the metadata export can benefit from an explicit Metadata type for better TS safety.

-import { headers } from "next/headers";
+import { headers } from "next/headers";
+import type { Metadata } from "next";
@@
-export const metadata = {
+export const metadata: Metadata = {
   title: "Sign In | Domainstack",
   description: "Sign in to track your domains and receive expiration alerts.",
 };
@@
-  const session = await auth.api.getSession({
-    headers: await headers(),
-  });
+  const session = await auth.api.getSession({
+    headers: headers(),
+  });

Please double-check against your Next.js and better-auth versions to ensure these typings and call shapes match your installed packages.

app/dashboard/layout.tsx (1)

15-22: Server-side auth check is correctly implemented.

The authentication check using auth.api.getSession with awaited headers is the correct pattern for Next.js 15+ server components. The redirect logic appropriately handles unauthenticated users.

Consider adding error handling to gracefully manage potential auth failures:

-  // Server-side auth check
-  const session = await auth.api.getSession({
-    headers: await headers(),
-  });
+  // Server-side auth check
+  let session;
+  try {
+    session = await auth.api.getSession({
+      headers: await headers(),
+    });
+  } catch (error) {
+    console.error("Auth check failed:", error);
+    redirect("/login");
+  }

This provides more explicit error handling, though Next.js error boundaries will catch unhandled exceptions at a higher level.

components/dashboard/settings-content.tsx (1)

428-431: Remove unused variable.

_effectiveValue is declared but never used in the mobile view. It's computed again as effectiveValue in ThreeStateCheckbox when needed.

-          const override = overrides[category];
-          const globalValue = globalPrefs[category];
-          const _effectiveValue = override ?? globalValue;
-          const isInherited = override === undefined;
+          const override = overrides[category];
+          const globalValue = globalPrefs[category];
+          const isInherited = override === undefined;
server/routers/tracking.ts (1)

43-54: Consider extracting validation to a separate step.

Throwing a TRPCError inside a Zod transform works but mixes validation concerns. If the domain validation fails, the error appears as a schema validation failure rather than a procedure error, which may affect error handling/logging.

This is a minor style consideration - the current approach is functional.

lib/db/repos/tracked-domains.test.ts (1)

461-570: Notification override tests cover merge semantics well.

The merge behavior test (lines 480-503) correctly validates that existing overrides are preserved when adding new ones. The reset functionality is also properly tested.

Consider adding a test case for getVerifiedTrackedDomainsWithExpiry that includes registration data to verify the full return structure when expiration dates are present.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d1852c and 1b07cc5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (32)
  • app/(static)/layout.tsx (1 hunks)
  • app/(static)/privacy/page.tsx (1 hunks)
  • app/(static)/terms/page.tsx (1 hunks)
  • app/dashboard/layout.tsx (1 hunks)
  • app/layout.tsx (1 hunks)
  • app/login/page.tsx (1 hunks)
  • components/auth/auth-button.tsx (1 hunks)
  • components/auth/login-content.tsx (1 hunks)
  • components/auth/sign-in-button.tsx (1 hunks)
  • components/auth/user-menu.tsx (1 hunks)
  • components/dashboard/settings-content.tsx (1 hunks)
  • components/layout/app-footer.tsx (2 hunks)
  • emails/certificate-expiry.tsx (1 hunks)
  • emails/domain-expiry.tsx (1 hunks)
  • emails/verification-failing.tsx (1 hunks)
  • emails/verification-revoked.tsx (1 hunks)
  • lib/constants/notifications.test.ts (1 hunks)
  • lib/constants/notifications.ts (1 hunks)
  • lib/db/repos/certificates.ts (2 hunks)
  • lib/db/repos/notifications.test.ts (1 hunks)
  • lib/db/repos/notifications.ts (1 hunks)
  • lib/db/repos/tracked-domains.test.ts (1 hunks)
  • lib/db/repos/tracked-domains.ts (1 hunks)
  • lib/db/repos/user-notification-preferences.test.ts (1 hunks)
  • lib/db/schema.ts (2 hunks)
  • lib/inngest/functions/check-certificate-expiry.ts (1 hunks)
  • lib/inngest/functions/check-domain-expiry.ts (1 hunks)
  • lib/inngest/functions/reverify-domains.ts (1 hunks)
  • lib/notifications.test.ts (1 hunks)
  • lib/notifications.ts (1 hunks)
  • package.json (5 hunks)
  • server/routers/tracking.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • components/auth/sign-in-button.tsx
  • components/auth/login-content.tsx
  • package.json
  • emails/verification-failing.tsx
  • emails/certificate-expiry.tsx
🧰 Additional context used
📓 Path-based instructions (13)
lib/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

lib/db/**/*.{ts,tsx}: Drizzle ORM schema, migrations, and repository layer should be organized in lib/db/ with repos in lib/db/repos/ for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)
Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/db/repos/certificates.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/repos/tracked-domains.test.ts
  • lib/db/repos/notifications.ts
  • lib/db/repos/notifications.test.ts
  • lib/db/schema.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/notifications.test.ts
  • app/(static)/terms/page.tsx
  • lib/inngest/functions/check-domain-expiry.ts
  • lib/inngest/functions/check-certificate-expiry.ts
  • lib/notifications.ts
  • lib/inngest/functions/reverify-domains.ts
  • app/login/page.tsx
  • emails/domain-expiry.tsx
  • app/dashboard/layout.tsx
  • lib/constants/notifications.test.ts
  • components/auth/auth-button.tsx
  • server/routers/tracking.ts
  • components/layout/app-footer.tsx
  • lib/db/repos/certificates.ts
  • app/(static)/layout.tsx
  • components/dashboard/settings-content.tsx
  • app/(static)/privacy/page.tsx
  • lib/constants/notifications.ts
  • lib/db/repos/tracked-domains.ts
  • emails/verification-revoked.tsx
  • lib/db/repos/tracked-domains.test.ts
  • lib/db/repos/notifications.ts
  • components/auth/user-menu.tsx
  • app/layout.tsx
  • lib/db/repos/notifications.test.ts
  • lib/db/schema.ts
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/notifications.test.ts
  • app/(static)/terms/page.tsx
  • lib/inngest/functions/check-domain-expiry.ts
  • lib/inngest/functions/check-certificate-expiry.ts
  • lib/notifications.ts
  • lib/inngest/functions/reverify-domains.ts
  • app/login/page.tsx
  • emails/domain-expiry.tsx
  • app/dashboard/layout.tsx
  • lib/constants/notifications.test.ts
  • components/auth/auth-button.tsx
  • server/routers/tracking.ts
  • components/layout/app-footer.tsx
  • lib/db/repos/certificates.ts
  • app/(static)/layout.tsx
  • components/dashboard/settings-content.tsx
  • app/(static)/privacy/page.tsx
  • lib/constants/notifications.ts
  • lib/db/repos/tracked-domains.ts
  • emails/verification-revoked.tsx
  • lib/db/repos/tracked-domains.test.ts
  • lib/db/repos/notifications.ts
  • components/auth/user-menu.tsx
  • app/layout.tsx
  • lib/db/repos/notifications.test.ts
  • lib/db/schema.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx}: Use Vitest with React Testing Library; configure in vitest.config.ts with threads pool for sandboxed environment compatibility
Mock @vercel/blob (put and del functions) in tests; set BLOB_READ_WRITE_TOKEN via vi.stubEnv in suites that touch uploads/deletes
Mock URL.createObjectURL/revokeObjectURL with vi.fn() in tests that need them
Logger is mocked in tests via vitest.setup.ts; use vi.mocked(logger.info) to assert log calls in tests

Files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/notifications.test.ts
  • lib/constants/notifications.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • lib/db/repos/notifications.test.ts
lib/db/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Replace @/lib/db/client with a PGlite-backed instance (@/lib/db/pglite) for database tests

Files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • lib/db/repos/notifications.test.ts
app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

app/**/*.tsx: Next.js App Router with default server components; keep app/page.tsx and app/api/* thin and delegate to server/ or lib/
Client components must begin with "use client" directive

Files:

  • app/(static)/terms/page.tsx
  • app/login/page.tsx
  • app/dashboard/layout.tsx
  • app/(static)/layout.tsx
  • app/(static)/privacy/page.tsx
  • app/layout.tsx
{app/**,server/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use distinct ID sourced from PostHog cookie via cache()-wrapped getDistinctId() to comply with Next.js restrictions

Files:

  • app/(static)/terms/page.tsx
  • app/login/page.tsx
  • app/dashboard/layout.tsx
  • server/routers/tracking.ts
  • app/(static)/layout.tsx
  • app/(static)/privacy/page.tsx
  • app/layout.tsx
lib/inngest/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Inngest client and functions for event-driven background section revalidation should be organized in lib/inngest/ with proper structure

Files:

  • lib/inngest/functions/check-domain-expiry.ts
  • lib/inngest/functions/check-certificate-expiry.ts
  • lib/inngest/functions/reverify-domains.ts
lib/constants/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Organize constants by domain in lib/constants/ submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via lib/constants/index.ts

Files:

  • lib/constants/notifications.test.ts
  • lib/constants/notifications.ts
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/auth/auth-button.tsx
  • components/layout/app-footer.tsx
  • components/dashboard/settings-content.tsx
  • components/auth/user-menu.tsx
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Isolate DNS, RDAP/WHOIS, TLS, and header probing services in server/services/ with tRPC routers in server/routers/

Files:

  • server/routers/tracking.ts
{server/**,app/api/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

{server/**,app/api/**}/*.{ts,tsx}: Use drizzle-zod for DB boundary validation with *Select schemas from lib/db/zod.ts for reads and *Insert/*Update for writes, with dates coerced in write schemas
Server-side logging: import singleton import { logger } from "@/lib/logger/server" or create service logger with createLogger({ source: "..." })
Server logging automatically injects OpenTelemetry trace/span IDs and correlation IDs via AsyncLocalStorage for request tracing
Server logging automatically tracks critical errors in PostHog via after()
Server logging supports levels: trace, debug, info, warn, error, fatal

Files:

  • server/routers/tracking.ts
server/routers/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Review trpc/init.ts when extending procedures to ensure auth/context remain intact

Files:

  • server/routers/tracking.ts
🧠 Learnings (19)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.test.{ts,tsx} : Replace `@/lib/db/client` with a PGlite-backed instance (`@/lib/db/pglite`) for database tests

Applied to files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • lib/db/repos/notifications.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to vitest.setup.ts : Global test setup in `vitest.setup.ts` mocks analytics clients/servers (`@/lib/analytics/server` and `@/lib/analytics/client`), logger clients/servers (`@/lib/logger/server` and `@/lib/logger/client`), and `server-only` module

Applied to files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/notifications.test.ts
  • lib/constants/notifications.test.ts
  • lib/db/repos/tracked-domains.test.ts
  • lib/db/repos/notifications.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/**/*.test.{ts,tsx} : Prefer `vi.hoisted` for ESM module mocks in server tests (e.g., `node:tls`)

Applied to files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/db/repos/tracked-domains.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.test.{ts,tsx} : Logger is mocked in tests via `vitest.setup.ts`; use `vi.mocked(logger.info)` to assert log calls in tests

Applied to files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/db/repos/notifications.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.test.{ts,tsx} : Use **Vitest** with React Testing Library; configure in `vitest.config.ts` with `threads` pool for sandboxed environment compatibility

Applied to files:

  • lib/db/repos/user-notification-preferences.test.ts
  • lib/db/repos/tracked-domains.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.test.{ts,tsx} : Mock `vercel/blob` (`put` and `del` functions) in tests; set `BLOB_READ_WRITE_TOKEN` via `vi.stubEnv` in suites that touch uploads/deletes

Applied to files:

  • lib/db/repos/user-notification-preferences.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.test.{ts,tsx} : Mock tRPC/React Query for components like `Favicon` and `Screenshot` in tests

Applied to files:

  • lib/db/repos/user-notification-preferences.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to app/**/*.tsx : Next.js App Router with default server components; keep `app/page.tsx` and `app/api/*` thin and delegate to `server/` or `lib/`

Applied to files:

  • app/(static)/terms/page.tsx
  • app/login/page.tsx
  • app/(static)/privacy/page.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/constants/**/*.{ts,tsx} : Organize constants by domain in `lib/constants/` submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via `lib/constants/index.ts`

Applied to files:

  • lib/notifications.ts
  • lib/inngest/functions/reverify-domains.ts
  • emails/domain-expiry.tsx
  • lib/constants/notifications.test.ts
  • lib/db/repos/certificates.ts
  • lib/constants/notifications.ts
  • lib/db/repos/tracked-domains.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/inngest/**/*.{ts,tsx} : Inngest client and functions for event-driven background section revalidation should be organized in `lib/inngest/` with proper structure

Applied to files:

  • lib/inngest/functions/reverify-domains.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use event-driven background revalidation via Inngest functions in `lib/inngest/functions/` with built-in concurrency control

Applied to files:

  • lib/inngest/functions/reverify-domains.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {app/**,server/**}/*.{ts,tsx} : Use distinct ID sourced from PostHog cookie via `cache()`-wrapped `getDistinctId()` to comply with Next.js restrictions

Applied to files:

  • app/login/page.tsx
  • app/(static)/privacy/page.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.{ts,tsx} : Use kebab-case for component file names and PascalCase for exports in `components/` directory

Applied to files:

  • emails/domain-expiry.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/**/*.{ts,tsx} : Isolate DNS, RDAP/WHOIS, TLS, and header probing services in `server/services/` with tRPC routers in `server/routers/`

Applied to files:

  • server/routers/tracking.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/routers/**/*.{ts,tsx} : Review `trpc/init.ts` when extending procedures to ensure auth/context remain intact

Applied to files:

  • server/routers/tracking.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Applied to files:

  • lib/db/repos/certificates.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/schema.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Drizzle ORM schema, migrations, and repository layer should be organized in `lib/db/` with repos in `lib/db/repos/` for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)

Applied to files:

  • lib/db/repos/certificates.ts
  • lib/db/repos/tracked-domains.ts
  • lib/db/repos/notifications.ts
  • lib/db/schema.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Use `drizzle-zod` for DB boundary validation with *Select schemas from `lib/db/zod.ts` for reads and *Insert/*Update for writes, with dates coerced in write schemas

Applied to files:

  • lib/db/repos/certificates.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.test.{ts,tsx} : Do not add direct tests for `components/ui/*` (shadcn components); mock Radix primitives (Accordion, Tooltip) when testing domain sections

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
🧬 Code graph analysis (17)
lib/db/repos/user-notification-preferences.test.ts (4)
lib/db/pglite.ts (2)
  • makePGliteDb (17-49)
  • closePGliteDb (55-66)
lib/db/client.ts (1)
  • db (27-27)
lib/db/schema.ts (2)
  • users (68-80)
  • userNotificationPreferences (228-245)
lib/db/repos/user-notification-preferences.ts (3)
  • getOrCreateUserNotificationPreferences (17-52)
  • getUserNotificationPreferences (85-103)
  • updateUserNotificationPreferences (57-80)
lib/notifications.test.ts (1)
lib/notifications.ts (3)
  • generateIdempotencyKey (18-23)
  • getDomainExpiryNotificationType (31-42)
  • getCertificateExpiryNotificationType (50-63)
app/(static)/terms/page.tsx (1)
app/(static)/privacy/page.tsx (1)
  • metadata (4-8)
lib/inngest/functions/check-domain-expiry.ts (10)
lib/inngest/client.ts (1)
  • inngest (10-13)
lib/db/schema.ts (1)
  • trackedDomains (150-188)
lib/db/repos/tracked-domains.ts (1)
  • getVerifiedTrackedDomainsWithExpiry (379-407)
lib/db/repos/notifications.ts (5)
  • clearDomainExpiryNotifications (152-180)
  • hasNotificationBeenSent (96-112)
  • NotificationType (12-12)
  • createNotification (23-61)
  • updateNotificationResendId (67-91)
lib/notifications.ts (3)
  • getDomainExpiryNotificationType (31-42)
  • NotificationType (10-10)
  • generateIdempotencyKey (18-23)
lib/db/repos/user-notification-preferences.ts (1)
  • getOrCreateUserNotificationPreferences (17-52)
lib/constants/notifications.ts (1)
  • NotificationType (43-47)
lib/resend.ts (2)
  • resend (14-14)
  • RESEND_FROM_EMAIL (16-17)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
emails/domain-expiry.tsx (1)
  • DomainExpiryEmail (24-111)
lib/inngest/functions/check-certificate-expiry.ts (9)
lib/inngest/client.ts (1)
  • inngest (10-13)
lib/db/repos/certificates.ts (1)
  • getVerifiedTrackedDomainsCertificates (61-93)
lib/db/repos/notifications.ts (5)
  • clearCertificateExpiryNotifications (187-215)
  • hasNotificationBeenSent (96-112)
  • NotificationType (12-12)
  • createNotification (23-61)
  • updateNotificationResendId (67-91)
lib/notifications.ts (3)
  • getCertificateExpiryNotificationType (50-63)
  • NotificationType (10-10)
  • generateIdempotencyKey (18-23)
lib/db/repos/user-notification-preferences.ts (1)
  • getOrCreateUserNotificationPreferences (17-52)
lib/constants/notifications.ts (1)
  • NotificationType (43-47)
lib/resend.ts (2)
  • resend (14-14)
  • RESEND_FROM_EMAIL (16-17)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
emails/certificate-expiry.tsx (1)
  • CertificateExpiryEmail (24-121)
lib/notifications.ts (2)
lib/db/repos/notifications.ts (1)
  • NotificationType (12-12)
lib/constants/notifications.ts (5)
  • NotificationType (43-47)
  • DOMAIN_EXPIRY_THRESHOLDS (35-35)
  • DOMAIN_THRESHOLD_TO_TYPE (50-58)
  • CERTIFICATE_EXPIRY_THRESHOLDS (36-36)
  • CERTIFICATE_THRESHOLD_TO_TYPE (60-68)
app/login/page.tsx (2)
lib/auth.ts (1)
  • auth (8-40)
components/auth/login-content.tsx (1)
  • LoginContent (16-68)
emails/domain-expiry.tsx (1)
components/ui/button.tsx (1)
  • Button (60-60)
app/dashboard/layout.tsx (3)
app/layout.tsx (1)
  • metadata (13-24)
app/login/page.tsx (1)
  • metadata (6-9)
lib/auth.ts (1)
  • auth (8-40)
lib/constants/notifications.test.ts (1)
lib/constants/notifications.ts (6)
  • NOTIFICATION_CATEGORIES (7-11)
  • NOTIFICATION_CATEGORY_INFO (16-32)
  • DOMAIN_EXPIRY_THRESHOLDS (35-35)
  • DOMAIN_THRESHOLD_TO_TYPE (50-58)
  • CERTIFICATE_EXPIRY_THRESHOLDS (36-36)
  • CERTIFICATE_THRESHOLD_TO_TYPE (60-68)
components/auth/auth-button.tsx (4)
components/auth/user-menu.tsx (1)
  • UserMenu (27-134)
components/ui/button.tsx (1)
  • Button (60-60)
components/ui/dialog.tsx (5)
  • Dialog (133-133)
  • DialogContent (135-135)
  • DialogHeader (138-138)
  • DialogTitle (141-141)
  • DialogDescription (136-136)
components/auth/login-content.tsx (1)
  • LoginContent (16-68)
components/dashboard/settings-content.tsx (5)
trpc/server.ts (1)
  • trpc (15-19)
lib/constants/notifications.ts (3)
  • NotificationCategory (13-13)
  • NOTIFICATION_CATEGORIES (7-11)
  • NOTIFICATION_CATEGORY_INFO (16-32)
lib/utils.ts (1)
  • cn (4-6)
lib/schemas/internal/notifications.ts (1)
  • NotificationOverrides (28-28)
components/domain/favicon.tsx (1)
  • Favicon (35-135)
app/(static)/privacy/page.tsx (1)
app/(static)/terms/page.tsx (1)
  • metadata (4-8)
lib/db/repos/tracked-domains.test.ts (4)
lib/db/pglite.ts (2)
  • makePGliteDb (17-49)
  • closePGliteDb (55-66)
lib/db/client.ts (1)
  • db (27-27)
lib/db/schema.ts (4)
  • users (68-80)
  • userLimits (212-225)
  • domains (280-300)
  • trackedDomains (150-188)
lib/db/repos/tracked-domains.ts (16)
  • createTrackedDomain (62-85)
  • findTrackedDomain (90-103)
  • findTrackedDomainById (108-116)
  • verifyTrackedDomain (250-269)
  • markVerificationFailing (500-516)
  • markVerificationSuccessful (481-493)
  • revokeVerification (523-540)
  • countTrackedDomainsForUser (235-244)
  • deleteTrackedDomain (340-348)
  • getTrackedDomainsForUser (156-230)
  • getVerifiedDomainsForReverification (413-439)
  • getPendingDomainsForAutoVerification (455-474)
  • findTrackedDomainWithDomainName (132-151)
  • updateNotificationOverrides (277-314)
  • resetNotificationOverrides (321-335)
  • getVerifiedTrackedDomainsWithExpiry (379-407)
lib/db/repos/notifications.ts (4)
lib/constants/notifications.ts (1)
  • NotificationType (43-47)
lib/notifications.ts (1)
  • NotificationType (10-10)
lib/db/client.ts (1)
  • db (27-27)
lib/db/schema.ts (1)
  • notifications (191-207)
lib/db/repos/notifications.test.ts (4)
lib/db/pglite.ts (2)
  • makePGliteDb (17-49)
  • closePGliteDb (55-66)
lib/db/client.ts (1)
  • db (27-27)
lib/db/schema.ts (5)
  • users (68-80)
  • userLimits (212-225)
  • domains (280-300)
  • trackedDomains (150-188)
  • notifications (191-207)
lib/db/repos/notifications.ts (7)
  • createNotification (23-61)
  • hasNotificationBeenSent (96-112)
  • updateNotificationResendId (67-91)
  • getNotificationsForTrackedDomain (117-127)
  • deleteNotificationsForTrackedDomain (133-145)
  • clearDomainExpiryNotifications (152-180)
  • clearCertificateExpiryNotifications (187-215)
lib/db/schema.ts (1)
lib/schemas/internal/notifications.ts (1)
  • NotificationOverrides (28-28)
🔇 Additional comments (44)
app/login/page.tsx (1)

11-31: Auth guard and thin page component look solid

The session check + redirect pattern is clean, and keeping this page as a thin server component that delegates UI to LoginContent aligns well with the app router guidelines; the gradient backdrop and centered layout are encapsulated nicely.

If you haven’t already, just confirm via a quick manual run that authenticated users never briefly see the login UI before redirect when hitting /login directly.

app/dashboard/layout.tsx (3)

1-3: LGTM: Correct imports for server-side auth.

The imports are appropriate for a Next.js server component performing authentication checks. The use of @/ alias follows the project's coding guidelines.


5-8: LGTM: Metadata is properly configured.

The metadata export follows the project's pattern and provides appropriate title and description for the dashboard layout.


24-24: LGTM: Clean container implementation.

The rendering is straightforward and uses appropriate Tailwind utility classes for a centered container layout.

components/layout/app-footer.tsx (1)

45-49: LGTM!

Clean addition of legal links using Next.js Link for client-side navigation. The styling is consistent with the existing footer patterns.

app/layout.tsx (1)

57-57: LGTM!

Placing the Toaster inside Providers ensures it has access to the theme context, and positioning it at the root layout enables toast notifications throughout the entire application.

lib/constants/notifications.test.ts (1)

1-47: LGTM!

Well-structured test coverage for notification constants. The tests validate:

  • All expected categories are present with correct length
  • Category info records have required label and description fields
  • Threshold-to-type mappings are complete and follow expected naming patterns

This ensures stability of the public constants surface used by notification flows.

lib/db/repos/user-notification-preferences.test.ts (1)

1-174: LGTM!

Comprehensive test coverage for the user notification preferences repository layer. The tests properly:

  • Mock the DB client with PGlite per project conventions
  • Isolate tests with proper setup/teardown lifecycle hooks
  • Cover creation with defaults, retrieval, partial updates, full updates, and persistence verification

Good adherence to the coding guidelines for database tests.

emails/verification-revoked.tsx (1)

1-92: LGTM!

Well-structured email template following the established pattern for this PR's email components. The component properly:

  • Uses @react-email/components for cross-client email compatibility
  • Provides clear messaging about verification revocation with actionable CTA
  • Includes PreviewProps for development preview
  • Follows consistent styling with other email templates
components/dashboard/settings-content.tsx (2)

69-169: Well-implemented optimistic updates.

The three mutations follow a consistent pattern with proper:

  • Query cancellation before optimistic update
  • Snapshot of previous state for rollback
  • Rollback on error with toast notification
  • Cache invalidation on settle

This provides a responsive UX while maintaining data consistency.


506-554: Clean three-state checkbox implementation.

The cycling logic (inherit → explicit opposite → explicit same → inherit) provides an intuitive UX for overriding global defaults. The tooltip feedback clearly communicates the current state to users.

server/routers/tracking.ts (2)

56-70: LGTM - Clean limits query.

The getLimits procedure efficiently combines user limits lookup with current count and derived canAddMore flag.


85-220: Well-structured addDomain with race condition handling.

Good implementation that:

  • Checks existing tracked domains before creating
  • Allows resuming verification for unverified domains
  • Enforces limits only for new additions
  • Handles race conditions gracefully by falling back to the existing record

The resumed flag provides useful feedback to the client.

components/auth/user-menu.tsx (3)

44-56: LGTM - Clean sign-out handling.

The try/catch with error logging and redirect on success follows good patterns.


58-65: Nice UX pattern for settings click.

Allowing modified clicks (ctrl/cmd/shift/middle-click) to open in new tab while normal clicks open the modal provides a good user experience.


121-131: LGTM - Modal settings dialog.

Clean implementation using the shared SettingsContent component with showCard={false} for modal context. The sr-only header provides accessibility for screen readers.

app/(static)/layout.tsx (1)

1-23: Clean static layout implementation with good progressive enhancement.

The glassmorphism styling with supports-[backdrop-filter] fallback and the article typography selectors provide a cohesive design system for static content pages. The fixed decorative background with -z-10 correctly layers behind content.

lib/notifications.test.ts (2)

1-55: Comprehensive tests for idempotency key generation.

Good coverage of uniqueness, consistency, and format validation across all notification types. The as const assertion ensures type safety for the notification type literals.


57-164: Thorough threshold boundary testing with clear intent.

The tests correctly verify the "most urgent" threshold selection logic and handle edge cases like already-expired domains (negative days). The explicit boundary value tests at 30/14/7/1 for domains and 14/7/3/1 for certificates provide good coverage.

components/auth/auth-button.tsx (2)

17-44: Well-implemented progressive enhancement pattern.

The handleClick logic correctly allows keyboard-modified clicks and middle-clicks to navigate to /login directly while presenting a modal for standard clicks. The loading placeholder prevents layout shift. This matches the pattern used in UserMenu.handleSettingsClick.


46-67: Good accessibility with screen reader support.

The sr-only DialogHeader ensures screen readers announce the dialog purpose while keeping the visual design clean. The glassmorphism styling is consistent with the app's design system.

app/(static)/privacy/page.tsx (2)

1-18: Clean metadata and header implementation.

The metadata export follows the same pattern as the Terms page. The header structure with border separator works well within the StaticLayout's article styling.


20-152: Well-structured privacy policy with appropriate semantic markup.

Good use of semantic HTML hierarchy (h2/h3), lists, and proper link handling (Next.js Link for internal routes, standard anchor for mailto). The content appropriately reflects the service's data collection via GitHub OAuth.

lib/db/repos/tracked-domains.test.ts (3)

1-84: Well-structured test setup following coding guidelines.

The PGlite mock pattern correctly replaces @/lib/db/client per the coding guidelines. The beforeAll/afterAll lifecycle properly manages test fixtures and cleans up the database connection to prevent resource leaks.


85-328: Comprehensive CRUD test coverage with proper edge case handling.

The tests cover creation, retrieval, updates, and deletion with appropriate assertions. The idempotent failure time test (lines 224-245) correctly verifies that verificationFailedAt is preserved on subsequent failures - important for tracking failure duration.


330-459: Good query function test coverage.

The tests verify complex return structures including nested provider objects (registrar, dns, hosting, email). The reverification and auto-verification filtering tests correctly validate the verified flag behavior.

app/(static)/terms/page.tsx (1)

1-195: LGTM!

The Terms of Service page is well-structured and follows the established pattern from the Privacy Policy page. The metadata, layout, and link usage are consistent with the codebase conventions. As a static content page with no business logic, the direct implementation in the page component is appropriate per the coding guidelines.

lib/inngest/functions/check-certificate-expiry.ts (3)

1-26: LGTM!

The imports and setup are well-organized. The "server-only" directive correctly restricts this module to server-side execution.


31-162: Well-structured cron job implementation.

The function follows best practices for Inngest:

  • Each operation is wrapped in step.run() for durability and retry isolation.
  • The certValidTo capture before step.run() correctly handles potential Date serialization issues across step boundaries.
  • The 15-minute offset from the domain expiry check helps distribute load.

164-267: Robust idempotency handling.

The implementation correctly addresses duplicate notification concerns:

  1. Idempotency key is generated before any operations.
  2. Notification record is created first as a "soft lock".
  3. Resend's idempotency key prevents duplicate emails on retry.
  4. Errors are re-thrown to leverage Inngest's retry mechanism safely.
lib/db/repos/certificates.ts (2)

57-93: LGTM!

The query correctly joins tracked domains with their certificates and users, filtering for verified domains. The post-query grouping to select the earliest expiring certificate per tracked domain is an appropriate approach when the min aggregation is complex in Drizzle.


45-55: No action required. The users table schema enforces .notNull() on both name and email columns, and the query uses innerJoin to select from the users table, which guarantees these fields are never null. The TrackedDomainCertificate type declaration is correct and safe.

lib/inngest/functions/check-domain-expiry.ts (3)

1-26: LGTM!

The imports and setup are well-organized and consistent with the certificate expiry function pattern.


89-94: Past review issue addressed.

The results.skipped++ increment is now correctly included when notificationType is null, ensuring the results count properly sums to results.total.


185-242: Past review issue addressed - robust idempotency implementation.

The duplicate notification concern from past reviews is now properly handled:

  1. Idempotency key is generated before any operations (lines 187-190).
  2. Notification record is created first as a "soft lock" (lines 199-202).
  3. Resend's idempotency key option prevents duplicate email sends (lines 239-241).

The inline comments clearly document this retry-safe strategy.

lib/notifications.ts (2)

18-23: LGTM!

The idempotency key generation is straightforward and correctly documented. The format {trackedDomainId}:{notificationType} provides a stable, unique key for Resend deduplication.


31-63: LGTM!

The threshold matching logic is correct and well-documented. Sorting ascending ensures the most urgent (smallest) threshold is matched first, which aligns with the documented behavior and examples.

lib/constants/notifications.ts (1)

1-68: LGTM! Constants and types are well-structured.

The notification system constants, thresholds, and type mappings are clearly defined and follow the coding guidelines for organizing constants in lib/constants/ submodules. The type system for notification categories and thresholds is type-safe and comprehensive.

Note: The past review comment about helper functions (getDomainExpiryNotificationType and getCertificateExpiryNotificationType) doesn't apply to this file, as it only contains constants and types without any runtime logic.

Based on learnings, this follows the guideline to organize constants by domain in lib/constants/ submodules.

lib/inngest/functions/reverify-domains.ts (1)

1-446: LGTM! Well-structured domain verification workflow.

The Inngest function implements a comprehensive domain verification lifecycle with:

  • Parallel workflows for pending and verified domains
  • Grace period handling (7 days before revocation)
  • Idempotent email notifications
  • Proper error handling and structured logging
  • Respect for per-domain and global notification preferences

The implementation follows Inngest best practices with concurrency control and step-based execution.

Based on learnings, this follows the guideline to organize Inngest functions in lib/inngest/functions/ with proper structure and event-driven background processing.

lib/db/repos/notifications.test.ts (1)

1-367: LGTM! Comprehensive test coverage with PGlite.

The test suite provides excellent coverage of the notifications repository with:

  • Proper PGlite setup and teardown
  • Test fixtures created in beforeAll
  • Comprehensive test cases for all exported functions
  • Idempotency verification (critical for notification deduplication)
  • Edge case handling (no matches, different types)
  • Selective clearing tests (domain vs certificate expiry)

Based on learnings, this follows the guideline to replace @/lib/db/client with PGlite-backed instance for database tests.

emails/domain-expiry.tsx (1)

1-237: LGTM! Well-structured email template.

The domain expiry email component is well-implemented with:

  • Clear prop types with optional registrar field
  • Conditional urgency indicators (⚠️ emoji, red highlighting) for <= 7 days
  • Responsive layout with proper email-safe styling
  • Preview props for development/testing
  • Proper pluralization handling ("1 day" vs "X days")
  • Dashboard CTA and settings link in footer

The template follows email best practices and integrates well with the notification system.

lib/db/repos/tracked-domains.ts (1)

1-540: LGTM! Comprehensive repository layer with proper patterns.

The tracked domains repository is well-implemented with:

  • Proper server-only enforcement
  • Comprehensive CRUD operations with null-safe returns
  • Complex multi-table joins with proper aliasing for multiple provider lookups
  • Idempotent operations using onConflictDoNothing
  • Conditional update logic (e.g., preserving existing verificationFailedAt)
  • Type-safe exports with clear type definitions
  • Structured logging for critical operations
  • Error handling with try-catch where appropriate
  • Filtering logic with type guards (line 435-438)

The repository follows Drizzle ORM best practices and integrates well with the domain verification and notification workflows.

Based on learnings, this follows the guideline to organize repository layer in lib/db/repos/ for each table with proper structure.

lib/db/repos/notifications.ts (1)

1-215: LGTM! Clean repository layer with idempotent operations.

The notifications repository is well-implemented with:

  • Proper server-only enforcement
  • Idempotent notification creation using onConflictDoNothing
  • Fallback fetch when notification already exists (lines 41-53)
  • Proper error handling with structured logging
  • Pattern matching for selective clearing (domain_expiry_%, certificate_expiry_%)
  • Informational logging when clearing notifications (useful for renewal tracking)
  • Consistent return types for success/failure scenarios
  • Type-safe re-export of NotificationType for consumer convenience

The repository integrates well with the notification workflow and supports the idempotency requirements for email deduplication.

Based on learnings, this follows the guideline to organize repository layer in lib/db/repos/ for each table with proper structure.

lib/db/schema.ts (1)

51-61: LGTM! Well-designed schema additions.

The authentication and domain tracking schema is well-structured with:

  • Type-safe enums for verification methods, statuses, and user tiers
  • Proper foreign key relationships with cascade deletes
  • Strategic indexing on foreign keys and commonly queried columns
  • Unique constraints for data integrity (e.g., u_notification_unique for idempotency)
  • JSONB columns with Zod types to prevent drift (e.g., NotificationOverrides)
  • Composite unique constraint for user-domain tracking (line 182)
  • Helpful comments explaining business logic (e.g., userLimits override)
  • Proper timestamp fields with timezone support

The schema integrates well with the repository layer and supports the verification, notification, and authentication workflows.

Based on learnings, this follows the guideline to reuse domain Zod types for JSON columns to avoid drift between database and validation schemas.

Also applies to: 63-245

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
components/auth/auth-button.tsx (1)

57-76: Desktop sign-in implementation is correct.

The progressive enhancement pattern with modified click detection is appropriate. Note: The login dialog pattern here is duplicated with mobile-menu.tsx—see earlier comment about extracting a shared LoginDialog component.

components/auth/user-menu.tsx (1)

49-55: Add defensive checks for user.name.

This code will throw a runtime error if user.name is undefined, null, or an empty string. This was flagged in a previous review and remains unfixed.

Apply defensive handling:

   const user = session.user;
-  const initials = user.name
-    .split(" ")
-    .map((n) => n[0])
-    .join("")
-    .toUpperCase()
-    .slice(0, 2);
+  const initials = (user.name || "")
+    .split(" ")
+    .filter((n) => n.length > 0)
+    .map((n) => n[0])
+    .join("")
+    .toUpperCase()
+    .slice(0, 2) || "??";
🧹 Nitpick comments (13)
components/layout/theme-toggle.tsx (1)

27-27: Simplify cn() usage or pass className directly.

Using cn(className) with a single optional argument is redundant—it just returns the className unchanged. Either pass className directly or use cn() when you need to merge with existing classes.

-      className={cn(className)}
+      className={className}

Alternatively, if the Button has default classes you want to preserve alongside external ones:

-      className={cn(className)}
+      className={cn("your-default-classes", className)}
components/layout/mobile-menu.tsx (2)

30-39: Consider extracting shared theme toggle logic.

The theme toggle logic (lines 30-39) is duplicated across mobile-menu.tsx, user-menu.tsx, and the pattern in theme-toggle.tsx. Consider extracting a shared hook to reduce duplication.

Example hook extraction:

// hooks/use-theme-toggle.ts
export function useThemeToggle() {
  const { theme, setTheme, systemTheme } = useTheme();
  const current = theme === "system" ? systemTheme : theme;
  const isDark = current === "dark";
  
  const toggleTheme = () => setTheme(isDark ? "light" : "dark");
  
  return { isDark, toggleTheme };
}

85-96: Duplicated login dialog pattern.

The login dialog structure and styling (lines 85-96) is identical to auth-button.tsx (lines 65-75). Consider extracting a shared LoginDialog component.

// components/auth/login-dialog.tsx
export function LoginDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-sm overflow-hidden rounded-3xl border-black/10 bg-background/80 p-0 backdrop-blur-xl supports-[backdrop-filter]:bg-background/80 dark:border-white/10">
        <DialogHeader className="sr-only">
          <DialogTitle>Sign In</DialogTitle>
          <DialogDescription>
            Sign in to track your domains and receive expiration alerts.
          </DialogDescription>
        </DialogHeader>
        <LoginContent showCard={false} />
      </DialogContent>
    </Dialog>
  );
}
components/dashboard/settings-content.tsx (1)

341-579: Consider extracting inner components to separate files.

This file is ~600 LOC, exceeding the guideline of ≈≤300 LOC. The inner components (GlobalNotificationRow, DomainNotificationRow, ThreeStateCheckbox, CategoryLabel, SettingsContentSkeleton) could be extracted to improve maintainability.

Suggested structure:

components/dashboard/settings/
├── settings-content.tsx
├── global-notification-row.tsx
├── domain-notification-row.tsx
├── three-state-checkbox.tsx
├── category-label.tsx
└── settings-skeleton.tsx

Based on coding guidelines, TypeScript modules should be ≈≤300 LOC.

server/routers/tracking.ts (2)

106-122: Extract duplicate instruction generation into a helper function.

The same instruction-generation pattern appears three times (lines 106–122, 162–178, 195–211), violating the DRY principle. Extract this into a reusable helper function to improve maintainability.

Add a helper function at the top of the file (after schema definitions):

function generateAllVerificationInstructions(
  domain: string,
  verificationToken: string,
) {
  return {
    dns_txt: getVerificationInstructions(domain, verificationToken, "dns_txt"),
    html_file: getVerificationInstructions(domain, verificationToken, "html_file"),
    meta_tag: getVerificationInstructions(domain, verificationToken, "meta_tag"),
  };
}

Then replace all three occurrences:

-        const instructions = {
-          dns_txt: getVerificationInstructions(
-            domain,
-            existing.verificationToken,
-            "dns_txt",
-          ),
-          html_file: getVerificationInstructions(
-            domain,
-            existing.verificationToken,
-            "html_file",
-          ),
-          meta_tag: getVerificationInstructions(
-            domain,
-            existing.verificationToken,
-            "meta_tag",
-          ),
-        };
+        const instructions = generateAllVerificationInstructions(
+          domain,
+          existing.verificationToken,
+        );

Also applies to: 162-178, 195-211


1-479: Consider adding structured logging for critical operations.

While tRPC middleware automatically logs all procedures with correlation IDs and OpenTelemetry context (as per learnings), adding explicit logging for critical operations would improve debugging and observability. Consider logging:

  • Domain verification attempts and outcomes (lines 258–275)
  • Race condition detection (lines 156–161)
  • Limit enforcement decisions (lines 137–143)

Based on coding guidelines:

Add at the top of the file:

import { createLogger } from "@/lib/logger/server";

const logger = createLogger({ source: "tracking-router" });

Then add logging at key points, for example:

// In verifyDomain procedure after line 273
logger.info("Domain verified successfully", {
  trackedDomainId,
  method: result.method,
  domain: tracked.domainName,
});

// In addDomain after line 156 when detecting race condition
logger.debug("Race condition detected, resuming existing tracked domain", {
  userId: ctx.user.id,
  domainId: domainRecord.id,
});
lib/db/repos/tracked-domains.test.ts (7)

94-121: createTrackedDomain tests cover defaults and method option; consider conflict-path coverage

The tests correctly assert default verification fields and the optional verificationMethod. To fully exercise onConflictDoNothing, consider adding a case where you call createTrackedDomain twice with the same (userId, domainId) and expect the second call to return null (and still only one DB row).


167-187: verifyTrackedDomain behavior validated; add an explicit non-null expectation for clarity

The test validates all key fields updated by verifyTrackedDomain, including date fields and status. For debuggability, you might add expect(result).not.toBeNull() before asserting properties so failures report more clearly if an unexpected null is ever returned.


189-213: markVerificationSuccessful test correctly exercises the full lifecycle

This test walks through verify → mark failing → mark successful and asserts that status and timestamps are reset appropriately, which matches the repo logic that clears verificationFailedAt and updates lastVerifiedAt. As above, an explicit expect(result).not.toBeNull() would slightly tighten the assertion story.


215-255: markVerificationFailing tests cover both initial failure and idempotent timestamp behavior

You validate that status flips to "failing" and that verificationFailedAt is set once and then preserved across subsequent calls, matching the existing.verificationFailedAt ?? new Date() logic. If you want exhaustive coverage, a small extra test for a non-existent ID returning null would finish off the edge cases.


339-369: getTrackedDomainsForUser shape and empty-state are validated; consider a provider-populated case

You verify that an empty result returns [] and that a basic tracked domain includes the expected core fields plus nested provider objects with { name: null, domain: null } when no related rows exist. As a future enhancement, adding one test where registrar/DNS/hosting/email providers are populated would exercise the positive branches of the join/transform logic.


371-406: getVerifiedDomainsForReverification tests cover core behavior; optional edge-case for null methods

The tests ensure that unverified domains are excluded and that verified domains with a method are returned with the expected fields, matching the query plus post-filter. If you want to fully cover the defensive filter that drops rows with verificationMethod === null, you could insert a row directly with verified = true and verificationMethod = null and assert it’s excluded.


556-662: getVerifiedTrackedDomainsWithExpiry tests are thorough; minor redundancy opportunity

The tests collectively verify:

  • No results when there’s an unverified tracked domain with no registration,
  • Exclusion of unverified domains,
  • Full record shape when registration and registrar exist (including Date equality on expirationDate), and
  • The null registrar case when no provider is linked.

This aligns closely with the query and left join behavior in the repo. The first two tests are effectively asserting the same outcome (no verified+registered domains); you could optionally collapse them into a single, clearly named case to reduce redundancy.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b07cc5 and 3f1e06a.

📒 Files selected for processing (10)
  • components/auth/auth-button.tsx (1 hunks)
  • components/auth/user-menu.tsx (1 hunks)
  • components/dashboard/settings-content.tsx (1 hunks)
  • components/domain/domain-report-header.tsx (1 hunks)
  • components/layout/app-header.tsx (2 hunks)
  • components/layout/bookmarklet-dialog.tsx (2 hunks)
  • components/layout/mobile-menu.tsx (1 hunks)
  • components/layout/theme-toggle.tsx (2 hunks)
  • lib/db/repos/tracked-domains.test.ts (1 hunks)
  • server/routers/tracking.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/layout/app-header.tsx
🧰 Additional context used
📓 Path-based instructions (10)
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/layout/mobile-menu.tsx
  • components/domain/domain-report-header.tsx
  • components/dashboard/settings-content.tsx
  • components/layout/bookmarklet-dialog.tsx
  • components/auth/auth-button.tsx
  • components/auth/user-menu.tsx
  • components/layout/theme-toggle.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • components/layout/mobile-menu.tsx
  • components/domain/domain-report-header.tsx
  • components/dashboard/settings-content.tsx
  • components/layout/bookmarklet-dialog.tsx
  • components/auth/auth-button.tsx
  • components/auth/user-menu.tsx
  • server/routers/tracking.ts
  • components/layout/theme-toggle.tsx
  • lib/db/repos/tracked-domains.test.ts
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • components/layout/mobile-menu.tsx
  • components/domain/domain-report-header.tsx
  • components/dashboard/settings-content.tsx
  • components/layout/bookmarklet-dialog.tsx
  • components/auth/auth-button.tsx
  • components/auth/user-menu.tsx
  • server/routers/tracking.ts
  • components/layout/theme-toggle.tsx
  • lib/db/repos/tracked-domains.test.ts
server/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Isolate DNS, RDAP/WHOIS, TLS, and header probing services in server/services/ with tRPC routers in server/routers/

Files:

  • server/routers/tracking.ts
{server/**,app/api/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

{server/**,app/api/**}/*.{ts,tsx}: Use drizzle-zod for DB boundary validation with *Select schemas from lib/db/zod.ts for reads and *Insert/*Update for writes, with dates coerced in write schemas
Server-side logging: import singleton import { logger } from "@/lib/logger/server" or create service logger with createLogger({ source: "..." })
Server logging automatically injects OpenTelemetry trace/span IDs and correlation IDs via AsyncLocalStorage for request tracing
Server logging automatically tracks critical errors in PostHog via after()
Server logging supports levels: trace, debug, info, warn, error, fatal

Files:

  • server/routers/tracking.ts
server/routers/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Review trpc/init.ts when extending procedures to ensure auth/context remain intact

Files:

  • server/routers/tracking.ts
{app/**,server/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use distinct ID sourced from PostHog cookie via cache()-wrapped getDistinctId() to comply with Next.js restrictions

Files:

  • server/routers/tracking.ts
lib/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

lib/db/**/*.{ts,tsx}: Drizzle ORM schema, migrations, and repository layer should be organized in lib/db/ with repos in lib/db/repos/ for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)
Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Files:

  • lib/db/repos/tracked-domains.test.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx}: Use Vitest with React Testing Library; configure in vitest.config.ts with threads pool for sandboxed environment compatibility
Mock @vercel/blob (put and del functions) in tests; set BLOB_READ_WRITE_TOKEN via vi.stubEnv in suites that touch uploads/deletes
Mock URL.createObjectURL/revokeObjectURL with vi.fn() in tests that need them
Logger is mocked in tests via vitest.setup.ts; use vi.mocked(logger.info) to assert log calls in tests

Files:

  • lib/db/repos/tracked-domains.test.ts
lib/db/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Replace @/lib/db/client with a PGlite-backed instance (@/lib/db/pglite) for database tests

Files:

  • lib/db/repos/tracked-domains.test.ts
🧠 Learnings (9)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/**/*.{ts,tsx} : Isolate DNS, RDAP/WHOIS, TLS, and header probing services in `server/services/` with tRPC routers in `server/routers/`

Applied to files:

  • server/routers/tracking.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/routers/**/*.{ts,tsx} : Review `trpc/init.ts` when extending procedures to ensure auth/context remain intact

Applied to files:

  • server/routers/tracking.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to trpc/init.ts : tRPC middleware in `trpc/init.ts` automatically logs all procedures with correlation IDs and OpenTelemetry context

Applied to files:

  • server/routers/tracking.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to vitest.setup.ts : Global test setup in `vitest.setup.ts` mocks analytics clients/servers (`@/lib/analytics/server` and `@/lib/analytics/client`), logger clients/servers (`@/lib/logger/server` and `@/lib/logger/client`), and `server-only` module

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.test.{ts,tsx} : Replace `@/lib/db/client` with a PGlite-backed instance (`@/lib/db/pglite`) for database tests

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.test.{ts,tsx} : Use **Vitest** with React Testing Library; configure in `vitest.config.ts` with `threads` pool for sandboxed environment compatibility

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.test.{ts,tsx} : Do not add direct tests for `components/ui/*` (shadcn components); mock Radix primitives (Accordion, Tooltip) when testing domain sections

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/**/*.test.{ts,tsx} : Prefer `vi.hoisted` for ESM module mocks in server tests (e.g., `node:tls`)

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Applied to files:

  • lib/db/repos/tracked-domains.test.ts
🧬 Code graph analysis (5)
components/layout/mobile-menu.tsx (5)
components/ui/dropdown-menu.tsx (5)
  • DropdownMenu (242-242)
  • DropdownMenuTrigger (244-244)
  • DropdownMenuContent (245-245)
  • DropdownMenuItem (248-248)
  • DropdownMenuSeparator (252-252)
components/ui/button.tsx (1)
  • Button (60-60)
components/ui/dialog.tsx (5)
  • Dialog (133-133)
  • DialogContent (135-135)
  • DialogHeader (138-138)
  • DialogTitle (141-141)
  • DialogDescription (136-136)
components/auth/login-content.tsx (1)
  • LoginContent (16-68)
components/layout/bookmarklet-dialog.tsx (1)
  • BookmarkletDialog (36-188)
components/layout/bookmarklet-dialog.tsx (2)
components/ui/dialog.tsx (2)
  • Dialog (133-133)
  • DialogTrigger (142-142)
components/ui/button.tsx (1)
  • Button (60-60)
components/auth/auth-button.tsx (6)
hooks/use-mobile.ts (1)
  • useIsMobile (5-7)
components/auth/user-menu.tsx (1)
  • UserMenu (30-178)
components/layout/mobile-menu.tsx (1)
  • MobileMenu (29-105)
components/ui/button.tsx (1)
  • Button (60-60)
components/ui/dialog.tsx (5)
  • Dialog (133-133)
  • DialogContent (135-135)
  • DialogHeader (138-138)
  • DialogTitle (141-141)
  • DialogDescription (136-136)
components/auth/login-content.tsx (1)
  • LoginContent (16-68)
components/layout/theme-toggle.tsx (1)
lib/utils.ts (1)
  • cn (4-6)
lib/db/repos/tracked-domains.test.ts (1)
lib/db/repos/tracked-domains.ts (16)
  • createTrackedDomain (62-85)
  • findTrackedDomain (90-103)
  • findTrackedDomainById (108-116)
  • verifyTrackedDomain (250-269)
  • markVerificationFailing (500-516)
  • markVerificationSuccessful (481-493)
  • revokeVerification (523-540)
  • countTrackedDomainsForUser (235-244)
  • deleteTrackedDomain (340-348)
  • getTrackedDomainsForUser (156-230)
  • getVerifiedDomainsForReverification (413-439)
  • getPendingDomainsForAutoVerification (455-474)
  • findTrackedDomainWithDomainName (132-151)
  • updateNotificationOverrides (277-314)
  • resetNotificationOverrides (321-335)
  • getVerifiedTrackedDomainsWithExpiry (379-407)
🔇 Additional comments (27)
components/domain/domain-report-header.tsx (1)

30-37: Layout change to anchor flex behavior looks fine; verify truncation on narrow widths

Removing flex-1 from the anchor keeps the left group from claiming all remaining space, which should better respect the fixed-width actions on the right while still allowing truncation via min-w-0 and truncate on the <h2>. Functionality, analytics tracking, and a11y (including rel="noopener") remain intact.

Please just sanity-check in the dashboard with very long domains on smaller viewports to confirm the text still truncates as intended and the buttons don’t get squashed.

components/layout/theme-toggle.tsx (1)

8-12: LGTM!

The ThemeToggleProps interface is clean and follows TypeScript conventions. Making className optional allows for flexible usage.

components/layout/mobile-menu.tsx (1)

50-103: LGTM!

The component structure is well-organized with proper accessibility (aria-label), controlled dialog state management, and progressive enhancement for the sign-in link (allowing modified clicks to open in new tab).

components/auth/auth-button.tsx (1)

19-43: LGTM!

Good handling of the different auth states with appropriate loading skeleton to prevent layout shift. The separation between UserMenu for logged-in users and MobileMenu for logged-out mobile users is clean.

components/layout/bookmarklet-dialog.tsx (2)

28-40: LGTM!

Clean implementation of the controlled/uncontrolled dialog pattern. The props interface is well-documented with JSDoc comments, and the component signature properly accepts optional controlled props.


58-80: Well-implemented controlled mode.

The handleOpenChange correctly dismisses the toast on close and forwards state changes. The conditional DialogTrigger rendering based on isControlled ensures the trigger is only shown for uncontrolled usage.

components/dashboard/settings-content.tsx (3)

70-169: Well-implemented optimistic updates.

The mutation handlers follow React Query best practices:

  • Cancel in-flight queries before optimistic update
  • Save previous state for rollback
  • Apply optimistic update
  • Rollback on error with proper error logging/toast
  • Invalidate queries on settle

505-553: LGTM!

The three-state checkbox logic correctly cycles through inherit → explicit override → back to inherit, with appropriate tooltips explaining the current state. The onCheckedChange override to implement custom cycling is intentional and works correctly.


194-228: Good error handling approach.

Surfacing query errors with a retry option is better UX than silently falling back to defaults. The error state respects the showCard prop for consistent rendering.

components/auth/user-menu.tsx (2)

57-69: LGTM!

Sign-out handling correctly uses try/catch with logger.error for error tracking, following the coding guidelines for client-side logging.


80-177: LGTM!

The dropdown menu structure is well-organized with proper mobile/desktop conditional rendering, and the controlled dialogs for settings and bookmarklet are correctly implemented.

server/routers/tracking.ts (5)

34-54: LGTM! Schema definitions are well-structured.

The schemas correctly use Zod 4's satisfies operator for type checking, and the DomainInputSchema properly validates and normalizes domain input with appropriate error handling.


246-251: LGTM! Ownership validation is consistently implemented.

All domain-specific procedures correctly verify ownership by checking tracked.userId !== ctx.user.id and throw FORBIDDEN errors appropriately. The consistent pattern ensures security across all operations.

Also applies to: 307-312, 377-382, 424-429, 468-473


384-399: LGTM! Null checks properly handle race conditions.

Both updateNotificationOverrides and resetNotificationOverrides now correctly validate the return value and throw descriptive NOT_FOUND errors if the domain was deleted between the ownership check and the update operation. This addresses the concerns raised in previous reviews.

Also applies to: 431-443


156-192: Excellent race condition handling.

The defensive programming here correctly handles the scenario where multiple concurrent requests attempt to add the same domain. By re-fetching after a null return from createTrackedDomain and returning the existing record with resumed: true, the implementation gracefully resolves the race condition without throwing errors to the user.


254-256: LGTM! Idempotent verification handling.

The early return for already-verified domains provides idempotent behavior, allowing clients to safely retry verification without side effects.

lib/db/repos/tracked-domains.test.ts (11)

1-27: DB mocking + environment setup look correct and follow guidelines

Using the node environment and mocking @/lib/db/client to a PGlite-backed instance before importing the repo is exactly what we want for DB tests; this should keep the tests isolated from the real client and fast.

Based on learnings, this matches the PGlite testing pattern documented for lib/db/**/*.test.{ts,tsx}.


47-92: Lifecycle hooks and data reset strategy are sensible

The shared user/domain in beforeAll and targeted cleanup of trackedDomains, registrations, and providers in beforeEach give each test a clean slate while avoiding unnecessary re-seeding. This ordering also respects FK constraints.


123-141: findTrackedDomain happy-path and empty-state coverage look good

You cover both the “not tracked” and “tracked” cases, verifying that the correct record is returned once created. This aligns with the repo implementation that limits to a single row and returns null when absent.


143-165: findTrackedDomainById tests are straightforward and adequate

The tests exercise both the non-existent ID and successful lookup paths and mirror the repo’s rows[0] ?? null behavior. Use of a sentinel UUID string is fine here.


257-278: revokeVerification test aligns with intended semantics

The test ensures verified becomes false, status returns to "unverified", and verificationFailedAt is cleared after revocation. That matches the repo function, including the expectation that revocation is a full reset of verification state.


280-311: countTrackedDomainsForUser is well covered

You check both the zero-count case and counting across multiple domains for the same user. This matches the underlying SELECT count() query and gives good confidence in the aggregate behavior.


313-337: deleteTrackedDomain tests validate success semantics for existing and missing IDs

The tests confirm that deletion returns true both when a record is removed and when the ID doesn’t exist (the intended no-op behavior), plus you assert that the existing record is actually gone. This lines up with the repo’s try/catch implementation.


408-440: getPendingDomainsForAutoVerification covers both unverified inclusion and verified exclusion

The tests correctly assert that unverified domains appear in the results and that once verified they are omitted, aligning with the .where(eq(trackedDomains.verified, false)) behavior. Field matching via toMatchObject is also appropriate here.


442-468: findTrackedDomainWithDomainName tests map cleanly to the join-based implementation

You validate both the null case and the happy path including domainName and verificationToken, which confirms the innerJoin(domains, ...) and field projection are wired as expected.


470-522: updateNotificationOverrides tests nicely verify merge semantics and missing-ID behavior

The suite exercises:

  • Initial set of an override,
  • Merge behavior when adding another key, and
  • The null return when the tracked domain doesn’t exist.

This matches the repo’s merge logic and gives good confidence that overrides behave as “patches” rather than replacements.


524-554: resetNotificationOverrides coverage is solid

You confirm both that overrides are reset back to {} after having been set and that a non-existent ID yields null, matching the updated.length === 0 ? null : row implementation.

…domains to prevent race conditions and enhance user subscription handling
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (8)
lib/inngest/functions/auto-verify-pending-domain.ts (2)

16-22: Align event schema and comment with actual usage

The handler never reads verificationToken from the event payload (only from the DB), yet the schema and comment still model it on the event. For clarity and slightly smaller event payloads, consider either:

  • Removing verificationToken from eventSchema, or
  • Explicitly logging / using the event’s token somewhere so the comment remains accurate.

Right now the comment suggests a behavior (“optional since we always read it fresh from the DB”) that doesn’t quite match the code (we don’t use the event token at all).


81-98: Defensive handling if verificationToken is missing from DB

In check-status-* you forward domain.verificationToken directly into tracked, and then pass currentToken into tryAllVerificationMethods(currentDomainName, currentToken) without a guard. If verificationToken can ever be null/empty (e.g., a partially created row or a future schema change), this could break verification in a non-obvious way.

A small defensive check before calling the verifier would make this more robust, for example:

-      const { domainName: currentDomainName, verificationToken: currentToken } =
-        tracked;
-      const result = await step.run(`verify-attempt-${attempt}`, async () => {
-        return await tryAllVerificationMethods(currentDomainName, currentToken);
-      });
+      const { domainName: currentDomainName, verificationToken: currentToken } =
+        tracked;
+
+      if (!currentToken) {
+        inngestLogger.warn("Skipping auto-verification: missing verification token", {
+          trackedDomainId,
+          domainName: currentDomainName,
+          attempt: attempt + 1,
+        });
+        continue;
+      }
+
+      const result = await step.run(`verify-attempt-${attempt}`, async () => {
+        return await tryAllVerificationMethods(currentDomainName, currentToken);
+      });

If your DB constraint guarantees a non-null token for any “pending” domain, this is mostly belt-and-suspenders, but it avoids surprising failures if that invariant is ever violated.

Also applies to: 118-122

components/dashboard/domain-filters.tsx (1)

128-132: Consider showing the sort dropdown whenever sorting is supported, even before a sort is chosen

Right now the sort UI only appears when sortOption is truthy, so if callers support sorting but start with sortOption undefined, users won’t see the control to pick an initial sort. Since currentSort already safely handles an undefined sortOption, you can gate rendering purely on viewMode and onSortChange:

-  const currentSort = sortOption
-    ? SORT_OPTIONS.find((o) => o.value === sortOption)
-    : undefined;
+  const currentSort = sortOption
+    ? SORT_OPTIONS.find((o) => o.value === sortOption)
+    : undefined;

   {/* Sort dropdown - only for grid view */}
-  {viewMode === "grid" && sortOption && onSortChange && (
+  {viewMode === "grid" && onSortChange && (
     <DropdownMenu>

This keeps behavior for existing callers with a preset sort, while also enabling callers that only provide onSortChange to expose the sort control from the outset.

Also applies to: 210-245

lib/inngest/functions/check-subscription-expiry.ts (2)

22-26: Minor: Redundant fallback in getFirstName.

The || "there" on line 25 is unreachable since trimmed is guaranteed to be non-empty at that point (line 24 returns early if empty). The split on a non-empty string always produces at least one element.

 function getFirstName(name: string | null | undefined): string {
   const trimmed = (name || "").trim();
   if (!trimmed) return "there";
-  return trimmed.split(/\s+/)[0] || "there";
+  return trimmed.split(/\s+/)[0];
 }

37-50: Optional: Sorting is unnecessary since thresholds are constant.

The SUBSCRIPTION_EXPIRY_THRESHOLDS array is already defined. Consider precomputing the sorted array or iterating in the desired order directly, rather than sorting on every call.

+// Pre-sorted ascending for threshold lookup
+const SORTED_THRESHOLDS = [...SUBSCRIPTION_EXPIRY_THRESHOLDS].sort((a, b) => a - b);
+
 function getSubscriptionExpiryThreshold(
   daysRemaining: number,
 ): SubscriptionExpiryThreshold | null {
-  // Sort ascending so we find the smallest (most urgent) threshold first
-  const sortedThresholds = [...SUBSCRIPTION_EXPIRY_THRESHOLDS].sort(
-    (a, b) => a - b,
-  );
-  for (const threshold of sortedThresholds) {
+  for (const threshold of SORTED_THRESHOLDS) {
     if (daysRemaining <= threshold) {
       return threshold;
     }
   }
   return null;
 }
lib/db/repos/user-subscription.ts (1)

210-223: Consider validating update success.

Unlike other update functions in this file, setLastExpiryNotification doesn't verify the record was actually updated. While it's called after a successful email send (implying valid userId), adding a check would catch database issues.

 export async function setLastExpiryNotification(
   userId: string,
   threshold: number,
 ): Promise<void> {
-  await db
+  const updated = await db
     .update(userSubscriptions)
     .set({
       lastExpiryNotification: threshold,
       updatedAt: new Date(),
     })
-    .where(eq(userSubscriptions.userId, userId));
+    .where(eq(userSubscriptions.userId, userId))
+    .returning({ userId: userSubscriptions.userId });
+
+  if (updated.length === 0) {
+    logger.warn("subscription not found when setting expiry notification", {
+      userId,
+      threshold,
+    });
+    return;
+  }

   logger.debug("set last expiry notification", { userId, threshold });
 }
hooks/use-domain-verification.ts (2)

72-78: Tie verification instructions query to trackedDomainId instead of resumeDomain for safer refetching

Right now instructionsQuery is keyed off resumeDomain?.id and enabled: !!resumeDomain && open, but refetchInstructions is exposed unconditionally. If refetchInstructions is ever called when resumeDomain is null (e.g., in a non‑resume path in the future), React Query will still try to refetch using a trackedDomainId of "", which is likely invalid server‑side.

You can make this more robust (and reusable for non‑resume flows) by basing the query input and enabled flag on trackedDomainId instead of resumeDomain, and simplifying the effect that hydrates instructions:

-  const instructionsQuery = useQuery({
-    ...trpc.tracking.getVerificationInstructions.queryOptions({
-      trackedDomainId: resumeDomain?.id ?? "",
-    }),
-    enabled: !!resumeDomain && open,
-  });
+  const instructionsQuery = useQuery({
+    ...trpc.tracking.getVerificationInstructions.queryOptions({
+      trackedDomainId: trackedDomainId ?? "",
+    }),
+    enabled: !!trackedDomainId && open,
+  });
@@
-  useEffect(() => {
-    if (instructionsQuery.data && resumeDomain) {
-      setInstructions(instructionsQuery.data);
-    }
-  }, [instructionsQuery.data, resumeDomain]);
+  useEffect(() => {
+    if (instructionsQuery.data) {
+      setInstructions(instructionsQuery.data);
+    }
+  }, [instructionsQuery.data]);

This keeps resume behavior intact, avoids ever hitting the API with an empty ID, and makes refetchInstructions safe to call whenever a trackedDomainId is present.

Also applies to: 95-101, 261-266


40-43: Optionally clear domainError when the domain input changes

domainError is reset before each handleAddDomain call and on some resets, but it will persist while the user edits the domain after a failed attempt. For a slightly smoother UX, you could clear the error as soon as the domain value changes:

@@
   const [domain, setDomain] = useState(prefillDomain ?? "");
   const [domainError, setDomainError] = useState("");
@@
   useEffect(() => {
     if (!open && prefillDomain !== undefined) {
       setDomain(prefillDomain);
     }
   }, [prefillDomain, open]);
+
+  useEffect(() => {
+    if (domainError) {
+      setDomainError("");
+    }
+  }, [domain, domainError]);

This keeps error messaging focused on the last submitted value and avoids showing a stale error while the user is correcting their input.

Also applies to: 129-152

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7a7a09 and 0f99e51.

📒 Files selected for processing (12)
  • components/auth/animated-background.tsx (1 hunks)
  • components/dashboard/domain-filters.tsx (1 hunks)
  • hooks/use-domain-verification.ts (1 hunks)
  • hooks/use-upgrade-checkout.ts (1 hunks)
  • lib/auth.ts (1 hunks)
  • lib/db/repos/tracked-domains.ts (1 hunks)
  • lib/db/repos/user-subscription.ts (1 hunks)
  • lib/inngest/functions/auto-verify-pending-domain.ts (1 hunks)
  • lib/inngest/functions/check-subscription-expiry.ts (1 hunks)
  • lib/polar/emails.ts (1 hunks)
  • lib/polar/handlers.test.ts (1 hunks)
  • lib/polar/handlers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/auth.ts
  • hooks/use-upgrade-checkout.ts
  • lib/polar/emails.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • lib/polar/handlers.test.ts
  • hooks/use-domain-verification.ts
  • lib/inngest/functions/auto-verify-pending-domain.ts
  • lib/db/repos/user-subscription.ts
  • lib/db/repos/tracked-domains.ts
  • lib/inngest/functions/check-subscription-expiry.ts
  • components/dashboard/domain-filters.tsx
  • components/auth/animated-background.tsx
  • lib/polar/handlers.ts
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • lib/polar/handlers.test.ts
  • hooks/use-domain-verification.ts
  • lib/inngest/functions/auto-verify-pending-domain.ts
  • lib/db/repos/user-subscription.ts
  • lib/db/repos/tracked-domains.ts
  • lib/inngest/functions/check-subscription-expiry.ts
  • components/dashboard/domain-filters.tsx
  • components/auth/animated-background.tsx
  • lib/polar/handlers.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx}: Use Vitest with React Testing Library; configure in vitest.config.ts with threads pool for sandboxed environment compatibility
Mock @vercel/blob (put and del functions) in tests; set BLOB_READ_WRITE_TOKEN via vi.stubEnv in suites that touch uploads/deletes
Mock URL.createObjectURL/revokeObjectURL with vi.fn() in tests that need them
Logger is mocked in tests via vitest.setup.ts; use vi.mocked(logger.info) to assert log calls in tests

Files:

  • lib/polar/handlers.test.ts
hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use camelCase named exports for shared stateful helpers in hooks/ directory

Files:

  • hooks/use-domain-verification.ts
lib/inngest/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Inngest client and functions for event-driven background section revalidation should be organized in lib/inngest/ with proper structure

Files:

  • lib/inngest/functions/auto-verify-pending-domain.ts
  • lib/inngest/functions/check-subscription-expiry.ts
lib/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

lib/db/**/*.{ts,tsx}: Drizzle ORM schema, migrations, and repository layer should be organized in lib/db/ with repos in lib/db/repos/ for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)
Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Files:

  • lib/db/repos/user-subscription.ts
  • lib/db/repos/tracked-domains.ts
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/dashboard/domain-filters.tsx
  • components/auth/animated-background.tsx
🧠 Learnings (5)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/inngest/**/*.{ts,tsx} : Inngest client and functions for event-driven background section revalidation should be organized in `lib/inngest/` with proper structure

Applied to files:

  • lib/inngest/functions/auto-verify-pending-domain.ts
  • lib/inngest/functions/check-subscription-expiry.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use event-driven background revalidation via Inngest functions in `lib/inngest/functions/` with built-in concurrency control

Applied to files:

  • lib/inngest/functions/auto-verify-pending-domain.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/constants/**/*.{ts,tsx} : Organize constants by domain in `lib/constants/` submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via `lib/constants/index.ts`

Applied to files:

  • lib/db/repos/tracked-domains.ts
  • components/dashboard/domain-filters.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Applied to files:

  • lib/db/repos/tracked-domains.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Drizzle ORM schema, migrations, and repository layer should be organized in `lib/db/` with repos in `lib/db/repos/` for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)

Applied to files:

  • lib/db/repos/tracked-domains.ts
🧬 Code graph analysis (4)
hooks/use-domain-verification.ts (3)
components/dashboard/add-domain/add-domain-dialog.tsx (1)
  • ResumeDomainData (24-24)
lib/schemas/internal/verification.ts (1)
  • VerificationInstructions (60-62)
trpc/server.ts (1)
  • trpc (15-19)
lib/inngest/functions/auto-verify-pending-domain.ts (3)
lib/inngest/client.ts (1)
  • inngest (10-13)
lib/db/repos/tracked-domains.ts (2)
  • findTrackedDomainWithDomainName (217-236)
  • verifyTrackedDomain (436-455)
server/services/verification.ts (1)
  • tryAllVerificationMethods (74-99)
lib/db/repos/user-subscription.ts (4)
lib/schemas/internal/user.ts (1)
  • UserTier (10-10)
lib/db/client.ts (1)
  • db (27-27)
lib/db/schema.ts (2)
  • userSubscriptions (163-186)
  • users (67-78)
lib/edge-config.ts (1)
  • getMaxDomainsForTier (111-116)
lib/polar/handlers.ts (4)
lib/polar/products.ts (1)
  • getTierForProductId (66-73)
lib/db/repos/user-subscription.ts (3)
  • updateUserTier (59-84)
  • clearSubscriptionEndsAt (124-141)
  • setSubscriptionEndsAt (100-118)
lib/polar/emails.ts (3)
  • sendProUpgradeEmail (31-82)
  • sendSubscriptionCancelingEmail (150-215)
  • sendSubscriptionExpiredEmail (221-273)
lib/polar/downgrade.ts (1)
  • handleDowngrade (20-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: codecov/project
🔇 Additional comments (38)
lib/inngest/functions/auto-verify-pending-domain.ts (1)

33-39: Nice staged retry schedule and concurrency configuration

The staged RETRY_DELAYS plus the internal retry loop with step.sleep and step.run reads clean and matches the desired “front‑loaded then backing‑off” schedule. Combined with concurrency: { limit: 1, key: "event.data.trackedDomainId" }, this gives you:

  • A single in‑flight verification schedule per tracked domain.
  • Early exit on deletion / prior verification.
  • Idempotent, clearly labeled steps for observability.

This looks well‑structured and consistent with the Inngest usage pattern described in the repo’s guidelines.

Also applies to: 73-151

components/auth/animated-background.tsx (2)

1-11: Excellent accessibility implementation with reduced motion.

The component properly respects prefers-reduced-motion for accessibility. The motion package (v12.23.25) is correctly installed in dependencies, and the motion/react import path is valid.


18-82: Well-structured animated blobs with proper accessibility and layout safety.

The varied animation durations (25s, 31s, 19s) create an organic, non-repetitive feel. The implementation correctly respects reduced motion preferences via useReducedMotion hook. Container positioning with fixed, pointer-events-none, -z-10, and overflow-hidden ensures the blobs don't cause layout shift or interfere with user interactions. All code follows project guidelines: kebab-case filename, PascalCase export, 2-space indentation.

components/dashboard/domain-filters.tsx (1)

1-319: Overall DomainFilters implementation looks solid

Clean, well-typed, and cohesive component: controlled props/callbacks, discriminated union for chips, and mobile/desktop layouts all look correct with no blocking issues identified.

lib/inngest/functions/check-subscription-expiry.ts (6)

1-16: LGTM! Well-structured imports and logger setup.

The file correctly uses server-only and organizes imports appropriately. The logger source is descriptive and follows the established pattern.


64-71: LGTM! Well-documented idempotency key generation.

The inclusion of endsAt date in the idempotency key is a thoughtful design that correctly handles re-subscription scenarios within Resend's idempotency window.


81-91: LGTM! Correct notification escalation logic.

The function properly ensures notifications are only sent when more urgent than previous ones, preventing duplicate notifications.


101-131: LGTM! Well-structured Inngest cron function.

Good use of:

  • Concurrency limit of 1 to prevent race conditions
  • Step-based execution for durability
  • Comprehensive results tracking

136-187: LGTM! Correct filtering and per-user step execution.

The skip conditions are well-ordered and comprehensive. Using individual step.run calls per user provides good failure isolation and allows partial progress if some emails fail.


194-279: LGTM! Robust email sending with appropriate error handling.

The function correctly:

  • Uses idempotency keys to prevent duplicate emails
  • Returns false on failure rather than throwing (preventing spam on retries)
  • Provides comprehensive logging for observability
lib/db/repos/user-subscription.ts (8)

1-11: LGTM! Proper repository setup.

Follows coding guidelines with server-only import, proper organization in lib/db/repos/, and descriptive logger source.


25-50: LGTM! Correct subscription retrieval with tier-based limits.

The function correctly throws when subscription is not found (as it should always exist via better-auth hooks) and properly derives maxDomains from Edge Config.


59-84: LGTM! Resilient tier update with fallback creation.

The defensive handling of missing subscription records (creating one if the better-auth hook failed) ensures webhook handlers don't fail permanently, which is the right design for reliability.


89-95: LGTM!

Simple and correct domain limit check.


100-118: LGTM! Proper end date management.

Correctly throws when subscription is not found and logs the ISO-formatted date for observability.


124-141: LGTM! Graceful clearing with notification reset.

Correctly clears both endsAt and lastExpiryNotification to reset the notification cycle when a user re-subscribes. The graceful handling of missing records (warn instead of throw) is appropriate for this use case.


150-162: LGTM! Idempotent subscription creation.

The onConflictDoNothing pattern correctly handles race conditions, and suppressing errors to avoid breaking user signup is the right tradeoff (logged for debugging).


178-204: LGTM! Correct query for ending subscriptions.

The query correctly joins to get user details and filters for future end dates. The post-query filter is a valid TypeScript workaround for type narrowing after isNotNull.

lib/db/repos/tracked-domains.ts (8)

1-30: LGTM! Proper repository setup with comprehensive imports.

Follows coding guidelines with server-only import and proper organization. The imported operators and aliases support the complex join queries needed for provider information.


109-170: LGTM! Proper atomic limit checking with transactions.

The transaction-based approach correctly prevents race conditions where concurrent domain additions could exceed limits. The discriminated union return type makes error handling explicit and type-safe.


172-236: LGTM! Correct find operations with proper null handling.

The find functions correctly use .limit(1) and return null for missing records via the ?? null pattern.


298-354: LGTM! Well-designed query helper with proper table aliasing.

The private helper correctly:

  • Uses aliases for the providers table joined multiple times
  • Uses left joins for optional provider relationships
  • Centralizes complex join logic for reuse

382-430: LGTM! Correct count operations with proper filtering.

The as SQL casts are necessary due to drizzle's type inference for and(), and the comments explain this appropriately.


436-503: LGTM! Correct verification and notification override operations.

The verification functions properly handle state transitions, and updateNotificationOverrides correctly performs partial merges (per the updated JSDoc from past review feedback).


819-885: LGTM! Robust unarchive with atomic limit checking.

The transaction-based approach correctly:

  • Verifies ownership before unarchiving
  • Checks current count within the same transaction
  • Returns discriminated union for clear error handling

957-975: Archived domains are not filtered from stale unverified cleanup.

getStaleUnverifiedDomains doesn't exclude archived domains from deletion. The userTrackedDomains table has an archivedAt field, but the function only filters on verified = false and createdAt < cutoffDate. Archived unverified domains that are stale would still be selected for deletion. Clarify whether archived domains should be excluded from this cleanup operation. If intentional, add a comment explaining why. If not, add isNull(userTrackedDomains.archivedAt) to the where clause.

lib/polar/handlers.test.ts (6)

1-57: LGTM! Well-structured test setup with proper mocking.

The test file correctly:

  • Extracts payload types from WebhooksOptions (matching the handler implementation)
  • Mocks all external dependencies
  • Imports mocked modules after vi.mock() calls

58-131: LGTM! Clean test helper factories.

The helper factories provide sensible defaults and allow easy overriding of specific fields, making tests concise and readable. The type assertions are appropriate given the partial mock data approach.


133-147: LGTM! Correct test for subscription created (pre-payment).

Correctly verifies that handleSubscriptionCreated doesn't trigger tier changes since payment hasn't been confirmed yet.


149-221: LGTM! Comprehensive test coverage for subscription activation.

Tests cover:

  • Happy path (tier upgrade + email)
  • Edge cases (unknown product, missing userId)
  • Error behavior (re-throws for retry, swallows email errors)

223-311: LGTM! Good test coverage for subscription cancellation.

Tests correctly verify:

  • End date is set when currentPeriodEnd is provided
  • No action when currentPeriodEnd is null or userId is missing
  • Error re-throwing and email failure tolerance

313-369: LGTM! Complete test coverage for subscription revocation.

Tests verify the full downgrade flow including:

  • handleDowngrade called with userId
  • clearSubscriptionEndsAt called
  • Email sent with correct archivedCount
  • Error handling for both critical and non-critical failures
lib/polar/handlers.ts (5)

1-18: LGTM! Clean imports and type extraction.

The type extraction from WebhooksOptions ensures compatibility with better-auth's webhook plugin. The comments explain the approach clearly.


45-58: LGTM! Correct observability-only handler for pending subscriptions.

Correctly logs the event for observability without triggering tier changes before payment confirmation. PII concerns from past review have been addressed.


66-113: LGTM! Correct tier upgrade flow with proper error handling.

The handler correctly:

  • Validates required fields before proceeding
  • Updates tier then clears pending cancellation
  • Treats email sending as best-effort (doesn't fail webhook)
  • Re-throws critical errors for webhook retry

122-163: LGTM! Correct cancellation handling.

The handler correctly:

  • Sets the subscription end date for dashboard visibility
  • Defers reminder emails to the cron job (check-subscription-expiry)
  • Sends immediate confirmation email as best-effort
  • Re-throws critical errors for retry

176-212: LGTM! Complete revocation handling with expiration email.

The handler correctly:

  • Downgrades user and captures archived domain count
  • Clears subscription end date after downgrade completes
  • Sends expiration email with archivedCount (addressing past review feedback)
  • Handles errors appropriately (rethrow critical, log non-critical)
hooks/use-domain-verification.ts (1)

33-118: Overall hook structure and state transitions look solid

The multi‑step flow (step 1→3), resume handling (resumeDomain + open effects), and dialog reset logic (resetDialog + handleOpenChange) are coherent and line up with how the TRPC mutations/queries are wired. Dependencies on useEffect/useCallback also look compatible with react-hooks/exhaustive-deps. I don’t see any blocking issues in this section.

…t option check and enhancing useDomainVerification hook with clearer state management and error handling
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
lib/inngest/functions/check-subscription-expiry.ts (1)

226-232: Remove unnecessary type cast for React element.

The explicit cast to React.ReactElement at line 231 is redundant since SubscriptionCancelingEmail() already returns a valid React element that render() can process.

Apply this diff:

-    const emailHtml = await render(
-      SubscriptionCancelingEmail({
-        userName: firstName,
-        endDate,
-        dashboardUrl,
-      }) as React.ReactElement,
-    );
+    const emailHtml = await render(
+      SubscriptionCancelingEmail({
+        userName: firstName,
+        endDate,
+        dashboardUrl,
+      }),
+    );
components/dashboard/domain-filters.tsx (1)

1-320: Consider minor refactoring to meet file length guideline.

The file is 320 lines, slightly exceeding the recommended ≈≤300 LOC guideline. Potential options to reduce length:

  1. Extract STATUS_OPTIONS and HEALTH_OPTIONS (lines 57-66) to lib/constants/domain-filters.ts if they're reused elsewhere.
  2. Extract the filterContent JSX (lines 156-261) into a separate helper component if further complexity is added.

The current organization is readable and cohesive, so this refactor can be deferred.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f99e51 and 1c3fbf8.

📒 Files selected for processing (5)
  • components/dashboard/domain-filters.tsx (1 hunks)
  • hooks/use-domain-verification.ts (1 hunks)
  • lib/db/repos/user-subscription.ts (1 hunks)
  • lib/inngest/functions/auto-verify-pending-domain.ts (1 hunks)
  • lib/inngest/functions/check-subscription-expiry.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/inngest/functions/auto-verify-pending-domain.ts
  • hooks/use-domain-verification.ts
🧰 Additional context used
📓 Path-based instructions (5)
lib/inngest/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Inngest client and functions for event-driven background section revalidation should be organized in lib/inngest/ with proper structure

Files:

  • lib/inngest/functions/check-subscription-expiry.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • lib/inngest/functions/check-subscription-expiry.ts
  • components/dashboard/domain-filters.tsx
  • lib/db/repos/user-subscription.ts
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • lib/inngest/functions/check-subscription-expiry.ts
  • components/dashboard/domain-filters.tsx
  • lib/db/repos/user-subscription.ts
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/dashboard/domain-filters.tsx
lib/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

lib/db/**/*.{ts,tsx}: Drizzle ORM schema, migrations, and repository layer should be organized in lib/db/ with repos in lib/db/repos/ for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)
Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Files:

  • lib/db/repos/user-subscription.ts
🧠 Learnings (3)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/inngest/**/*.{ts,tsx} : Inngest client and functions for event-driven background section revalidation should be organized in `lib/inngest/` with proper structure

Applied to files:

  • lib/inngest/functions/check-subscription-expiry.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/constants/**/*.{ts,tsx} : Organize constants by domain in `lib/constants/` submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via `lib/constants/index.ts`

Applied to files:

  • components/dashboard/domain-filters.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Drizzle ORM schema, migrations, and repository layer should be organized in `lib/db/` with repos in `lib/db/repos/` for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)

Applied to files:

  • lib/db/repos/user-subscription.ts
🧬 Code graph analysis (3)
lib/inngest/functions/check-subscription-expiry.ts (5)
lib/inngest/client.ts (1)
  • inngest (10-13)
lib/db/repos/user-subscription.ts (2)
  • getUsersWithEndingSubscriptions (178-204)
  • setLastExpiryNotification (210-234)
lib/resend.ts (2)
  • resend (14-14)
  • RESEND_FROM_EMAIL (16-17)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
emails/subscription-canceling.tsx (1)
  • SubscriptionCancelingEmail (21-86)
components/dashboard/domain-filters.tsx (5)
hooks/use-domain-filters.ts (2)
  • StatusFilter (9-9)
  • HealthFilter (10-10)
hooks/use-view-preference.ts (1)
  • ViewMode (5-5)
hooks/use-sort-preference.ts (2)
  • SortOption (6-11)
  • SORT_OPTIONS (20-46)
components/ui/multi-select.tsx (1)
  • MultiSelect (50-125)
lib/utils.ts (1)
  • cn (4-6)
lib/db/repos/user-subscription.ts (4)
lib/schemas/internal/user.ts (1)
  • UserTier (10-10)
lib/db/client.ts (1)
  • db (27-27)
lib/db/schema.ts (2)
  • userSubscriptions (163-186)
  • users (67-78)
lib/edge-config.ts (1)
  • getMaxDomainsForTier (111-116)
🔇 Additional comments (3)
lib/inngest/functions/check-subscription-expiry.ts (1)

1-280: Well-structured subscription expiry workflow following established patterns.

This implementation mirrors the domain and certificate expiry notification patterns effectively. The use of idempotency keys, threshold-based notifications, and defensive error handling demonstrates good engineering practices.

lib/db/repos/user-subscription.ts (1)

1-234: Well-architected repository layer with proper defensive patterns.

The module provides a clean abstraction over subscription data with consistent error handling, logging, and idempotency. The defensive fallbacks in updateUserTier and createSubscription ensure resilience against edge cases without breaking user flows.

components/dashboard/domain-filters.tsx (1)

1-320: Excellent implementation with proper fix for search filter chips.

The component is well-structured and addresses the previous review concern about search filter inconsistency. Key strengths:

  • Past issue resolved: Search is now included in activeFilterChips (lines 102-110), making the badge count and chip display consistent with hasActiveFilters.
  • Type safety: The discriminated union FilterChip (lines 68-73) enables type-safe filter removal without runtime checks.
  • Performance: Proper use of useMemo for TLD options and useCallback for the remove handler.
  • Accessibility: Clear aria-labels on interactive elements (lines 174, 310).
  • Responsive design: Mobile collapsible view with accurate filter count badge.

…emoving unused queries, enhancing subscription management, and improving loading states for better user experience
…Skeleton component and enhance skeleton structure in DashboardSkeleton for improved consistency and layout
…new dashboard features, subscription management, and updated email notifications for improved clarity and usability
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
components/dashboard/settings-content.tsx (1)

447-480: Consider removing duplicate skeleton component.

SettingsContentSkeleton appears to duplicate the functionality of SettingsSkeleton which is already imported on line 11. The loading state on line 202 uses SettingsSkeleton, making this local skeleton potentially unused.

If SettingsContentSkeleton serves a different purpose than SettingsSkeleton, consider documenting the distinction. Otherwise, remove it to reduce maintenance overhead:

-/**
- * Loading skeleton for settings content
- */
-export function SettingsContentSkeleton({
-  showCard = true,
-}: {
-  showCard?: boolean;
-}) {
-  const skeletonContent = (
-    <div className="space-y-6">
-      {/* Subscription skeleton */}
-      <div className="space-y-4">
-        <Skeleton className="h-6 w-32" />
-        <Skeleton className="h-4 w-48" />
-        <Skeleton className="h-20 w-full rounded-xl" />
-        <Skeleton className="h-10 w-full rounded-lg" />
-      </div>
-      <div className="h-px bg-border/50" />
-      {/* Notifications skeleton */}
-      <div className="space-y-4">
-        <Skeleton className="h-6 w-40" />
-        <Skeleton className="h-4 w-56" />
-        <div className="space-y-2">
-          <Skeleton className="h-14 w-full rounded-xl" />
-          <Skeleton className="h-14 w-full rounded-xl" />
-          <Skeleton className="h-14 w-full rounded-xl" />
-        </div>
-      </div>
-    </div>
-  );
-
-  if (!showCard) {
-    return <div className="py-1">{skeletonContent}</div>;
-  }
-
-  return <Card className="p-6">{skeletonContent}</Card>;
-}
lib/constants/domain-filters.ts (1)

1-14: Typed filter options look good; consider moving filter types into constants to avoid hooks dependency

The STATUS_OPTIONS and HEALTH_OPTIONS arrays are well-typed and match the StatusFilter / HealthFilter unions used by the dashboard filters.

To keep layering clean (constants as a low-level module) and avoid lib/constants depending on hooks/, consider moving the filter type definitions into this module (or a shared domain types module) and having hooks/use-domain-filters.ts import them from here instead of the other way around. For example:

-import type { HealthFilter, StatusFilter } from "@/hooks/use-domain-filters";
+export type StatusFilter = "verified" | "pending";
+export type HealthFilter = "healthy" | "expiring" | "expired";

Then update the hook to import these types from @/lib/constants / @/lib/constants/domain-filters.

components/dashboard/subscription-section.tsx (3)

37-37: Consider clamping percentage to a maximum of 100.

If activeCount exceeds maxDomains, the percentage will be greater than 100%. While the Progress component likely handles this gracefully, explicitly clamping ensures expected behavior.

Apply this diff:

-  const percentage = maxDomains > 0 ? (activeCount / maxDomains) * 100 : 0;
+  const percentage = maxDomains > 0 ? Math.min((activeCount / maxDomains) * 100, 100) : 0;

41-41: Simplify conditional className logic.

The ternary on line 52 duplicates "space-y-4" in both branches. Consider using a utility like cn() to avoid duplication and improve readability.

Example for line 52:

-        <CardContent className={showCard ? "space-y-4" : "space-y-4 px-0 pb-0"}>
+        <CardContent className={cn("space-y-4", !showCard && "px-0 pb-0")}>

(You'll need to import cn from @/lib/utils if not already present.)

Also applies to: 52-52


118-120: Card sub-components used outside Card wrapper.

When showCard={false}, the CardHeader and CardContent components render outside of a Card wrapper. While this works (they're just styled divs), it's semantically unusual. The JSDoc comment indicates this is intentional for modal usage, which is reasonable. Consider whether alternative, more semantic wrapper components might be clearer for future maintainers.

components/dashboard/dashboard-content.tsx (1)

329-455: Base optimistic count deltas on actual archived/deleted items, not just ID length

In the bulk archive/delete onMutate handlers, activeCount/archivedCount adjustments use trackedDomainIds.length. If, for any reason, some selected IDs are no longer present in the current cached lists (e.g., concurrent changes), the item arrays and limit counts could briefly diverge until invalidation.

You can make the optimistic state more robust by deriving deltas from the actual intersection:

- const idsSet = new Set(trackedDomainIds);
- const domainsToArchive = (
-   previousDomains as TrackedDomainWithDetails[] | undefined
- )?.filter((d) => idsSet.has(d.id));
+ const idsSet = new Set(trackedDomainIds);
+ const domainsToArchive = (
+   previousDomains as TrackedDomainWithDetails[] | undefined
+ )?.filter((d) => idsSet.has(d.id));
+ const archivedDelta = domainsToArchive?.length ?? 0;
...
- activeCount: Math.max(
-   0,
-   old.activeCount - trackedDomainIds.length,
- ),
- archivedCount: old.archivedCount + trackedDomainIds.length,
+ activeCount: Math.max(0, old.activeCount - archivedDelta),
+ archivedCount: old.archivedCount + archivedDelta,

and similarly for bulk delete using the number of actually removed domains instead of trackedDomainIds.length. Not critical because you invalidate after settle, but this keeps the optimistic view internally consistent even under races.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c3fbf8 and 538601f.

📒 Files selected for processing (15)
  • AGENTS.md (3 hunks)
  • README.md (1 hunks)
  • app/dashboard/loading.tsx (1 hunks)
  • app/dashboard/settings/page.tsx (1 hunks)
  • components/auth/user-menu.tsx (1 hunks)
  • components/dashboard/dashboard-content.tsx (1 hunks)
  • components/dashboard/domain-filters.tsx (1 hunks)
  • components/dashboard/settings-content.tsx (1 hunks)
  • components/dashboard/settings/domain-notification-row.tsx (1 hunks)
  • components/dashboard/settings/global-notification-row.tsx (1 hunks)
  • components/dashboard/settings/settings-skeleton.tsx (1 hunks)
  • components/dashboard/subscription-section.tsx (1 hunks)
  • components/domain/domain-loading-state.tsx (1 hunks)
  • lib/constants/domain-filters.ts (1 hunks)
  • lib/constants/index.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • components/dashboard/settings/settings-skeleton.tsx
  • components/dashboard/domain-filters.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/dashboard/loading.tsx
  • lib/constants/index.ts
🧰 Additional context used
📓 Path-based instructions (6)
lib/constants/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Organize constants by domain in lib/constants/ submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via lib/constants/index.ts

Files:

  • lib/constants/domain-filters.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • lib/constants/domain-filters.ts
  • components/auth/user-menu.tsx
  • app/dashboard/settings/page.tsx
  • components/dashboard/settings/domain-notification-row.tsx
  • components/dashboard/settings/global-notification-row.tsx
  • components/dashboard/subscription-section.tsx
  • components/dashboard/dashboard-content.tsx
  • components/domain/domain-loading-state.tsx
  • components/dashboard/settings-content.tsx
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • lib/constants/domain-filters.ts
  • components/auth/user-menu.tsx
  • app/dashboard/settings/page.tsx
  • components/dashboard/settings/domain-notification-row.tsx
  • components/dashboard/settings/global-notification-row.tsx
  • components/dashboard/subscription-section.tsx
  • components/dashboard/dashboard-content.tsx
  • components/domain/domain-loading-state.tsx
  • components/dashboard/settings-content.tsx
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/auth/user-menu.tsx
  • components/dashboard/settings/domain-notification-row.tsx
  • components/dashboard/settings/global-notification-row.tsx
  • components/dashboard/subscription-section.tsx
  • components/dashboard/dashboard-content.tsx
  • components/domain/domain-loading-state.tsx
  • components/dashboard/settings-content.tsx
app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

app/**/*.tsx: Next.js App Router with default server components; keep app/page.tsx and app/api/* thin and delegate to server/ or lib/
Client components must begin with "use client" directive

Files:

  • app/dashboard/settings/page.tsx
{app/**,server/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use distinct ID sourced from PostHog cookie via cache()-wrapped getDistinctId() to comply with Next.js restrictions

Files:

  • app/dashboard/settings/page.tsx
🧠 Learnings (22)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/constants/**/*.{ts,tsx} : Organize constants by domain in `lib/constants/` submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via `lib/constants/index.ts`

Applied to files:

  • lib/constants/domain-filters.ts
  • components/dashboard/settings/domain-notification-row.tsx
  • components/domain/domain-loading-state.tsx
  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to app/**/*.tsx : Client components must begin with `"use client"` directive

Applied to files:

  • app/dashboard/settings/page.tsx
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/**/*.{ts,tsx} : Isolate DNS, RDAP/WHOIS, TLS, and header probing services in `server/services/` with tRPC routers in `server/routers/`

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/routers/**/*.{ts,tsx} : Review `trpc/init.ts` when extending procedures to ensure auth/context remain intact

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to app/**/*.tsx : Next.js App Router with default server components; keep `app/page.tsx` and `app/api/*` thin and delegate to `server/` or `lib/`

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Drizzle ORM schema, migrations, and repository layer should be organized in `lib/db/` with repos in `lib/db/repos/` for each table (domains, certificates, dns, favicons, headers, hosting, providers, registrations, screenshots, seo)

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/inngest/**/*.{ts,tsx} : Inngest client and functions for event-driven background section revalidation should be organized in `lib/inngest/` with proper structure

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {app/**,server/**}/*.{ts,tsx} : Use distinct ID sourced from PostHog cookie via `cache()`-wrapped `getDistinctId()` to comply with Next.js restrictions

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to **/*.{ts,tsx} : TypeScript only with `strict` enabled; prefer small, pure modules (≈≤300 LOC)

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.{ts,tsx} : Use kebab-case for component file names and PascalCase for exports in `components/` directory

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Server-side logging: import singleton `import { logger } from "@/lib/logger/server"` or create service logger with `createLogger({ source: "..." })`

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use Vercel Edge Config for dynamic, low-latency configuration (e.g., `domain_suggestions` array) without redeployment

Applied to files:

  • AGENTS.md
  • README.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Keep secrets in `.env.local`; see `.env.example` for required variables

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use Vercel Blob for favicon/screenshot storage with automatic public URLs; metadata cached in Postgres

Applied to files:

  • AGENTS.md
  • README.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/services/screenshot.ts : Prefer `puppeteer-core` + `sparticuz/chromium` on Vercel for screenshots

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/analytics/server.ts : Server-side analytics captured via `posthog-node` in `lib/analytics/server.ts` using `analytics.track()` and `analytics.trackException()` with Next.js 16 `after()` for background event capture

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use Next.js 16 `after()` for fire-and-forget background operations (analytics, domain access tracking) with graceful degradation

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Server logging automatically tracks critical errors in PostHog via `after()`

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to instrumentation-client.ts : Client-side analytics captured via `posthog-js` initialized in `instrumentation-client.ts`

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Use **PostHog** for analytics and error tracking with reverse proxy via `/_proxy/ingest/*`

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to {server/**,app/api/**}/*.{ts,tsx} : Server logging automatically injects OpenTelemetry trace/span IDs and correlation IDs via AsyncLocalStorage for request tracing

Applied to files:

  • AGENTS.md
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to components/**/*.{ts,tsx} : Client logging errors automatically tracked in PostHog with console output in development and always for errors

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (8)
lib/constants/domain-filters.ts (1)
hooks/use-domain-filters.ts (2)
  • StatusFilter (9-9)
  • HealthFilter (10-10)
app/dashboard/settings/page.tsx (1)
components/dashboard/settings-content.tsx (1)
  • SettingsContent (45-442)
components/dashboard/settings/domain-notification-row.tsx (5)
lib/schemas/internal/notifications.ts (1)
  • NotificationOverrides (28-28)
lib/constants/notifications.ts (3)
  • NotificationCategory (13-13)
  • NOTIFICATION_CATEGORIES (7-11)
  • NOTIFICATION_CATEGORY_INFO (16-32)
lib/utils.ts (1)
  • cn (4-6)
components/domain/favicon.tsx (1)
  • Favicon (35-135)
components/dashboard/settings/three-state-checkbox.tsx (1)
  • ThreeStateCheckbox (32-75)
components/dashboard/settings/global-notification-row.tsx (5)
lib/constants/notifications.ts (2)
  • NotificationCategory (13-13)
  • NOTIFICATION_CATEGORY_INFO (16-32)
components/dashboard/settings/index.ts (1)
  • GlobalNotificationRow (3-3)
lib/utils.ts (1)
  • cn (4-6)
components/ui/tooltip.tsx (3)
  • Tooltip (147-147)
  • TooltipTrigger (147-147)
  • TooltipContent (147-147)
components/ui/switch.tsx (1)
  • Switch (31-31)
components/dashboard/subscription-section.tsx (7)
lib/schemas/internal/user.ts (1)
  • UserTier (10-10)
hooks/use-upgrade-checkout.ts (1)
  • useUpgradeCheckout (13-44)
hooks/use-customer-portal.ts (1)
  • useCustomerPortal (12-40)
components/ui/card.tsx (5)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardDescription (90-90)
  • CardContent (91-91)
  • Card (85-85)
components/ui/progress.tsx (1)
  • Progress (31-31)
components/ui/button.tsx (1)
  • Button (60-60)
lib/polar/products.ts (1)
  • PRO_TIER_INFO (78-88)
components/dashboard/dashboard-content.tsx (4)
hooks/use-domain-verification.ts (1)
  • ResumeDomainData (13-17)
hooks/use-domain-filters.ts (1)
  • useDomainFilters (42-185)
lib/db/repos/tracked-domains.ts (1)
  • TrackedDomainWithDetails (47-67)
components/dashboard/confirm-action-dialog.tsx (1)
  • ConfirmActionDialog (26-63)
components/domain/domain-loading-state.tsx (1)
components/ui/skeleton.tsx (1)
  • Skeleton (13-13)
components/dashboard/settings-content.tsx (9)
hooks/use-upgrade-checkout.ts (1)
  • useUpgradeCheckout (13-44)
hooks/use-customer-portal.ts (1)
  • useCustomerPortal (12-40)
lib/constants/notifications.ts (2)
  • NotificationCategory (13-13)
  • NOTIFICATION_CATEGORIES (7-11)
components/dashboard/settings/settings-skeleton.tsx (1)
  • SettingsSkeleton (16-54)
components/dashboard/settings/index.ts (4)
  • SettingsSkeleton (4-4)
  • GlobalNotificationRow (3-3)
  • CategoryLabel (1-1)
  • DomainNotificationRow (2-2)
lib/utils.ts (1)
  • cn (4-6)
lib/polar/products.ts (1)
  • PRO_TIER_INFO (78-88)
components/dashboard/settings/global-notification-row.tsx (1)
  • GlobalNotificationRow (36-89)
components/dashboard/settings/domain-notification-row.tsx (1)
  • DomainNotificationRow (40-170)
🪛 LanguageTool
AGENTS.md

[grammar] ~129-~129: Ensure spelling is correct
Context: ...(). ### Re-verification & Grace Period - Inngest function reverifyDomains` runs daily a...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~154-~154: Ensure spelling is correct
Context: ...nOverrides: Per-domain overrides. ### Inngest Background Jobs - check-domain-expiry`...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (20)
app/dashboard/settings/page.tsx (1)

1-18: LGTM!

Clean page component that correctly uses the "use client" directive and delegates content rendering to SettingsContent. The structure follows the coding guidelines for thin page components that delegate to reusable components.

components/dashboard/settings/global-notification-row.tsx (1)

1-89: LGTM!

Well-structured component with clean separation of concerns. The icon mapping, tooltip for truncated descriptions, and disabled state handling are all implemented correctly. The component follows coding guidelines for client components and kebab-case file naming.

components/dashboard/settings-content.tsx (2)

72-171: Well-implemented optimistic updates.

The mutation patterns correctly implement:

  • Query cancellation before optimistic updates
  • Previous state preservation for rollback
  • Optimistic UI updates
  • Error rollback with logging and user feedback
  • Query invalidation on settlement

This provides a responsive UX while maintaining data consistency.


196-243: LGTM!

Loading and error states are handled correctly. The error UI provides a clear retry mechanism that refetches all queries. Fallback values for domains and globalPrefs are applied only after confirming no error state, which is appropriate.

components/auth/user-menu.tsx (3)

43-50: LGTM!

Good defensive handling for user.name with proper null coalescing, filtering empty strings, and fallback to "?". This addresses the previous review feedback correctly.


96-105: LGTM!

The user label section handles the name fallback correctly. The email display on line 102 doesn't have a fallback, but since email is typically required from auth providers with Better Auth, this is acceptable.


159-169: Good accessibility pattern.

Using sr-only for the DialogHeader ensures screen readers get proper context while the visual header comes from SettingsContent. The showCard={false} prop correctly adapts the content for modal presentation.

components/dashboard/settings/domain-notification-row.tsx (2)

40-170: LGTM!

Well-structured responsive component with:

  • Proper mobile/desktop view separation using Tailwind breakpoints
  • Correct disabled state handling on both views
  • ThreeStateCheckbox integration for three-way override logic
  • Grid alignment maintained with spacer when reset button is hidden
  • Tooltip on desktop reset button for better UX

63-74: Good mobile UX consideration.

Including the "Reset" text label alongside the icon on mobile provides better clarity on smaller screens where tooltip hover isn't practical. This is a thoughtful responsive design choice.

README.md (1)

12-31: README feature & stack updates clearly match new tracking/subscription system

The new feature bullets and tech stack list accurately describe the domain tracking dashboard, Pro tier limits (5 vs 50 domains), email notifications, advanced dashboard views, and Edge Config–backed tier limits introduced in this PR. No changes needed.

AGENTS.md (1)

5-248: Agent guidelines comprehensively document new auth, tracking, notification, & subscription surfaces

This update does a solid job of mapping the new features to concrete files and concepts (better-auth config, tracking router, repos, Inngest jobs, notification categories/thresholds, Edge Config tier_limits, filter constants, Polar flows, and TanStack Query patterns). It should be very helpful both for future contributors and for tooling/agents navigating the codebase. No changes needed.

components/domain/domain-loading-state.tsx (1)

17-35: Header skeleton layout looks consistent and responsive

The split into left (favicon + domain name) and right (track/export/tools) with responsive widths and comments closely mirrors a real header and should produce a clear, stable loading state on both mobile and desktop. No issues from a logic or accessibility standpoint (given these are non-interactive skeletons).

components/dashboard/dashboard-content.tsx (8)

39-82: Good separation of state, limits/domains queries, and filter wiring

The top of DashboardContent cleanly separates local UI state, TRPC queries, and filter/sort preferences. Passing domainsQuery.data ?? [] into useDomainFilters and layering sortDomains only for grid view keeps concerns well isolated and avoids coupling table sort with client-side sort logic.


99-107: ?upgraded=true handling is safe in client-only context

Using useEffect with window.location.search and router.replace("/dashboard", { scroll: false }) is appropriate here given "use client" at the top. This avoids SSR issues while ensuring the upgraded banner shows once and then the URL is cleaned up without a full navigation.


114-168: Remove mutation optimistic updates are sound

The removeMutation onMutate handler correctly:

  • Cancels relevant queries.
  • Snapshots previous domains/limits.
  • Optimistically filters the removed domain and decrements activeCount with clamping and updated canAddMore.
    Rollback and invalidation paths are also in place, so the optimistic state should stay consistent even under errors.

170-327: Archive/unarchive optimistic flows are symmetric and robust

The archive/unarchive mutations:

  • Move items between active and archived lists in cache.
  • Adjust archivedAt to new Date() on archive and back to null on unarchive.
  • Keep activeCount, archivedCount, and canAddMore in sync with those moves.
    Error handlers correctly restore snapshots, and the unarchive success handler flipping activeTab back to "active" is a nice UX touch. No correctness issues spotted.

486-601: Confirm-action and unarchive handlers are straightforward and side-effect safe

handleRemove, handleArchive, and the bulk handlers only stage a confirmAction, with actual mutations centralized in handleConfirmAction. Clearing confirmAction after execution and resetting resumeDomain when dialogs close avoids stale state. The unarchive handler is a simple wrapper around the mutation and keeps the component logic readable.


647-667: Loading and error gating correctly short-circuit render

Computing isLoading/hasError across the three queries and early-returning <DashboardSkeleton /> or <DashboardError /> prevents the main dashboard UI from rendering against partial/undefined data. The handleRetry callback only refetches errored queries, which is a nice touch to avoid unnecessary network work.


678-823: Dashboard layout wiring and props flow look coherent

The main render tree:

  • Wires DashboardHeader, banners, UpgradePrompt, TrackedDomainsView, and ArchivedDomainsView with the right derived counts, tier, filters, and selection.
  • Correctly guards HealthSummary and filters to only show when there are domains.
  • Uses archivedCount > 0 to conditionally show the “View archived domains” affordance and passes canUnarchive based on current limits.

The confirmation dialog uses confirmDialogContent plus open={confirmAction !== null} and clears state via onOpenChange, which ties the UX together cleanly.


827-910: Skeleton components align well with the real dashboard structure

DashboardSkeleton and DomainCardSkeleton mirror the structure of DashboardHeader and the domain cards (favicon, title, badges, info rows). Using consistent spacing, rounded corners, and grid breakpoints should make the loading state feel very close to the final UI, and the small list [1, 2, 3].map(...) is a simple, effective way to show multiple placeholder cards.

…ual counts for optimistic updates and streamline domain filter types for better clarity and organization
…ncing upgrade prompts and email notifications with accurate domain tracking capabilities
…ariables for staging and production environments
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
components/dashboard/settings-content.tsx (1)

250-337: Add accessible label to the subscription usage Progress bar

The subscription logic is solid, but the Progress here lacks labeling, so screen readers won’t know what it represents. You already addressed this in SubscriptionSection; mirroring that here would keep things consistent:

-            <Progress value={percentage} className="w-24" />
+            <Progress
+              value={percentage}
+              className="w-24"
+              aria-label="Domain usage"
+              aria-valuetext={`${activeCount} of ${maxDomains} domains used`}
+            />
🧹 Nitpick comments (3)
lib/constants/domain-filters.ts (1)

1-18: Unify filter types/options with use-domain-filters and constants index

The filter unions and option arrays look good, but it seems StatusFilter / HealthFilter are also defined in hooks/use-domain-filters.ts. Consider making this file the single source of truth (types and options) and importing from here in the hook to avoid drift, and ensure it’s re-exported via lib/constants/index.ts / appropriate submodule for consistency with your constants layout.

Based on learnings, this keeps constants organized and centralized.

components/dashboard/dashboard-content.tsx (1)

40-487: Consider extracting mutation/cache logic and view sections from DashboardContent

The optimistic mutations and view logic look correct, but this component is doing a lot: multiple queries, four mutations with similar limit-cache updates, selection, filters, and both active/archived views in one file. That makes it harder to reason about and evolve.

Consider:

  • Extracting shared “update limits” helpers for the various mutations (remove/archive/unarchive/bulk) to ensure semantics stay in sync.
  • Splitting the component into smaller pieces (e.g., data/mutation hook, active-view subcomponent, archived-view subcomponent) to better align with your “small, focused modules” guideline and simplify future changes.
lib/polar/products.ts (1)

78-85: Consider using Array.find() for more idiomatic code.

The function correctly implements the reverse lookup, but could be more concise using find():

 export function getTierForProductId(productId: string): UserTier | null {
-  for (const product of Object.values(POLAR_PRODUCTS)) {
-    if (product.productId === productId) {
-      return product.tier;
-    }
-  }
-  return null;
+  const product = Object.values(POLAR_PRODUCTS).find(
+    (p) => p.productId === productId
+  );
+  return product?.tier ?? null;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 538601f and 368dac4.

📒 Files selected for processing (19)
  • .env.example (1 hunks)
  • components/dashboard/dashboard-content.tsx (1 hunks)
  • components/dashboard/domain-filters.tsx (1 hunks)
  • components/dashboard/settings-content.tsx (1 hunks)
  • components/dashboard/subscription-section.tsx (1 hunks)
  • components/dashboard/tracked-domains-grid.tsx (1 hunks)
  • components/dashboard/tracked-domains-table.tsx (1 hunks)
  • components/dashboard/tracked-domains-view.tsx (1 hunks)
  • components/dashboard/upgrade-banner.tsx (1 hunks)
  • components/dashboard/upgrade-card.tsx (1 hunks)
  • components/dashboard/upgrade-prompt.tsx (1 hunks)
  • emails/pro-upgrade-success.tsx (1 hunks)
  • emails/pro-welcome.tsx (1 hunks)
  • emails/subscription-expired.tsx (1 hunks)
  • hooks/use-domain-filters.ts (1 hunks)
  • lib/constants/domain-filters.ts (1 hunks)
  • lib/polar/emails.ts (1 hunks)
  • lib/polar/products.test.ts (1 hunks)
  • lib/polar/products.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • components/dashboard/domain-filters.tsx
  • components/dashboard/tracked-domains-view.tsx
  • components/dashboard/upgrade-banner.tsx
  • components/dashboard/upgrade-card.tsx
  • components/dashboard/tracked-domains-grid.tsx
  • lib/polar/products.test.ts
  • hooks/use-domain-filters.ts
  • components/dashboard/upgrade-prompt.tsx
🧰 Additional context used
📓 Path-based instructions (4)
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/dashboard/settings-content.tsx
  • components/dashboard/tracked-domains-table.tsx
  • components/dashboard/subscription-section.tsx
  • components/dashboard/dashboard-content.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • components/dashboard/settings-content.tsx
  • components/dashboard/tracked-domains-table.tsx
  • emails/pro-welcome.tsx
  • components/dashboard/subscription-section.tsx
  • components/dashboard/dashboard-content.tsx
  • emails/subscription-expired.tsx
  • emails/pro-upgrade-success.tsx
  • lib/constants/domain-filters.ts
  • lib/polar/products.ts
  • lib/polar/emails.ts
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • components/dashboard/settings-content.tsx
  • components/dashboard/tracked-domains-table.tsx
  • emails/pro-welcome.tsx
  • components/dashboard/subscription-section.tsx
  • components/dashboard/dashboard-content.tsx
  • emails/subscription-expired.tsx
  • emails/pro-upgrade-success.tsx
  • lib/constants/domain-filters.ts
  • lib/polar/products.ts
  • lib/polar/emails.ts
lib/constants/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Organize constants by domain in lib/constants/ submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via lib/constants/index.ts

Files:

  • lib/constants/domain-filters.ts
🧠 Learnings (2)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/constants/**/*.{ts,tsx} : Organize constants by domain in `lib/constants/` submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via `lib/constants/index.ts`

Applied to files:

  • lib/constants/domain-filters.ts
  • lib/polar/products.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Keep secrets in `.env.local`; see `.env.example` for required variables

Applied to files:

  • .env.example
🧬 Code graph analysis (5)
components/dashboard/tracked-domains-table.tsx (13)
lib/db/repos/tracked-domains.ts (2)
  • TrackedDomainWithDetails (47-67)
  • ProviderInfo (42-45)
lib/schemas/internal/user.ts (1)
  • UserTier (10-10)
components/ui/button.tsx (1)
  • Button (60-60)
lib/utils.ts (1)
  • cn (4-6)
components/ui/checkbox.tsx (1)
  • Checkbox (32-32)
components/domain/screenshot-tooltip.tsx (1)
  • ScreenshotTooltip (13-70)
components/dashboard/verification-badge.tsx (1)
  • VerificationBadge (14-60)
components/dashboard/domain-health-badge.tsx (1)
  • DomainHealthBadge (15-28)
components/ui/tooltip.tsx (3)
  • Tooltip (147-147)
  • TooltipTrigger (147-147)
  • TooltipContent (147-147)
components/domain/relative-expiry.tsx (1)
  • RelativeExpiryString (7-60)
components/ui/dropdown-menu.tsx (5)
  • DropdownMenu (242-242)
  • DropdownMenuTrigger (244-244)
  • DropdownMenuContent (245-245)
  • DropdownMenuItem (248-248)
  • DropdownMenuSeparator (252-252)
lib/db/schema.ts (1)
  • domains (304-324)
components/dashboard/upgrade-banner.tsx (1)
  • UpgradeBanner (12-60)
emails/pro-upgrade-success.tsx (1)
components/ui/button.tsx (1)
  • Button (60-60)
lib/constants/domain-filters.ts (1)
hooks/use-domain-filters.ts (2)
  • StatusFilter (15-15)
  • HealthFilter (14-14)
lib/polar/products.ts (1)
lib/schemas/internal/user.ts (1)
  • UserTier (10-10)
lib/polar/emails.ts (4)
lib/resend.ts (2)
  • resend (14-14)
  • RESEND_FROM_EMAIL (16-17)
lib/db/repos/users.ts (1)
  • getUserById (16-32)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
lib/edge-config.ts (1)
  • getTierLimits (78-105)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 19-19: [UnorderedKey] The POLAR_MONTHLY_PRODUCT_ID key should go before the POLAR_WEBHOOK_SECRET key

(UnorderedKey)

🔇 Additional comments (10)
.env.example (1)

10-24: Well-structured addition of authentication, payment, and notification configuration.

The new environment variables properly support the PR objectives (user accounts via Better Auth/GitHub, subscriptions via Polar, and email notifications via Resend). Comments clearly indicate optional overrides and their purposes. Ensure all these variables are required for local development; consider whether any have sensible defaults or can be optional for the initial setup.

emails/pro-welcome.tsx (1)

1-209: Pro welcome email template looks solid

Props, preview data, and links are wired correctly, and the content clearly communicates Pro capabilities; no issues from a correctness or structure standpoint.

emails/pro-upgrade-success.tsx (1)

1-190: Upgrade success email is well-structured and consistent

Props, preview data, and CTAs are wired correctly, and the copy accurately reflects Pro activation and limits; looks good to ship as-is.

components/dashboard/subscription-section.tsx (1)

28-78: SubscriptionSection logic and Progress accessibility look good

The Pro/Free branching, percentage calculation, and Pro tier info are correct, and adding aria-label / aria-valuetext to the Progress component improves accessibility versus earlier versions.

emails/subscription-expired.tsx (1)

15-217: Subscription expired email template looks correct and consistent

Archived-domain messaging, free/pro limits, and resubscribe/dashboard CTAs are wired correctly, with appropriate PreviewProps; no changes needed.

lib/polar/emails.ts (2)

18-26: LGTM! Robust null safety implementation.

The getFirstName helper properly addresses past review feedback by safely handling null, undefined, empty, or whitespace-only names with a friendly fallback.


141-220: LGTM! Well-designed idempotency handling.

The cancellation email flow properly uses an idempotency key to prevent duplicates on webhook retries, with a thoughtful key format that allows re-sending when users cancel again with a different end date. The inclusion of the idempotency key in logs aids debugging.

lib/polar/products.ts (3)

15-46: LGTM! Clear product configuration with helpful documentation.

The POLAR_PRODUCTS configuration is well-structured with helpful comments about Polar dashboard locations and price formatting. The distinction between monthly and yearly products is clear, and the savings field on the yearly plan is a nice UX touch.


91-111: LGTM! Well-designed pattern for static and dynamic tier info.

The separation between static PRO_TIER_INFO and dynamic getProTierInfo(proMaxDomains) is clean, with a helpful comment explaining when to use each. This pattern works well for UI components that may or may not have access to edge config limits.


9-13: No changes needed. The fallback to production Polar product IDs is intentional and documented design. The code explicitly allows environment-specific overrides via environment variables (lines 10, 13), with instructions in the comments directing developers to set these for different environments (sandbox vs production). The pattern ensures a sane default when env vars are not set while enabling proper environment separation through configuration.

…mproved user awareness of subscription status
…ption expired emails to prevent duplicates on webhook retries
… for improved cache management and optimistic updates across dashboard components
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (7)
components/dashboard/dashboard-content.tsx (3)

111-119: Consider preserving other query params when clearing ?upgraded=true

router.replace("/dashboard", { scroll: false }) will drop any other existing query parameters on that URL. If users might arrive with additional filters or tracking params, you could instead remove only upgraded and keep the rest:

useEffect(() => {
  const params = new URLSearchParams(window.location.search);
  if (params.get("upgraded") === "true") {
    setShowUpgradedBanner(true);
-   // Clear the query param from URL without triggering navigation
-   router.replace("/dashboard", { scroll: false });
+   // Clear only the `upgraded` param while preserving others
+   params.delete("upgraded");
+   const newSearch = params.toString();
+   const newUrl =
+     window.location.pathname + (newSearch ? `?${newSearch}` : "");
+   router.replace(newUrl, { scroll: false });
  }
}, [router]);

381-447: Avoid recreating small wrapper callbacks for onRemove / onArchive

TrackedDomainsView and ArchivedDomainsView already call onRemove / onArchive with the correct arguments, so wrapping them in inline arrows (e.g. (id, domainName) => handleRemove(id, domainName)) is unnecessary and allocates new functions each render. You can pass the handlers directly:

-            onRemove={(id, domainName) => handleRemove(id, domainName)}
-            onArchive={(id, domainName) => handleArchive(id, domainName)}
+            onRemove={handleRemove}
+            onArchive={handleArchive}
...
-            onRemove={(id, domainName) => handleRemove(id, domainName)}
+            onRemove={handleRemove}

This keeps props stable and shaves a bit of overhead, especially useful if these child components use memoization.

Also applies to: 449-470


40-263: Component is getting quite large; consider extracting subcomponents/hooks

DashboardContent is doing a lot: query orchestration, filter/sort/view state, banners, bulk actions, and both active/archived layouts. It’s still readable, but at ~300+ LOC you may want to tease out a few concerns (e.g., useUpgradedBannerEffect, ActiveDomainsSection, ArchivedDomainsSection, or a useBulkActions hook) to keep each unit small and focused, and to align better with the “small, pure modules (~≤300 LOC)” guideline.
As per coding guidelines, prefer smaller modules.

Also applies to: 343-491

lib/polar/emails.ts (1)

36-48: Consider extracting a helper for date string generation.

The pattern new Date().toISOString().split("T")[0] is repeated in multiple idempotency key generators (lines 37, 46, 291). A small helper would reduce duplication.

+function getTodayDateString(): string {
+  return new Date().toISOString().split("T")[0];
+}
+
 function generateUpgradeIdempotencyKey(userId: string): string {
-  const dateStr = new Date().toISOString().split("T")[0];
+  const dateStr = getTodayDateString();
   return `pro_upgrade_success:${userId}:${dateStr}`;
 }
hooks/use-domain-mutations.ts (1)

95-95: Consider adding an explicit return type.

The hook function doesn't specify an explicit return type, relying on TypeScript inference. Adding an explicit type improves IDE autocomplete and serves as documentation.

+type UseDomainMutationsReturn = {
+  removeMutation: ReturnType<typeof useMutation<unknown, Error, { trackedDomainId: string }, MutationContext>>;
+  archiveMutation: ReturnType<typeof useMutation<unknown, Error, { trackedDomainId: string }, MutationContext>>;
+  unarchiveMutation: ReturnType<typeof useMutation<unknown, Error, { trackedDomainId: string }, MutationContext>>;
+  bulkArchiveMutation: ReturnType<typeof useMutation<unknown, Error, { trackedDomainIds: string[] }, MutationContext>>;
+  bulkDeleteMutation: ReturnType<typeof useMutation<unknown, Error, { trackedDomainIds: string[] }, MutationContext>>;
+  invalidateQueries: (includeArchived?: boolean) => void;
+};
+
-export function useDomainMutations(options: MutationHandlerOptions = {}) {
+export function useDomainMutations(options: MutationHandlerOptions = {}): UseDomainMutationsReturn {

Alternatively, you can use a simpler approach with ReturnType if you prefer not to spell out all mutation types manually.

components/dashboard/settings-content.tsx (2)

1-39: LGTM! Coding guidelines followed.

The file structure correctly uses kebab-case for the filename, imports are properly aliased via @/, and the client logger singleton is imported as specified in the guidelines.

Optional enhancement: The coding guidelines mention an alternative useLogger({ component: "SettingsContent" }) hook that provides automatic component context. This could be used instead of the singleton for better log correlation, though the current approach is valid.

Based on coding guidelines: Client-side logging can use either the singleton import or the useLogger hook with component name.


46-469: Well-structured component with clear separation of concerns.

The component effectively organizes subscription management and notification preferences into distinct sections with proper loading states, error handling, and user feedback. The logic flow is clear and maintainable.

Optional consideration: The file is 469 lines, which exceeds the coding guideline's preference for ≈≤300 LOC. While the component is cohesive and well-organized, consider whether subscription and notification sections could be extracted into smaller sub-components if future changes make the file harder to navigate.

Based on coding guidelines: Prefer small, pure modules (≈≤300 LOC).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 368dac4 and 97ec978.

📒 Files selected for processing (6)
  • components/dashboard/dashboard-content.tsx (1 hunks)
  • components/dashboard/dashboard-header.tsx (1 hunks)
  • components/dashboard/settings-content.tsx (1 hunks)
  • hooks/use-domain-mutations.ts (1 hunks)
  • lib/polar/emails.ts (1 hunks)
  • lib/polar/products.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/polar/products.ts
  • components/dashboard/dashboard-header.tsx
🧰 Additional context used
📓 Path-based instructions (4)
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/dashboard/settings-content.tsx
  • components/dashboard/dashboard-content.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • components/dashboard/settings-content.tsx
  • hooks/use-domain-mutations.ts
  • components/dashboard/dashboard-content.tsx
  • lib/polar/emails.ts
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • components/dashboard/settings-content.tsx
  • hooks/use-domain-mutations.ts
  • components/dashboard/dashboard-content.tsx
  • lib/polar/emails.ts
hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use camelCase named exports for shared stateful helpers in hooks/ directory

Files:

  • hooks/use-domain-mutations.ts
🧠 Learnings (1)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/constants/**/*.{ts,tsx} : Organize constants by domain in `lib/constants/` submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via `lib/constants/index.ts`

Applied to files:

  • hooks/use-domain-mutations.ts
🧬 Code graph analysis (3)
components/dashboard/settings-content.tsx (7)
hooks/use-upgrade-checkout.ts (1)
  • useUpgradeCheckout (13-44)
hooks/use-customer-portal.ts (1)
  • useCustomerPortal (12-40)
lib/constants/notifications.ts (2)
  • NotificationCategory (13-13)
  • NOTIFICATION_CATEGORIES (7-11)
components/dashboard/settings/settings-skeleton.tsx (1)
  • SettingsSkeleton (16-54)
lib/polar/products.ts (1)
  • getProTierInfo (99-109)
components/dashboard/settings/global-notification-row.tsx (1)
  • GlobalNotificationRow (36-89)
components/dashboard/settings/domain-notification-row.tsx (1)
  • DomainNotificationRow (40-170)
hooks/use-domain-mutations.ts (2)
lib/db/repos/tracked-domains.ts (1)
  • TrackedDomainWithDetails (47-67)
trpc/server.ts (1)
  • trpc (15-19)
lib/polar/emails.ts (8)
lib/resend.ts (2)
  • resend (14-14)
  • RESEND_FROM_EMAIL (16-17)
lib/db/repos/users.ts (1)
  • getUserById (16-32)
lib/constants/app.ts (1)
  • BASE_URL (1-3)
lib/edge-config.ts (1)
  • getTierLimits (78-105)
emails/pro-upgrade-success.tsx (1)
  • ProUpgradeSuccessEmail (21-84)
emails/pro-welcome.tsx (1)
  • ProWelcomeEmail (21-102)
emails/subscription-canceling.tsx (1)
  • SubscriptionCancelingEmail (21-86)
emails/subscription-expired.tsx (1)
  • SubscriptionExpiredEmail (23-98)
🔇 Additional comments (18)
components/dashboard/dashboard-content.tsx (2)

40-63: Data fetching and loading/error handling look solid

Centralizing the three queries and gating on isLoading / isError, while defaulting derived values (?? [] / ?? 0 / ?? DEFAULT_TIER_LIMITS) gives a predictable UI state and avoids undefined access. The flow into DashboardSkeleton / DashboardError looks clean and consistent with the rest of the codebase.

Also applies to: 310-341


495-578: Skeleton components are well-aligned and lightweight

DashboardSkeleton and DomainCardSkeleton closely mirror the real layout while staying presentational-only, which should keep perceived loading smooth and avoid layout shifts. The use of simple <Skeleton> blocks and small static arrays is straightforward and maintainable.

lib/polar/emails.ts (8)

1-16: LGTM!

Imports are well-organized, using @/... aliases consistently as per coding guidelines. The server-only import properly restricts this module to server-side execution.


18-26: LGTM!

The getFirstName helper robustly handles all edge cases (null, undefined, empty, whitespace-only strings) with appropriate fallback. This addresses the previously raised concern about null safety.


28-48: LGTM!

Good use of date-based idempotency keys with clear documentation explaining the design rationale. The keys prevent duplicates within Resend's window while allowing fresh emails on re-subscription.


56-126: LGTM!

The function now includes an idempotency key to prevent duplicate emails on webhook retries, addressing the previously raised concern. The best-effort pattern for the follow-up welcome email (lines 109-116) is well-implemented with isolated error handling that won't affect the primary email's success status.


133-192: LGTM!

Good design keeping this as an internal helper function. It accepts user details as parameters rather than re-fetching, which is efficient since it's called immediately after sendProUpgradeEmail already fetched the user.


194-207: LGTM!

Smart design using endsAt date rather than today's date for the cancellation idempotency key. This correctly handles the scenario where a user cancels, resubscribes, and cancels again with a different end date.


215-280: LGTM!

Clean implementation with good UX touches—using date-fns to format the end date for both the email body and subject line gives users clear, human-readable information.


301-368: LGTM!

Good implementation that fetches both free and pro tier limits to provide helpful context in the expiration email. Including archivedCount in both the email and logs aids user communication and debugging.

hooks/use-domain-mutations.ts (4)

42-87: LGTM! Clean helper functions with defensive guards.

The limit update helpers correctly:

  • Guard against undefined with early returns
  • Use Math.max(0, ...) to prevent negative counts
  • Recalculate canAddMore consistently based on the updated activeCount and maxDomains

145-179: LGTM! Solid optimistic update pattern.

The mutation correctly implements React Query best practices:

  • Cancels in-flight queries to prevent race conditions
  • Snapshots previous state for rollback
  • Updates cache optimistically
  • Handles errors with rollback and user feedback
  • Invalidates queries on settled to ensure eventual consistency

182-289: LGTM! Archive/unarchive logic correctly handles state transitions.

Both mutations properly:

  • Move domains between active and archived lists
  • Update the archivedAt timestamp appropriately (set to new Date() for archive, null for unarchive)
  • Handle server-side limit violations in unarchive with meaningful error messages
  • Invoke optional success callbacks for UI coordination

379-388: LGTM! Clean public API.

The hook returns a well-organized object with:

  • All five mutation handlers
  • A utility invalidateQueries function with clear JSDoc explaining its use case (e.g., after adding a domain)
components/dashboard/settings-content.tsx (4)

73-172: Excellent optimistic update implementation!

The mutation patterns are robust and follow React Query best practices:

  • Proper query cancellation to avoid race conditions
  • Optimistic updates with rollback on error
  • Comprehensive error handling (restore state, log, user feedback)
  • Query invalidation on settlement for cache consistency

The pattern of returning undefined when old data doesn't exist (lines 82, 121-122, 153) is correct—React Query treats this as "skip the update," which is the intended behavior.


206-235: Solid error handling with retry capability.

The error state properly surfaces failures to the user instead of silently falling back to defaults, and provides a clear retry mechanism that refetches all required queries. The conditional wrapper based on showCard ensures consistent behavior across different usage contexts.


288-296: Safe date handling.

The format() calls are properly guarded by conditional checks ensuring subscriptionEndsAt is truthy before formatting, preventing runtime errors from null/undefined values.

Also applies to: 323-328


303-308: Good accessibility implementation.

The Progress component includes proper ARIA attributes (aria-label, aria-valuetext) that provide meaningful context for assistive technologies.

…able components with centralized formatDateTimeUtc function for consistency
…amline domain mutation logic for better readability and maintainability
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (5)
components/dashboard/tracked-domain-card.tsx (1)

161-186: UTC timezone issue has been addressed.

The previous review flagged that format(expirationDate, "yyyy-MM-dd HH:mm:ss") with " UTC" suffix was incorrect because date-fns/format uses local timezone. This has been fixed by using formatDateTimeUtc(expirationDate.toISOString()) which properly formats in UTC.

components/dashboard/tracked-domains-table.tsx (1)

190-212: UTC timezone issue has been addressed.

The previous review flagged incorrect UTC labeling. This has been fixed by using formatDateTimeUtc(date.toISOString()) on line 204, which properly formats the date in UTC.

hooks/use-domain-mutations.ts (3)

11-20: Type inference from TRPC router has been implemented.

The previous review suggested inferring LimitsData from the TRPC router output instead of manually defining it. This has been addressed using inferRouterOutputs<AppRouter> which ensures compile-time type safety.


297-346: Bulk archive mutation lacks success toast but this is intentional.

The previous review suggested adding success toasts for bulk operations. Looking at dashboard-content.tsx (lines 186-210), the executeBulkArchive function handles success toasts with partial failure reporting, which provides more nuanced feedback than a simple onSuccess handler in the mutation could provide.


348-383: Bulk delete mutation follows same pattern as bulk archive.

Success toasts are handled in dashboard-content.tsx executeBulkDelete function (lines 213-237) with proper partial failure handling.

🧹 Nitpick comments (3)
components/dashboard/dashboard-content.tsx (2)

112-123: Direct URL manipulation could cause issues with framework routing.

Using window.location.search and router.replace with a manually constructed URL bypasses Next.js's URL state management. Consider using the useSearchParams hook from next/navigation for cleaner integration.

+import { useSearchParams } from "next/navigation";
...
+const searchParams = useSearchParams();

 useEffect(() => {
-  const params = new URLSearchParams(window.location.search);
-  if (params.get("upgraded") === "true") {
+  if (searchParams.get("upgraded") === "true") {
     setShowUpgradedBanner(true);
+    const params = new URLSearchParams(searchParams.toString());
     params.delete("upgraded");
     const newSearch = params.toString();
     const newUrl =
       window.location.pathname + (newSearch ? `?${newSearch}` : "");
     router.replace(newUrl, { scroll: false });
   }
-}, [router]);
+}, [router, searchParams]);

40-496: Consider splitting this large component.

At ~500 lines, DashboardContent exceeds the ~300 LOC guideline. Consider extracting logical sections into sub-components:

  • Confirmation dialog logic → useConfirmDialog hook
  • Active/Archived tab content → separate view components
  • Banner rendering logic → DashboardBanners component

This would improve maintainability without changing functionality.

components/dashboard/tracked-domains-table.tsx (1)

164-170: Sorting function returns -1 for verified, but convention typically uses 1 for "greater".

The sorting logic works but the semantics are inverted from typical sort comparators where returning -1 means "a comes before b". Here verified domains sort first, which may be intentional, but consider adding a comment clarifying the expected sort order.

       sortingFn: (rowA, rowB) => {
+        // Sort verified domains first (verified = -1, unverified = 1)
         return rowA.original.verified === rowB.original.verified
           ? 0
           : rowA.original.verified
             ? -1
             : 1;
       },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97ec978 and e766d95.

📒 Files selected for processing (5)
  • components/dashboard/dashboard-content.tsx (1 hunks)
  • components/dashboard/tracked-domain-card.tsx (1 hunks)
  • components/dashboard/tracked-domains-table.tsx (1 hunks)
  • hooks/use-domain-mutations.ts (1 hunks)
  • lib/polar/emails.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/polar/emails.ts
🧰 Additional context used
📓 Path-based instructions (4)
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

components/**/*.{ts,tsx}: Use kebab-case for component file names and PascalCase for exports in components/ directory
Client-side logging: import singleton import { logger } from "@/lib/logger/client" or use hook useLogger({ component: "..." })
Client logging errors automatically tracked in PostHog with console output in development and always for errors
Client logging propagates correlation IDs from server via header/cookie/localStorage

Files:

  • components/dashboard/tracked-domains-table.tsx
  • components/dashboard/dashboard-content.tsx
  • components/dashboard/tracked-domain-card.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: TypeScript only with strict enabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via @/... aliases

Files:

  • components/dashboard/tracked-domains-table.tsx
  • components/dashboard/dashboard-content.tsx
  • hooks/use-domain-mutations.ts
  • components/dashboard/tracked-domain-card.tsx
**/*.{ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the project

Files:

  • components/dashboard/tracked-domains-table.tsx
  • components/dashboard/dashboard-content.tsx
  • hooks/use-domain-mutations.ts
  • components/dashboard/tracked-domain-card.tsx
hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use camelCase named exports for shared stateful helpers in hooks/ directory

Files:

  • hooks/use-domain-mutations.ts
🧠 Learnings (4)
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/constants/**/*.{ts,tsx} : Organize constants by domain in `lib/constants/` submodules (app, decay, domain-validation, external-apis, headers, ttl) and re-export via `lib/constants/index.ts`

Applied to files:

  • hooks/use-domain-mutations.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/routers/**/*.{ts,tsx} : Review `trpc/init.ts` when extending procedures to ensure auth/context remain intact

Applied to files:

  • hooks/use-domain-mutations.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to server/**/*.{ts,tsx} : Isolate DNS, RDAP/WHOIS, TLS, and header probing services in `server/services/` with tRPC routers in `server/routers/`

Applied to files:

  • hooks/use-domain-mutations.ts
📚 Learning: 2025-11-28T16:39:43.325Z
Learnt from: CR
Repo: jakejarvis/domainstack.io PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T16:39:43.325Z
Learning: Applies to lib/db/**/*.{ts,tsx} : Reuse domain Zod types for JSON columns (SEO, registration) to avoid drift between database and validation schemas

Applied to files:

  • hooks/use-domain-mutations.ts
🧬 Code graph analysis (2)
hooks/use-domain-mutations.ts (3)
server/routers/_app.ts (1)
  • AppRouter (10-10)
lib/db/repos/tracked-domains.ts (1)
  • TrackedDomainWithDetails (47-67)
trpc/server.ts (1)
  • trpc (15-19)
components/dashboard/tracked-domain-card.tsx (13)
lib/db/repos/tracked-domains.ts (2)
  • VerificationStatusType (32-33)
  • ProviderInfo (42-45)
components/dashboard/domain-health-badge.tsx (2)
  • getHealthAccent (95-111)
  • DomainHealthBadge (15-28)
components/ui/card.tsx (4)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardContent (91-91)
lib/utils.ts (1)
  • cn (4-6)
components/domain/favicon.tsx (1)
  • Favicon (35-135)
components/domain/screenshot-tooltip.tsx (1)
  • ScreenshotTooltip (13-70)
components/dashboard/verification-badge.tsx (1)
  • VerificationBadge (14-60)
lib/db/schema.ts (2)
  • verificationStatus (56-60)
  • hosting (451-478)
components/ui/dropdown-menu.tsx (5)
  • DropdownMenu (242-242)
  • DropdownMenuTrigger (244-244)
  • DropdownMenuContent (245-245)
  • DropdownMenuItem (248-248)
  • DropdownMenuSeparator (252-252)
components/ui/button.tsx (1)
  • Button (60-60)
components/ui/tooltip.tsx (3)
  • Tooltip (147-147)
  • TooltipTrigger (147-147)
  • TooltipContent (147-147)
lib/format.ts (1)
  • formatDateTimeUtc (29-56)
components/domain/relative-expiry.tsx (1)
  • RelativeExpiryString (7-60)
🔇 Additional comments (17)
components/dashboard/dashboard-content.tsx (5)

1-39: LGTM!

Clean imports organization with proper @/ aliases. The ConfirmAction discriminated union type is well-defined for the different confirmation dialog states.


40-75: LGTM!

State management is well-organized with clear separation between UI state (dialogs, banners, tabs), view preferences, and data fetching via TRPC queries. The mutation hook integration with the onUnarchiveSuccess callback is clean.


186-237: Bulk operation success toasts are handled in-component.

The executeBulkArchive and executeBulkDelete functions correctly handle success toasts with proper pluralization and partial failure warnings. This addresses the feedback pattern from the mutations hook where success toasts were noted as potentially missing.


269-310: LGTM!

The getConfirmDialogContent helper function cleanly maps action types to dialog content with appropriate destructive/default variants and well-written confirmation messages.


499-582: LGTM!

The DashboardSkeleton component provides a faithful skeleton representation of the dashboard layout, maintaining visual consistency during loading states. The DomainCardSkeleton helper is appropriately scoped as a private function.

components/dashboard/tracked-domain-card.tsx (3)

1-39: LGTM!

Clean import structure with proper @/ aliases. Good use of formatDateTimeUtc from @/lib/format which properly handles UTC conversion.


56-216: LGTM!

Well-structured card component with:

  • Proper health accent styling via getHealthAccent
  • Accessible dropdown menu with sr-only labels
  • Clean conditional rendering for verified vs unverified states
  • Good use of tooltip for full datetime display

218-254: LGTM!

The InfoRow helper component is well-designed with flexible prop handling for both provider and children patterns. The null coalescing for provider?.name correctly handles the optional provider case.

components/dashboard/tracked-domains-table.tsx (5)

1-49: LGTM!

Well-organized imports with proper @/ aliases. Good use of formatDateTimeUtc for UTC-correct date display.


61-103: LGTM!

Clean helper components:

  • ProviderCell handles null provider gracefully with em-dash fallback
  • SortableHeader provides consistent styling and sort indicator behavior

105-106: Good optimization with module-level constant.

Using EMPTY_SET as a default prevents new Set instances on each render, avoiding unnecessary re-renders of components that use referential equality checks.


365-450: LGTM!

The unverified row rendering with dynamic colspan calculation is well-implemented. Using a Map to look up cells by column ID is maintainable and clearer than index-based access.


319-487: LGTM!

Table structure is well-implemented with:

  • Proper semantic HTML (<table>, <thead>, <tbody>)
  • Accessible styling with proper contrast
  • Selection state highlighting
  • Responsive overflow handling
  • Clean upgrade banner placement for free tier
hooks/use-domain-mutations.ts (4)

48-93: LGTM!

Well-designed helper functions for limits cache updates:

  • Pure functions with clear single responsibility
  • Proper null/undefined handling with early return
  • Consistent use of Math.max(0, ...) prevents negative counts
  • canAddMore correctly recalculated based on new active count

110-148: LGTM!

Clean shared utilities for query management:

  • cancelQueries prevents race conditions during optimistic updates
  • invalidateQueries ensures fresh data after mutations
  • rollback handles error recovery with proper null checks

150-185: LGTM!

Remove mutation implementation is solid:

  • Cancels queries before optimistic update
  • Stores previous state for rollback
  • Updates both domains list and limits atomically
  • Proper error handling with logging and toast
  • Invalidates queries on settlement

385-394: LGTM!

Clean return object exposing all mutations and the invalidateQueries helper for external use (e.g., after adding a domain).

…e Next.js navigation for improved performance and maintainability
@jakejarvis jakejarvis merged commit e16c9cf into main Dec 6, 2025
2 of 4 checks passed
@jakejarvis jakejarvis deleted the feat/dashboard branch December 6, 2025 14:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants