-
Notifications
You must be signed in to change notification settings - Fork 10
feat: add user accounts, domain verification flow, and expiry notifications #212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. WalkthroughAdded 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
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
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this 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 unusedonAddDomainprop
onAddDomainis 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 fromTrackedDomainsGridPropsto 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 assertionsThe suite exercises all key branches (verified, pending, failing, status overrides, and
classNamepassthrough) 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 strategyThe 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.passwordis a plaintextcolumn—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 useuseLogger({ 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 ingetNotificationTypefor 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 unnecessarysetTimeoutin failure-timestamp testIn the
"does not overwrite existing failure time on subsequent calls"test, thesetTimeout(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 resolvedomainNamefor verification
verifyDomaincurrently callsgetTrackedDomainsForUser(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
domainsrow 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 wrappingtryAllVerificationMethodsincache
tryAllVerificationMethodsis wrapped in React’scache, 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
domainandtokencan 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
cachewrapper 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 constantsThe 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.txtBut the actual constants use
_domainstack-verifyanddomainstack-verify. To avoid confusion for future readers, updating these comments to matchDNS_VERIFICATION_HOST,DNS_VERIFICATION_PREFIX, andHTML_FILE_PATHwould be helpful.
117-126: LetfetchWithTimeoutAndRetryown the User-Agent headerIn both
verifyDnsTxt,verifyHtmlFile, andverifyMetaTag, you explicitly pass a lowercased"user-agent"header even thoughfetchWithTimeoutAndRetryalready injects"User-Agent": USER_AGENTby default. This can lead to redundant headers (User-Agentanduser-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 usingCOUNT(*)instead of selecting all rows for user domain counts
countTrackedDomainsForUsercurrently does a fullselectof all columns and then returnsrows.length. For small per-user limits this is fine, but aCOUNT(*)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
createTrackedDomainassumes 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 concurrentaddDomainrequests 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 returningnullwhen 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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 vialib/constants/index.ts
Files:
lib/constants/tier-limits.tslib/constants/index.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: TypeScript only withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
lib/constants/tier-limits.tsapp/dashboard/page.tsxcomponents/dashboard/verification-badge.tsxlib/inngest/functions/reverify-domains.tsapp/api/inngest/route.tscomponents/auth/sign-in-button.tsxcomponents/auth/user-menu.tsxcomponents/dashboard/domain-health-badge.tsxserver/routers/tracking.tscomponents/dashboard/dashboard-header.tsxlib/db/repos/user-limits.tsapp/dashboard/loading.tsxcomponents/dashboard/add-domain-dialog.tsxcomponents/layout/app-header.tsxemails/domain-expiry.tsxapp/login/page.tsxapp/dashboard/settings/page.tsxlib/auth-client.tsemails/verification-failing.tsxcomponents/dashboard/tracked-domain-card.tsxcomponents/dashboard/tracked-domains-table.tsxcomponents/dashboard/tracked-domains-grid.tsxlib/db/repos/user-limits.test.tslib/db/repos/tracked-domains.test.tscomponents/dashboard/tracked-domains-view.tsxlib/db/repos/notifications.tscomponents/dashboard/verification-badge.test.tsxlib/inngest/functions/check-domain-expiry.tslib/db/repos/tracked-domains.tslib/auth.tsapp/dashboard/layout.tsxserver/services/verification.tscomponents/auth/auth-button.tsxlib/resend.tsemails/verification-revoked.tsxlib/edge-config.test.tscomponents/brand-icons.tsxserver/routers/_app.tshooks/use-view-preference.tsapp/api/auth/[...all]/route.tslib/edge-config.tsserver/services/verification.test.tslib/constants/index.tstrpc/init.tslib/db/schema.ts
**/*.{ts,tsx,json,css}
📄 CodeRabbit inference engine (AGENTS.md)
Use 2-space indentation throughout the project
Files:
lib/constants/tier-limits.tsapp/dashboard/page.tsxcomponents/dashboard/verification-badge.tsxlib/inngest/functions/reverify-domains.tsapp/api/inngest/route.tscomponents/auth/sign-in-button.tsxcomponents/auth/user-menu.tsxdrizzle/meta/_journal.jsoncomponents/dashboard/domain-health-badge.tsxpackage.jsonserver/routers/tracking.tscomponents/dashboard/dashboard-header.tsxlib/db/repos/user-limits.tsapp/dashboard/loading.tsxcomponents/dashboard/add-domain-dialog.tsxcomponents/layout/app-header.tsxemails/domain-expiry.tsxapp/login/page.tsxapp/dashboard/settings/page.tsxlib/auth-client.tsemails/verification-failing.tsxcomponents/dashboard/tracked-domain-card.tsxcomponents/dashboard/tracked-domains-table.tsxcomponents/dashboard/tracked-domains-grid.tsxlib/db/repos/user-limits.test.tslib/db/repos/tracked-domains.test.tscomponents/dashboard/tracked-domains-view.tsxlib/db/repos/notifications.tscomponents/dashboard/verification-badge.test.tsxlib/inngest/functions/check-domain-expiry.tslib/db/repos/tracked-domains.tslib/auth.tsapp/dashboard/layout.tsxserver/services/verification.tscomponents/auth/auth-button.tsxlib/resend.tsemails/verification-revoked.tsxlib/edge-config.test.tscomponents/brand-icons.tsxserver/routers/_app.tshooks/use-view-preference.tsapp/api/auth/[...all]/route.tslib/edge-config.tsserver/services/verification.test.tslib/constants/index.tsdrizzle/meta/0004_snapshot.jsontrpc/init.tslib/db/schema.ts
app/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
app/**/*.tsx: Next.js App Router with default server components; keepapp/page.tsxandapp/api/*thin and delegate toserver/orlib/
Client components must begin with"use client"directive
Files:
app/dashboard/page.tsxapp/dashboard/loading.tsxapp/login/page.tsxapp/dashboard/settings/page.tsxapp/dashboard/layout.tsx
{app/**,server/**}/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use distinct ID sourced from PostHog cookie via
cache()-wrappedgetDistinctId()to comply with Next.js restrictions
Files:
app/dashboard/page.tsxapp/api/inngest/route.tsserver/routers/tracking.tsapp/dashboard/loading.tsxapp/login/page.tsxapp/dashboard/settings/page.tsxapp/dashboard/layout.tsxserver/services/verification.tsserver/routers/_app.tsapp/api/auth/[...all]/route.tsserver/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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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.tsxcomponents/auth/sign-in-button.tsxcomponents/auth/user-menu.tsxcomponents/dashboard/domain-health-badge.tsxcomponents/dashboard/dashboard-header.tsxcomponents/dashboard/add-domain-dialog.tsxcomponents/layout/app-header.tsxcomponents/dashboard/tracked-domain-card.tsxcomponents/dashboard/tracked-domains-table.tsxcomponents/dashboard/tracked-domains-grid.tsxcomponents/dashboard/tracked-domains-view.tsxcomponents/dashboard/verification-badge.test.tsxcomponents/auth/auth-button.tsxcomponents/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.tslib/inngest/functions/check-domain-expiry.ts
{server/**,app/api/**}/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
{server/**,app/api/**}/*.{ts,tsx}: Usedrizzle-zodfor DB boundary validation with *Select schemas fromlib/db/zod.tsfor reads and *Insert/*Update for writes, with dates coerced in write schemas
Server-side logging: import singletonimport { logger } from "@/lib/logger/server"or create service logger withcreateLogger({ 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 viaafter()
Server logging supports levels:trace,debug,info,warn,error,fatal
Files:
app/api/inngest/route.tsserver/routers/tracking.tsserver/services/verification.tsserver/routers/_app.tsapp/api/auth/[...all]/route.tsserver/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 inserver/routers/
Files:
server/routers/tracking.tsserver/services/verification.tsserver/routers/_app.tsserver/services/verification.test.ts
server/routers/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Review
trpc/init.tswhen extending procedures to ensure auth/context remain intact
Files:
server/routers/tracking.tsserver/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 inlib/db/with repos inlib/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.tslib/db/repos/user-limits.test.tslib/db/repos/tracked-domains.test.tslib/db/repos/notifications.tslib/db/repos/tracked-domains.tslib/db/schema.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.test.{ts,tsx}: Use Vitest with React Testing Library; configure invitest.config.tswiththreadspool for sandboxed environment compatibility
Mock@vercel/blob(putanddelfunctions) in tests; setBLOB_READ_WRITE_TOKENviavi.stubEnvin suites that touch uploads/deletes
MockURL.createObjectURL/revokeObjectURLwithvi.fn()in tests that need them
Logger is mocked in tests viavitest.setup.ts; usevi.mocked(logger.info)to assert log calls in tests
Files:
lib/db/repos/user-limits.test.tslib/db/repos/tracked-domains.test.tscomponents/dashboard/verification-badge.test.tsxlib/edge-config.test.tsserver/services/verification.test.ts
lib/db/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Replace
@/lib/db/clientwith a PGlite-backed instance (@/lib/db/pglite) for database tests
Files:
lib/db/repos/user-limits.test.tslib/db/repos/tracked-domains.test.ts
components/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
components/**/*.test.{ts,tsx}: Do not add direct tests forcomponents/ui/*(shadcn components); mock Radix primitives (Accordion, Tooltip) when testing domain sections
Mock tRPC/React Query for components likeFaviconandScreenshotin 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.hoistedfor 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.tsautomatically 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.tslib/inngest/functions/reverify-domains.tslib/db/repos/user-limits.tsemails/domain-expiry.tsxlib/auth-client.tslib/inngest/functions/check-domain-expiry.tslib/db/repos/tracked-domains.tsserver/services/verification.tslib/edge-config.test.tslib/edge-config.tslib/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.tslib/edge-config.test.tslib/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.tsapp/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.tsapp/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.tsserver/services/verification.tsserver/routers/_app.tsserver/services/verification.test.tstrpc/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.tslib/auth.tsserver/routers/_app.tsapp/api/auth/[...all]/route.tstrpc/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.tslib/db/repos/notifications.tslib/db/repos/tracked-domains.tslib/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.tsxapp/dashboard/layout.tsxserver/routers/_app.tsapp/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.tsxapp/dashboard/layout.tsxserver/routers/_app.tsapp/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.tsserver/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.tslib/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.tslib/db/repos/tracked-domains.test.tslib/edge-config.test.tsserver/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.tslib/db/repos/tracked-domains.test.tscomponents/dashboard/verification-badge.test.tsxlib/edge-config.test.tsserver/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.tslib/db/repos/tracked-domains.test.tslib/edge-config.test.tsserver/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.tslib/edge-config.test.tsserver/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.tslib/db/repos/tracked-domains.tslib/auth.tsdrizzle/meta/0004_snapshot.jsonlib/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.tscomponents/dashboard/verification-badge.test.tsxlib/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.tsserver/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.tslib/auth.tsserver/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)
… layout with new components
…otTooltip and improved date formatting tooltips
…ding certificate and domain expiry alerts
There was a problem hiding this 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 thepasswordfield in the OAuth accounts table.The
accountstable includes apasswordfield (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 ProvidersRendering
{auth}inside<Providers>and after the main app shell makes sense for auth-related overlays or parallel routes that should sit outside the primarychildrentree 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 guardingrouter.back()for direct visitsRight 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 missingnotificationOverridesin optimistic updateIn the domain overrides mutation, the optimistic update assumes
d.notificationOverridesis always a defined object:notificationOverrides: { ...d.notificationOverrides, ...overrides, },If the backend ever returns
notificationOverridesasnullorundefined, 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 dataWhen either query fails,
isLoadingis false anddataisundefined, so the UI quietly:
- Treats
domainsas[]and shows “Verify your domains…”- Treats
globalPrefsas 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 → inheritregardless of the global value.Also applies to: 424-472, 474-521
lib/db/repos/certificates.ts (2)
80-82: Potentially redundantisNotNullcheck.According to the schema definition,
certificates.validTois defined with.notNull(), so theisNotNull(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
getCertificateExpiryNotificationTypeandgenerateIdempotencyKeyare duplicated between this file andcheck-domain-expiry.ts. Consider:
- Adding
getCertificateExpiryNotificationTypetolib/constants/notifications.tsalongsidegetDomainExpiryNotificationType- Extracting
generateIdempotencyKeyto a shared location (e.g.,lib/inngest/utils.tsor 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.
getVerificationInstructionscurrently fetches all tracked domains for the user viagetTrackedDomainsForUser(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.
createNotificationusesonConflictDoNothing()and returnsnullwhen 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
nullclearly indicates an error.lib/db/schema.ts (1)
164-166: Simplify the field name for clarity.The field is named
verificationStatusEnumbut the SQL column isverification_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
verificationStatusEnumtoverificationStatus.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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.tsxcomponents/dashboard/verification-badge.test.tsxcomponents/auth/sign-in-button.tsxcomponents/dashboard/add-domain-dialog.tsxcomponents/dashboard/settings-content.tsxcomponents/dashboard/dashboard-header.tsxcomponents/auth/login-content.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: TypeScript only withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
components/dashboard/tracked-domains-table.tsxlib/schemas/internal/notifications.tscomponents/dashboard/verification-badge.test.tsxlib/schemas/index.tslib/db/repos/certificates.tslib/inngest/functions/check-certificate-expiry.tsapp/@auth/[...catchAll]/page.tsxcomponents/auth/sign-in-button.tsxapp/@auth/(.)login/page.tsxcomponents/dashboard/add-domain-dialog.tsxserver/routers/tracking.tscomponents/dashboard/settings-content.tsxlib/db/repos/user-notification-preferences.tsapp/layout.tsxlib/constants/notifications.tscomponents/dashboard/dashboard-header.tsxlib/inngest/functions/check-domain-expiry.tsapp/api/inngest/route.tsapp/dashboard/@settings/(.)settings/page.tsxlib/db/repos/user-limits.tsemails/certificate-expiry.tsxapp/dashboard/@settings/default.tsxlib/inngest/functions/reverify-domains.tscomponents/auth/login-content.tsxlib/db/repos/notifications.tslib/db/repos/tracked-domains.tslib/db/schema.tsserver/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.tsxlib/schemas/internal/notifications.tscomponents/dashboard/verification-badge.test.tsxlib/schemas/index.tslib/db/repos/certificates.tslib/inngest/functions/check-certificate-expiry.tsapp/@auth/[...catchAll]/page.tsxcomponents/auth/sign-in-button.tsxapp/@auth/(.)login/page.tsxcomponents/dashboard/add-domain-dialog.tsxserver/routers/tracking.tscomponents/dashboard/settings-content.tsxlib/db/repos/user-notification-preferences.tsapp/layout.tsxlib/constants/notifications.tscomponents/dashboard/dashboard-header.tsxlib/inngest/functions/check-domain-expiry.tsapp/api/inngest/route.tsapp/dashboard/@settings/(.)settings/page.tsxlib/db/repos/user-limits.tsemails/certificate-expiry.tsxapp/dashboard/@settings/default.tsxlib/inngest/functions/reverify-domains.tscomponents/auth/login-content.tsxlib/db/repos/notifications.tslib/db/repos/tracked-domains.tslib/db/schema.tsserver/services/verification.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.test.{ts,tsx}: Use Vitest with React Testing Library; configure invitest.config.tswiththreadspool for sandboxed environment compatibility
Mock@vercel/blob(putanddelfunctions) in tests; setBLOB_READ_WRITE_TOKENviavi.stubEnvin suites that touch uploads/deletes
MockURL.createObjectURL/revokeObjectURLwithvi.fn()in tests that need them
Logger is mocked in tests viavitest.setup.ts; usevi.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 forcomponents/ui/*(shadcn components); mock Radix primitives (Accordion, Tooltip) when testing domain sections
Mock tRPC/React Query for components likeFaviconandScreenshotin 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 inlib/db/with repos inlib/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.tslib/db/repos/user-notification-preferences.tslib/db/repos/user-limits.tslib/db/repos/notifications.tslib/db/repos/tracked-domains.tslib/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.tslib/inngest/functions/check-domain-expiry.tslib/inngest/functions/reverify-domains.ts
app/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
app/**/*.tsx: Next.js App Router with default server components; keepapp/page.tsxandapp/api/*thin and delegate toserver/orlib/
Client components must begin with"use client"directive
Files:
app/@auth/[...catchAll]/page.tsxapp/@auth/(.)login/page.tsxapp/layout.tsxapp/dashboard/@settings/(.)settings/page.tsxapp/dashboard/@settings/default.tsx
{app/**,server/**}/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use distinct ID sourced from PostHog cookie via
cache()-wrappedgetDistinctId()to comply with Next.js restrictions
Files:
app/@auth/[...catchAll]/page.tsxapp/@auth/(.)login/page.tsxserver/routers/tracking.tsapp/layout.tsxapp/api/inngest/route.tsapp/dashboard/@settings/(.)settings/page.tsxapp/dashboard/@settings/default.tsxserver/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 inserver/routers/
Files:
server/routers/tracking.tsserver/services/verification.ts
{server/**,app/api/**}/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
{server/**,app/api/**}/*.{ts,tsx}: Usedrizzle-zodfor DB boundary validation with *Select schemas fromlib/db/zod.tsfor reads and *Insert/*Update for writes, with dates coerced in write schemas
Server-side logging: import singletonimport { logger } from "@/lib/logger/server"or create service logger withcreateLogger({ 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 viaafter()
Server logging supports levels:trace,debug,info,warn,error,fatal
Files:
server/routers/tracking.tsapp/api/inngest/route.tsserver/services/verification.ts
server/routers/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Review
trpc/init.tswhen 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 vialib/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.tslib/db/repos/certificates.tslib/db/repos/notifications.tslib/db/repos/tracked-domains.tslib/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.tslib/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.tslib/constants/notifications.tslib/db/repos/user-limits.tsAGENTS.mdlib/inngest/functions/reverify-domains.tslib/db/repos/tracked-domains.tsserver/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.tslib/db/repos/user-notification-preferences.tslib/db/repos/user-limits.tsAGENTS.mdlib/db/repos/notifications.tslib/db/repos/tracked-domains.tslib/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.tsxapp/@auth/(.)login/page.tsxAGENTS.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.tsxAGENTS.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.tsxAGENTS.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.tsxAGENTS.mdlib/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.tsAGENTS.mdserver/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.tsAGENTS.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.mdAGENTS.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.mdAGENTS.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.tsAGENTS.mdlib/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.tslib/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.tsxfallback for the@settingsparallel route slot. Returningnullis appropriate when the settings route is not active.app/layout.tsx (1)
26-32: Auth slot prop addition looks correct and type-safeExtending
RootLayoutto accept anauth: React.ReactNodeprop viaReadonly<{ 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 appropriateUsing 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 goodThe intercepted modal wiring and reuse of
SettingsContentwithshowCard={false}are consistent and keep this route thin, as desired.components/auth/login-content.tsx (1)
14-49: Reusable login content withshowCardtoggle looks solidGood use of a shared
contentfragment and theshowCardprop 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 comprehensiveThe suite exercises all key combinations of
verified,verificationStatus, andclassName, 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 patternRe‑exporting
./internal/notificationshere 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 correctThe current
logger.error()call matches the intended API. The logger supports both 2-argumenterror(message, context)and 3-argumenterror(message, error, context)patterns (documented overloads inlib/logger/client.tslines 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 inlib/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.skippedwhen 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
sendExpiryNotificationhelper 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
userNamevalues 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
undefinedis 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 cascadeare 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: Thepasswordfield 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
NotificationOverridesfollows 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
notificationOverridesfield correctly uses the importedNotificationOverridestype 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
userLimitstable 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
userNotificationPreferencestable 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
NotificationTypeis elegant, and the metadata structure will work well for UI rendering.
…plates for consistency in branding and layout
…ry, including tests for notification types and user preferences
…ons, with corresponding tests for functionality
…ved user experience
There was a problem hiding this 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 foruser.name.This issue was previously identified: the code will throw if
user.nameis 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:
- Defines a clear
VerificationFailureActiontype- Returns the specific action from
handleVerificationFailure- Increments the correct counter (
revokedorfailing) based on the returned actionThis 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
userIdinstead ofuserEmailin 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 andheaders()usage nit
headers()fromnext/headersis synchronous, so theawaitis unnecessary, and themetadataexport can benefit from an explicitMetadatatype 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-authversions 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.getSessionwith 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.
_effectiveValueis declared but never used in the mobile view. It's computed again aseffectiveValueinThreeStateCheckboxwhen 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
TRPCErrorinside 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
getVerifiedTrackedDomainsWithExpirythat 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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 inlib/db/with repos inlib/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.tslib/db/repos/certificates.tslib/db/repos/tracked-domains.tslib/db/repos/tracked-domains.test.tslib/db/repos/notifications.tslib/db/repos/notifications.test.tslib/db/schema.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: TypeScript only withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
lib/db/repos/user-notification-preferences.test.tslib/notifications.test.tsapp/(static)/terms/page.tsxlib/inngest/functions/check-domain-expiry.tslib/inngest/functions/check-certificate-expiry.tslib/notifications.tslib/inngest/functions/reverify-domains.tsapp/login/page.tsxemails/domain-expiry.tsxapp/dashboard/layout.tsxlib/constants/notifications.test.tscomponents/auth/auth-button.tsxserver/routers/tracking.tscomponents/layout/app-footer.tsxlib/db/repos/certificates.tsapp/(static)/layout.tsxcomponents/dashboard/settings-content.tsxapp/(static)/privacy/page.tsxlib/constants/notifications.tslib/db/repos/tracked-domains.tsemails/verification-revoked.tsxlib/db/repos/tracked-domains.test.tslib/db/repos/notifications.tscomponents/auth/user-menu.tsxapp/layout.tsxlib/db/repos/notifications.test.tslib/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.tslib/notifications.test.tsapp/(static)/terms/page.tsxlib/inngest/functions/check-domain-expiry.tslib/inngest/functions/check-certificate-expiry.tslib/notifications.tslib/inngest/functions/reverify-domains.tsapp/login/page.tsxemails/domain-expiry.tsxapp/dashboard/layout.tsxlib/constants/notifications.test.tscomponents/auth/auth-button.tsxserver/routers/tracking.tscomponents/layout/app-footer.tsxlib/db/repos/certificates.tsapp/(static)/layout.tsxcomponents/dashboard/settings-content.tsxapp/(static)/privacy/page.tsxlib/constants/notifications.tslib/db/repos/tracked-domains.tsemails/verification-revoked.tsxlib/db/repos/tracked-domains.test.tslib/db/repos/notifications.tscomponents/auth/user-menu.tsxapp/layout.tsxlib/db/repos/notifications.test.tslib/db/schema.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.test.{ts,tsx}: Use Vitest with React Testing Library; configure invitest.config.tswiththreadspool for sandboxed environment compatibility
Mock@vercel/blob(putanddelfunctions) in tests; setBLOB_READ_WRITE_TOKENviavi.stubEnvin suites that touch uploads/deletes
MockURL.createObjectURL/revokeObjectURLwithvi.fn()in tests that need them
Logger is mocked in tests viavitest.setup.ts; usevi.mocked(logger.info)to assert log calls in tests
Files:
lib/db/repos/user-notification-preferences.test.tslib/notifications.test.tslib/constants/notifications.test.tslib/db/repos/tracked-domains.test.tslib/db/repos/notifications.test.ts
lib/db/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Replace
@/lib/db/clientwith a PGlite-backed instance (@/lib/db/pglite) for database tests
Files:
lib/db/repos/user-notification-preferences.test.tslib/db/repos/tracked-domains.test.tslib/db/repos/notifications.test.ts
app/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
app/**/*.tsx: Next.js App Router with default server components; keepapp/page.tsxandapp/api/*thin and delegate toserver/orlib/
Client components must begin with"use client"directive
Files:
app/(static)/terms/page.tsxapp/login/page.tsxapp/dashboard/layout.tsxapp/(static)/layout.tsxapp/(static)/privacy/page.tsxapp/layout.tsx
{app/**,server/**}/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use distinct ID sourced from PostHog cookie via
cache()-wrappedgetDistinctId()to comply with Next.js restrictions
Files:
app/(static)/terms/page.tsxapp/login/page.tsxapp/dashboard/layout.tsxserver/routers/tracking.tsapp/(static)/layout.tsxapp/(static)/privacy/page.tsxapp/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.tslib/inngest/functions/check-certificate-expiry.tslib/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 vialib/constants/index.ts
Files:
lib/constants/notifications.test.tslib/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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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.tsxcomponents/layout/app-footer.tsxcomponents/dashboard/settings-content.tsxcomponents/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 inserver/routers/
Files:
server/routers/tracking.ts
{server/**,app/api/**}/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
{server/**,app/api/**}/*.{ts,tsx}: Usedrizzle-zodfor DB boundary validation with *Select schemas fromlib/db/zod.tsfor reads and *Insert/*Update for writes, with dates coerced in write schemas
Server-side logging: import singletonimport { logger } from "@/lib/logger/server"or create service logger withcreateLogger({ 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 viaafter()
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.tswhen 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.tslib/db/repos/tracked-domains.test.tslib/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.tslib/notifications.test.tslib/constants/notifications.test.tslib/db/repos/tracked-domains.test.tslib/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.tslib/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.tslib/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.tslib/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.tsxapp/login/page.tsxapp/(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.tslib/inngest/functions/reverify-domains.tsemails/domain-expiry.tsxlib/constants/notifications.test.tslib/db/repos/certificates.tslib/constants/notifications.tslib/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.tsxapp/(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.tslib/db/repos/tracked-domains.tslib/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.tslib/db/repos/tracked-domains.tslib/db/repos/notifications.tslib/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 solidThe session check + redirect pattern is clean, and keeping this page as a thin server component that delegates UI to
LoginContentaligns 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
/logindirectly.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
Linkfor client-side navigation. The styling is consistent with the existing footer patterns.app/layout.tsx (1)
57-57: LGTM!Placing the
ToasterinsideProvidersensures 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/componentsfor cross-client email compatibility- Provides clear messaging about verification revocation with actionable CTA
- Includes
PreviewPropsfor 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
getLimitsprocedure efficiently combines user limits lookup with current count and derivedcanAddMoreflag.
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
resumedflag 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
SettingsContentcomponent withshowCard={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-10correctly 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 constassertion 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
handleClicklogic correctly allows keyboard-modified clicks and middle-clicks to navigate to/logindirectly while presenting a modal for standard clicks. The loading placeholder prevents layout shift. This matches the pattern used inUserMenu.handleSettingsClick.
46-67: Good accessibility with screen reader support.The
sr-onlyDialogHeaderensures 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
Linkfor 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/clientper the coding guidelines. ThebeforeAll/afterAlllifecycle 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
verificationFailedAtis 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,verifiedflag 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
certValidTocapture beforestep.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:
- Idempotency key is generated before any operations.
- Notification record is created first as a "soft lock".
- Resend's idempotency key prevents duplicate emails on retry.
- 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. Theuserstable schema enforces.notNull()on bothnameandinnerJointo select from the users table, which guarantees these fields are never null. TheTrackedDomainCertificatetype 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 whennotificationTypeis null, ensuring the results count properly sums toresults.total.
185-242: Past review issue addressed - robust idempotency implementation.The duplicate notification concern from past reviews is now properly handled:
- Idempotency key is generated before any operations (lines 187-190).
- Notification record is created first as a "soft lock" (lines 199-202).
- 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 (
getDomainExpiryNotificationTypeandgetCertificateExpiryNotificationType) 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/clientwith 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_uniquefor 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
…o manage non-existent domains
… test setup for tracked domains with new registration and provider handling
…ctionality, including bookmarklet support for logged-out users
There was a problem hiding this 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 sharedLoginDialogcomponent.components/auth/user-menu.tsx (1)
49-55: Add defensive checks foruser.name.This code will throw a runtime error if
user.nameisundefined,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: Simplifycn()usage or passclassNamedirectly.Using
cn(className)with a single optional argument is redundant—it just returns the className unchanged. Either passclassNamedirectly or usecn()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 intheme-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 sharedLoginDialogcomponent.// 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.tsxBased 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:createTrackedDomaintests cover defaults and method option; consider conflict-path coverageThe tests correctly assert default verification fields and the optional
verificationMethod. To fully exerciseonConflictDoNothing, consider adding a case where you callcreateTrackedDomaintwice with the same(userId, domainId)and expect the second call to returnnull(and still only one DB row).
167-187:verifyTrackedDomainbehavior validated; add an explicit non-null expectation for clarityThe test validates all key fields updated by
verifyTrackedDomain, including date fields and status. For debuggability, you might addexpect(result).not.toBeNull()before asserting properties so failures report more clearly if an unexpectednullis ever returned.
189-213:markVerificationSuccessfultest correctly exercises the full lifecycleThis test walks through verify → mark failing → mark successful and asserts that status and timestamps are reset appropriately, which matches the repo logic that clears
verificationFailedAtand updateslastVerifiedAt. As above, an explicitexpect(result).not.toBeNull()would slightly tighten the assertion story.
215-255:markVerificationFailingtests cover both initial failure and idempotent timestamp behaviorYou validate that status flips to
"failing"and thatverificationFailedAtis set once and then preserved across subsequent calls, matching theexisting.verificationFailedAt ?? new Date()logic. If you want exhaustive coverage, a small extra test for a non-existent ID returningnullwould finish off the edge cases.
339-369:getTrackedDomainsForUsershape and empty-state are validated; consider a provider-populated caseYou 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:getVerifiedDomainsForReverificationtests cover core behavior; optional edge-case for null methodsThe 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 withverified = trueandverificationMethod = nulland assert it’s excluded.
556-662:getVerifiedTrackedDomainsWithExpirytests are thorough; minor redundancy opportunityThe 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
nullregistrar 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
📒 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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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.tsxcomponents/domain/domain-report-header.tsxcomponents/dashboard/settings-content.tsxcomponents/layout/bookmarklet-dialog.tsxcomponents/auth/auth-button.tsxcomponents/auth/user-menu.tsxcomponents/layout/theme-toggle.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: TypeScript only withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
components/layout/mobile-menu.tsxcomponents/domain/domain-report-header.tsxcomponents/dashboard/settings-content.tsxcomponents/layout/bookmarklet-dialog.tsxcomponents/auth/auth-button.tsxcomponents/auth/user-menu.tsxserver/routers/tracking.tscomponents/layout/theme-toggle.tsxlib/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.tsxcomponents/domain/domain-report-header.tsxcomponents/dashboard/settings-content.tsxcomponents/layout/bookmarklet-dialog.tsxcomponents/auth/auth-button.tsxcomponents/auth/user-menu.tsxserver/routers/tracking.tscomponents/layout/theme-toggle.tsxlib/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 inserver/routers/
Files:
server/routers/tracking.ts
{server/**,app/api/**}/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
{server/**,app/api/**}/*.{ts,tsx}: Usedrizzle-zodfor DB boundary validation with *Select schemas fromlib/db/zod.tsfor reads and *Insert/*Update for writes, with dates coerced in write schemas
Server-side logging: import singletonimport { logger } from "@/lib/logger/server"or create service logger withcreateLogger({ 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 viaafter()
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.tswhen 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()-wrappedgetDistinctId()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 inlib/db/with repos inlib/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 invitest.config.tswiththreadspool for sandboxed environment compatibility
Mock@vercel/blob(putanddelfunctions) in tests; setBLOB_READ_WRITE_TOKENviavi.stubEnvin suites that touch uploads/deletes
MockURL.createObjectURL/revokeObjectURLwithvi.fn()in tests that need them
Logger is mocked in tests viavitest.setup.ts; usevi.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/clientwith 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 widthsRemoving
flex-1from 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 viamin-w-0andtruncateon the<h2>. Functionality, analytics tracking, and a11y (includingrel="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
ThemeTogglePropsinterface is clean and follows TypeScript conventions. MakingclassNameoptional 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
UserMenufor logged-in users andMobileMenufor 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
handleOpenChangecorrectly dismisses the toast on close and forwards state changes. The conditionalDialogTriggerrendering based onisControlledensures 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
onCheckedChangeoverride 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
showCardprop for consistent rendering.components/auth/user-menu.tsx (2)
57-69: LGTM!Sign-out handling correctly uses try/catch with
logger.errorfor 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
satisfiesoperator for type checking, and theDomainInputSchemaproperly 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.idand throwFORBIDDENerrors 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
updateNotificationOverridesandresetNotificationOverridesnow correctly validate the return value and throw descriptiveNOT_FOUNDerrors 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
createTrackedDomainand returning the existing record withresumed: 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 guidelinesUsing the node environment and mocking
@/lib/db/clientto 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 sensibleThe shared user/domain in
beforeAlland targeted cleanup oftrackedDomains,registrations, andprovidersinbeforeEachgive each test a clean slate while avoiding unnecessary re-seeding. This ordering also respects FK constraints.
123-141:findTrackedDomainhappy-path and empty-state coverage look goodYou 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
nullwhen absent.
143-165:findTrackedDomainByIdtests are straightforward and adequateThe tests exercise both the non-existent ID and successful lookup paths and mirror the repo’s
rows[0] ?? nullbehavior. Use of a sentinel UUID string is fine here.
257-278:revokeVerificationtest aligns with intended semanticsThe test ensures
verifiedbecomesfalse, status returns to"unverified", andverificationFailedAtis cleared after revocation. That matches the repo function, including the expectation that revocation is a full reset of verification state.
280-311:countTrackedDomainsForUseris well coveredYou 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:deleteTrackedDomaintests validate success semantics for existing and missing IDsThe tests confirm that deletion returns
trueboth 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’stry/catchimplementation.
408-440:getPendingDomainsForAutoVerificationcovers both unverified inclusion and verified exclusionThe 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 viatoMatchObjectis also appropriate here.
442-468:findTrackedDomainWithDomainNametests map cleanly to the join-based implementationYou validate both the null case and the happy path including
domainNameandverificationToken, which confirms theinnerJoin(domains, ...)and field projection are wired as expected.
470-522:updateNotificationOverridestests nicely verify merge semantics and missing-ID behaviorThe suite exercises:
- Initial set of an override,
- Merge behavior when adding another key, and
- The
nullreturn 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:resetNotificationOverridescoverage is solidYou confirm both that overrides are reset back to
{}after having been set and that a non-existent ID yieldsnull, matching theupdated.length === 0 ? null : rowimplementation.
… handling in authentication flows
…domains to prevent race conditions and enhance user subscription handling
There was a problem hiding this 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 usageThe handler never reads
verificationTokenfrom 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
verificationTokenfromeventSchema, 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 ifverificationTokenis missing from DBIn
check-status-*you forwarddomain.verificationTokendirectly intotracked, and then passcurrentTokenintotryAllVerificationMethods(currentDomainName, currentToken)without a guard. IfverificationTokencan 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 chosenRight now the sort UI only appears when
sortOptionis truthy, so if callers support sorting but start withsortOptionundefined, users won’t see the control to pick an initial sort. SincecurrentSortalready safely handles an undefinedsortOption, you can gate rendering purely onviewModeandonSortChange:- 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
onSortChangeto 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 ingetFirstName.The
|| "there"on line 25 is unreachable sincetrimmedis 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_THRESHOLDSarray 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,
setLastExpiryNotificationdoesn'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 totrackedDomainIdinstead ofresumeDomainfor safer refetchingRight now
instructionsQueryis keyed offresumeDomain?.idandenabled: !!resumeDomain && open, butrefetchInstructionsis exposed unconditionally. IfrefetchInstructionsis ever called whenresumeDomainisnull(e.g., in a non‑resume path in the future), React Query will still try to refetch using atrackedDomainIdof"", which is likely invalid server‑side.You can make this more robust (and reusable for non‑resume flows) by basing the query input and
enabledflag ontrackedDomainIdinstead ofresumeDomain, and simplifying the effect that hydratesinstructions:- 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
refetchInstructionssafe to call whenever atrackedDomainIdis present.Also applies to: 95-101, 261-266
40-43: Optionally cleardomainErrorwhen the domain input changes
domainErroris reset before eachhandleAddDomaincall 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
📒 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 withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
lib/polar/handlers.test.tshooks/use-domain-verification.tslib/inngest/functions/auto-verify-pending-domain.tslib/db/repos/user-subscription.tslib/db/repos/tracked-domains.tslib/inngest/functions/check-subscription-expiry.tscomponents/dashboard/domain-filters.tsxcomponents/auth/animated-background.tsxlib/polar/handlers.ts
**/*.{ts,tsx,json,css}
📄 CodeRabbit inference engine (AGENTS.md)
Use 2-space indentation throughout the project
Files:
lib/polar/handlers.test.tshooks/use-domain-verification.tslib/inngest/functions/auto-verify-pending-domain.tslib/db/repos/user-subscription.tslib/db/repos/tracked-domains.tslib/inngest/functions/check-subscription-expiry.tscomponents/dashboard/domain-filters.tsxcomponents/auth/animated-background.tsxlib/polar/handlers.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.test.{ts,tsx}: Use Vitest with React Testing Library; configure invitest.config.tswiththreadspool for sandboxed environment compatibility
Mock@vercel/blob(putanddelfunctions) in tests; setBLOB_READ_WRITE_TOKENviavi.stubEnvin suites that touch uploads/deletes
MockURL.createObjectURL/revokeObjectURLwithvi.fn()in tests that need them
Logger is mocked in tests viavitest.setup.ts; usevi.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.tslib/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 inlib/db/with repos inlib/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.tslib/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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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.tsxcomponents/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.tslib/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.tscomponents/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 configurationThe staged
RETRY_DELAYSplus the internal retry loop withstep.sleepandstep.runreads clean and matches the desired “front‑loaded then backing‑off” schedule. Combined withconcurrency: { 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-motionfor accessibility. Themotionpackage (v12.23.25) is correctly installed in dependencies, and themotion/reactimport 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
useReducedMotionhook. Container positioning withfixed,pointer-events-none,-z-10, andoverflow-hiddenensures 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 solidClean, 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-onlyand organizes imports appropriately. The logger source is descriptive and follows the established pattern.
64-71: LGTM! Well-documented idempotency key generation.The inclusion of
endsAtdate 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.runcalls 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
falseon 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-onlyimport, proper organization inlib/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
maxDomainsfrom 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
endsAtandlastExpiryNotificationto 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
onConflictDoNothingpattern 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-onlyimport 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 returnnullfor missing records via the?? nullpattern.
298-354: LGTM! Well-designed query helper with proper table aliasing.The private helper correctly:
- Uses aliases for the
providerstable 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 SQLcasts are necessary due to drizzle's type inference forand(), and the comments explain this appropriately.
436-503: LGTM! Correct verification and notification override operations.The verification functions properly handle state transitions, and
updateNotificationOverridescorrectly 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.
getStaleUnverifiedDomainsdoesn't exclude archived domains from deletion. TheuserTrackedDomainstable has anarchivedAtfield, but the function only filters onverified = falseandcreatedAt < 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, addisNull(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
handleSubscriptionCreateddoesn'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
currentPeriodEndis provided- No action when
currentPeriodEndis 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:
handleDowngradecalled with userIdclearSubscriptionEndsAtcalled- 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
WebhooksOptionsensures 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 solidThe multi‑step flow (
step1→3), resume handling (resumeDomain+openeffects), and dialog reset logic (resetDialog+handleOpenChange) are coherent and line up with how the TRPC mutations/queries are wired. Dependencies onuseEffect/useCallbackalso look compatible withreact-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
There was a problem hiding this 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.ReactElementat line 231 is redundant sinceSubscriptionCancelingEmail()already returns a valid React element thatrender()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:
- Extract
STATUS_OPTIONSandHEALTH_OPTIONS(lines 57-66) tolib/constants/domain-filters.tsif they're reused elsewhere.- Extract the
filterContentJSX (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
📒 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 withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
lib/inngest/functions/check-subscription-expiry.tscomponents/dashboard/domain-filters.tsxlib/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.tscomponents/dashboard/domain-filters.tsxlib/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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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 inlib/db/with repos inlib/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
updateUserTierandcreateSubscriptionensure 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 withhasActiveFilters.- Type safety: The discriminated union
FilterChip(lines 68-73) enables type-safe filter removal without runtime checks.- Performance: Proper use of
useMemofor TLD options anduseCallbackfor 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
There was a problem hiding this 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.
SettingsContentSkeletonappears to duplicate the functionality ofSettingsSkeletonwhich is already imported on line 11. The loading state on line 202 usesSettingsSkeleton, making this local skeleton potentially unused.If
SettingsContentSkeletonserves a different purpose thanSettingsSkeleton, 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 dependencyThe
STATUS_OPTIONSandHEALTH_OPTIONSarrays are well-typed and match theStatusFilter/HealthFilterunions used by the dashboard filters.To keep layering clean (constants as a low-level module) and avoid
lib/constantsdepending onhooks/, consider moving the filter type definitions into this module (or a shared domain types module) and havinghooks/use-domain-filters.tsimport 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
activeCountexceedsmaxDomains, 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 likecn()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
cnfrom@/lib/utilsif not already present.)Also applies to: 52-52
118-120: Card sub-components used outside Card wrapper.When
showCard={false}, theCardHeaderandCardContentcomponents render outside of aCardwrapper. 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 lengthIn the bulk archive/delete
onMutatehandlers,activeCount/archivedCountadjustments usetrackedDomainIds.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
📒 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 vialib/constants/index.ts
Files:
lib/constants/domain-filters.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: TypeScript only withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
lib/constants/domain-filters.tscomponents/auth/user-menu.tsxapp/dashboard/settings/page.tsxcomponents/dashboard/settings/domain-notification-row.tsxcomponents/dashboard/settings/global-notification-row.tsxcomponents/dashboard/subscription-section.tsxcomponents/dashboard/dashboard-content.tsxcomponents/domain/domain-loading-state.tsxcomponents/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.tscomponents/auth/user-menu.tsxapp/dashboard/settings/page.tsxcomponents/dashboard/settings/domain-notification-row.tsxcomponents/dashboard/settings/global-notification-row.tsxcomponents/dashboard/subscription-section.tsxcomponents/dashboard/dashboard-content.tsxcomponents/domain/domain-loading-state.tsxcomponents/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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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.tsxcomponents/dashboard/settings/domain-notification-row.tsxcomponents/dashboard/settings/global-notification-row.tsxcomponents/dashboard/subscription-section.tsxcomponents/dashboard/dashboard-content.tsxcomponents/domain/domain-loading-state.tsxcomponents/dashboard/settings-content.tsx
app/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
app/**/*.tsx: Next.js App Router with default server components; keepapp/page.tsxandapp/api/*thin and delegate toserver/orlib/
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()-wrappedgetDistinctId()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.tscomponents/dashboard/settings/domain-notification-row.tsxcomponents/domain/domain-loading-state.tsxAGENTS.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.mdREADME.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.mdREADME.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 toSettingsContent. 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
domainsandglobalPrefsare applied only after confirming no error state, which is appropriate.components/auth/user-menu.tsx (3)
43-50: LGTM!Good defensive handling for
user.namewith 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-onlyfor the DialogHeader ensures screen readers get proper context while the visual header comes fromSettingsContent. TheshowCard={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 systemThe 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 surfacesThis 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 responsiveThe 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 wiringThe top of
DashboardContentcleanly separates local UI state, TRPC queries, and filter/sort preferences. PassingdomainsQuery.data ?? []intouseDomainFiltersand layeringsortDomainsonly for grid view keeps concerns well isolated and avoids coupling table sort with client-side sort logic.
99-107:?upgraded=truehandling is safe in client-only contextUsing
useEffectwithwindow.location.searchandrouter.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 soundThe
removeMutationonMutate handler correctly:
- Cancels relevant queries.
- Snapshots previous domains/limits.
- Optimistically filters the removed domain and decrements
activeCountwith clamping and updatedcanAddMore.
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 robustThe archive/unarchive mutations:
- Move items between active and archived lists in cache.
- Adjust
archivedAttonew Date()on archive and back tonullon unarchive.- Keep
activeCount,archivedCount, andcanAddMorein sync with those moves.
Error handlers correctly restore snapshots, and the unarchive success handler flippingactiveTabback 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 aconfirmAction, with actual mutations centralized inhandleConfirmAction. ClearingconfirmActionafter execution and resettingresumeDomainwhen 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 renderComputing
isLoading/hasErroracross the three queries and early-returning<DashboardSkeleton />or<DashboardError />prevents the main dashboard UI from rendering against partial/undefined data. ThehandleRetrycallback only refetches errored queries, which is a nice touch to avoid unnecessary network work.
678-823: Dashboard layout wiring and props flow look coherentThe main render tree:
- Wires
DashboardHeader, banners,UpgradePrompt,TrackedDomainsView, andArchivedDomainsViewwith the right derived counts, tier, filters, and selection.- Correctly guards HealthSummary and filters to only show when there are domains.
- Uses
archivedCount > 0to conditionally show the “View archived domains” affordance and passescanUnarchivebased on current limits.The confirmation dialog uses
confirmDialogContentplusopen={confirmAction !== null}and clears state viaonOpenChange, which ties the UX together cleanly.
827-910: Skeleton components align well with the real dashboard structure
DashboardSkeletonandDomainCardSkeletonmirror the structure ofDashboardHeaderand 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
There was a problem hiding this 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 usageProgressbarThe subscription logic is solid, but the
Progresshere lacks labeling, so screen readers won’t know what it represents. You already addressed this inSubscriptionSection; 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 withuse-domain-filtersand constants indexThe filter unions and option arrays look good, but it seems
StatusFilter/HealthFilterare also defined inhooks/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 vialib/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 fromDashboardContentThe 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
📒 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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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.tsxcomponents/dashboard/tracked-domains-table.tsxcomponents/dashboard/subscription-section.tsxcomponents/dashboard/dashboard-content.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: TypeScript only withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
components/dashboard/settings-content.tsxcomponents/dashboard/tracked-domains-table.tsxemails/pro-welcome.tsxcomponents/dashboard/subscription-section.tsxcomponents/dashboard/dashboard-content.tsxemails/subscription-expired.tsxemails/pro-upgrade-success.tsxlib/constants/domain-filters.tslib/polar/products.tslib/polar/emails.ts
**/*.{ts,tsx,json,css}
📄 CodeRabbit inference engine (AGENTS.md)
Use 2-space indentation throughout the project
Files:
components/dashboard/settings-content.tsxcomponents/dashboard/tracked-domains-table.tsxemails/pro-welcome.tsxcomponents/dashboard/subscription-section.tsxcomponents/dashboard/dashboard-content.tsxemails/subscription-expired.tsxemails/pro-upgrade-success.tsxlib/constants/domain-filters.tslib/polar/products.tslib/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 vialib/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.tslib/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 solidProps, 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 consistentProps, 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 goodThe Pro/Free branching, percentage calculation, and Pro tier info are correct, and adding
aria-label/aria-valuetextto theProgresscomponent improves accessibility versus earlier versions.emails/subscription-expired.tsx (1)
15-217: Subscription expired email template looks correct and consistentArchived-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
getFirstNamehelper 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_PRODUCTSconfiguration 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_INFOand dynamicgetProTierInfo(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
There was a problem hiding this 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 onlyupgradedand 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 foronRemove/onArchive
TrackedDomainsViewandArchivedDomainsViewalready callonRemove/onArchivewith 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
DashboardContentis 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 auseBulkActionshook) 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
ReturnTypeif 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
useLoggerhook 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
📒 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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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.tsxcomponents/dashboard/dashboard-content.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: TypeScript only withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
components/dashboard/settings-content.tsxhooks/use-domain-mutations.tscomponents/dashboard/dashboard-content.tsxlib/polar/emails.ts
**/*.{ts,tsx,json,css}
📄 CodeRabbit inference engine (AGENTS.md)
Use 2-space indentation throughout the project
Files:
components/dashboard/settings-content.tsxhooks/use-domain-mutations.tscomponents/dashboard/dashboard-content.tsxlib/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 solidCentralizing 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 intoDashboardSkeleton/DashboardErrorlooks clean and consistent with the rest of the codebase.Also applies to: 310-341
495-578: Skeleton components are well-aligned and lightweight
DashboardSkeletonandDomainCardSkeletonclosely 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. Theserver-onlyimport properly restricts this module to server-side execution.
18-26: LGTM!The
getFirstNamehelper 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
sendProUpgradeEmailalready fetched the user.
194-207: LGTM!Smart design using
endsAtdate 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-fnsto 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
archivedCountin 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
canAddMoreconsistently based on the updatedactiveCountandmaxDomains
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
archivedAttimestamp appropriately (set tonew Date()for archive,nullfor 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
invalidateQueriesfunction 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
undefinedwhen 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
showCardensures consistent behavior across different usage contexts.
288-296: Safe date handling.The
format()calls are properly guarded by conditional checks ensuringsubscriptionEndsAtis 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
There was a problem hiding this 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 becausedate-fns/formatuses local timezone. This has been fixed by usingformatDateTimeUtc(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
LimitsDatafrom the TRPC router output instead of manually defining it. This has been addressed usinginferRouterOutputs<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), theexecuteBulkArchivefunction handles success toasts with partial failure reporting, which provides more nuanced feedback than a simpleonSuccesshandler in the mutation could provide.
348-383: Bulk delete mutation follows same pattern as bulk archive.Success toasts are handled in
dashboard-content.tsxexecuteBulkDeletefunction (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.searchandrouter.replacewith a manually constructed URL bypasses Next.js's URL state management. Consider using theuseSearchParamshook fromnext/navigationfor 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,
DashboardContentexceeds the ~300 LOC guideline. Consider extracting logical sections into sub-components:
- Confirmation dialog logic →
useConfirmDialoghook- Active/Archived tab content → separate view components
- Banner rendering logic →
DashboardBannerscomponentThis 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
📒 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 incomponents/directory
Client-side logging: import singletonimport { logger } from "@/lib/logger/client"or use hookuseLogger({ 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.tsxcomponents/dashboard/dashboard-content.tsxcomponents/dashboard/tracked-domain-card.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: TypeScript only withstrictenabled; prefer small, pure modules (≈≤300 LOC)
Consolidate imports via@/...aliases
Files:
components/dashboard/tracked-domains-table.tsxcomponents/dashboard/dashboard-content.tsxhooks/use-domain-mutations.tscomponents/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.tsxcomponents/dashboard/dashboard-content.tsxhooks/use-domain-mutations.tscomponents/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. TheConfirmActiondiscriminated 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
onUnarchiveSuccesscallback is clean.
186-237: Bulk operation success toasts are handled in-component.The
executeBulkArchiveandexecuteBulkDeletefunctions 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
getConfirmDialogContenthelper function cleanly maps action types to dialog content with appropriate destructive/default variants and well-written confirmation messages.
499-582: LGTM!The
DashboardSkeletoncomponent provides a faithful skeleton representation of the dashboard layout, maintaining visual consistency during loading states. TheDomainCardSkeletonhelper 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 offormatDateTimeUtcfrom@/lib/formatwhich 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
InfoRowhelper component is well-designed with flexible prop handling for bothproviderandchildrenpatterns. The null coalescing forprovider?.namecorrectly handles the optional provider case.components/dashboard/tracked-domains-table.tsx (5)
1-49: LGTM!Well-organized imports with proper
@/aliases. Good use offormatDateTimeUtcfor UTC-correct date display.
61-103: LGTM!Clean helper components:
ProviderCellhandles null provider gracefully with em-dash fallbackSortableHeaderprovides consistent styling and sort indicator behavior
105-106: Good optimization with module-level constant.Using
EMPTY_SETas 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
Mapto 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 countscanAddMorecorrectly recalculated based on new active count
110-148: LGTM!Clean shared utilities for query management:
cancelQueriesprevents race conditions during optimistic updatesinvalidateQueriesensures fresh data after mutationsrollbackhandles 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
invalidateQuerieshelper for external use (e.g., after adding a domain).
…e Next.js navigation for improved performance and maintainability
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.