diff --git a/release-notes/v0.0.24.md b/release-notes/v0.0.24.md
index 1f06301..8378cee 100644
--- a/release-notes/v0.0.24.md
+++ b/release-notes/v0.0.24.md
@@ -1,10 +1,11 @@
-GSC UI, improved app layout and beta in app agent.
+GSC UI, improved app layout and beta in app agent.
## What's new
+
- Get GSC Insights inside the app — thanks @mattmacrocket
- - See queries in "striking distance" of ranking
+ - See queries in "striking distance" of ranking
- (Beta) In app agent - MCP is still recommended, but we'll be working to improve this during the summer.
- - Requires `OPENROUTER_API_KEY`
+ - Requires `OPENROUTER_API_KEY`
- Redesigned the app layout
## Fixed
diff --git a/src/client/features/audit/launch/LaunchView.tsx b/src/client/features/audit/launch/LaunchView.tsx
index e5db4a1..b3d5a99 100644
--- a/src/client/features/audit/launch/LaunchView.tsx
+++ b/src/client/features/audit/launch/LaunchView.tsx
@@ -1,4 +1,4 @@
-import { AutumnProvider, useCustomer } from "autumn-js/react";
+import { useCustomer } from "autumn-js/react";
import { AuditHistorySection } from "@/client/features/audit/launch/AuditHistorySection";
import { LaunchFormCard } from "@/client/features/audit/launch/LaunchFormCard";
import { useLaunchController } from "@/client/features/audit/launch/useLaunchController";
@@ -18,11 +18,7 @@ export function LaunchView(props: LaunchViewProps) {
return ;
}
- return (
-
-
-
- );
+ return ;
}
function HostedLaunchView(props: LaunchViewProps) {
diff --git a/src/client/features/billing/FreePlanBanner.tsx b/src/client/features/billing/FreePlanBanner.tsx
index 8b483ca..11b5351 100644
--- a/src/client/features/billing/FreePlanBanner.tsx
+++ b/src/client/features/billing/FreePlanBanner.tsx
@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router";
-import { AutumnProvider, useCustomer } from "autumn-js/react";
+import { useCustomer } from "autumn-js/react";
import { useSession } from "@/lib/auth-client";
import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection";
import {
@@ -12,14 +12,6 @@ import {
} from "@/shared/billing";
export function FreePlanBanner() {
- return (
-
-
-
- );
-}
-
-function FreePlanBannerContent() {
const { data: session } = useSession();
const customerQuery = useCustomer({
queryOptions: {
diff --git a/src/client/features/billing/HostedPlanGate.tsx b/src/client/features/billing/HostedPlanGate.tsx
index 0508a47..ec020a6 100644
--- a/src/client/features/billing/HostedPlanGate.tsx
+++ b/src/client/features/billing/HostedPlanGate.tsx
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
-import { AutumnProvider, useCustomer } from "autumn-js/react";
+import { useCustomer } from "autumn-js/react";
import { useSession } from "@/lib/auth-client";
import { isHostedClientAuthMode } from "@/lib/auth-mode";
import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection";
@@ -23,11 +23,7 @@ export function HostedPlanGate({
return children(SELF_HOSTED_PLAN_GATE);
}
- return (
-
- {children}
-
- );
+ return {children};
}
function HostedPlanGateContent({
diff --git a/src/client/features/onboarding/OnboardingChat.tsx b/src/client/features/onboarding/OnboardingChat.tsx
index b02191f..7075ec9 100644
--- a/src/client/features/onboarding/OnboardingChat.tsx
+++ b/src/client/features/onboarding/OnboardingChat.tsx
@@ -1,5 +1,4 @@
import { useMutation, useQuery } from "@tanstack/react-query";
-import { AutumnProvider } from "autumn-js/react";
import { useState } from "react";
import { Loader2 } from "lucide-react";
import { DEFAULT_LOCATION_CODE } from "@/shared/keyword-locations";
@@ -57,9 +56,7 @@ export function OnboardingChat() {
{!domain ? (
) : (
-
-
-
+
)}
);
diff --git a/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx b/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx
index b59fbcd..c777631 100644
--- a/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx
+++ b/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx
@@ -1,7 +1,7 @@
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { AutumnProvider, useCustomer } from "autumn-js/react";
+import { useCustomer } from "autumn-js/react";
import {
getLatestRankResults,
getRankPositionMatrix,
@@ -57,20 +57,7 @@ function deviceVisibility(
};
}
-export function RankTrackingDomainDetail(props: {
- config: RankTrackingConfig;
- projectId: string;
- onBack: () => void;
- onEdit: () => void;
-}) {
- return (
-
-
-
- );
-}
-
-function RankTrackingDomainDetailInner({
+export function RankTrackingDomainDetail({
config,
projectId,
onBack,
diff --git a/src/lib/auth-options.ts b/src/lib/auth-options.ts
index 9debbd4..377164c 100644
--- a/src/lib/auth-options.ts
+++ b/src/lib/auth-options.ts
@@ -20,4 +20,17 @@ export const baseAuthOptions = {
user: {
additionalFields: userAdditionalFields,
},
+ session: {
+ // Serve getSession from a signed cookie instead of a DB round trip. The
+ // session lookup runs on every authenticated request, and the DB lives in
+ // us-east — from far colos that single query was ~1s of wall time. The
+ // trade-off is revocation lag: a session revoked elsewhere (sign-out on
+ // another device, password reset) stays valid on an already-issued cookie
+ // for up to maxAge. Authorization still hits the DB via the canonical
+ // project-access checks, so the cookie only vouches for identity.
+ cookieCache: {
+ enabled: true,
+ maxAge: 5 * 60,
+ },
+ },
};
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index a7abfa8..c1e97b5 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -9,6 +9,7 @@ import {
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { TanStackDevtools } from "@tanstack/react-devtools";
import { QueryClientProvider } from "@tanstack/react-query";
+import { AutumnProvider } from "autumn-js/react";
import * as React from "react";
import { DefaultCatchBoundary } from "@/client/components/DefaultCatchBoundary";
import { ExportToSheetsModal } from "@/client/components/table/ExportToSheetsModal";
@@ -173,27 +174,37 @@ function RootDocument({ children }: { children: React.ReactNode }) {
-
- <>
-
- {children}
-
-
- {showDevtools ? (
- ,
- defaultOpen: true,
- },
- ]}
+ {/* Keep this the ONLY AutumnProvider. Each provider creates its own
+ customer cache (autumn-js bundles a private react-query, so it
+ never shares the app QueryClient), and every extra provider mount
+ pays its own ~1s getOrCreateCustomer round trip. It only provides
+ context — nothing fetches until a useCustomer consumer mounts. */}
+
+
+ <>
+
+ {children}
+
+
- ) : null}
- >
-
+ {showDevtools ? (
+ ,
+ defaultOpen: true,
+ },
+ ]}
+ />
+ ) : null}
+ >
+
+
diff --git a/src/routes/_app/billing.tsx b/src/routes/_app/billing.tsx
index 0e040f9..f34cae5 100644
--- a/src/routes/_app/billing.tsx
+++ b/src/routes/_app/billing.tsx
@@ -1,5 +1,5 @@
import { createFileRoute, notFound } from "@tanstack/react-router";
-import { AutumnProvider, useCustomer } from "autumn-js/react";
+import { useCustomer } from "autumn-js/react";
import { useEffect, useState } from "react";
import { useSession } from "@/lib/auth-client";
import { isHostedClientAuthMode } from "@/lib/auth-mode";
@@ -31,14 +31,6 @@ export const Route = createFileRoute("/_app/billing")({
});
function BillingPage() {
- return (
-
-
-
- );
-}
-
-function BillingPageContent() {
const { data: session, isPending: isSessionPending } = useSession();
const [topUpAmount, setTopUpAmount] = useState("20");
const [isPending, setIsPending] = useState(false);
diff --git a/src/routes/_authenticated.subscribe.tsx b/src/routes/_authenticated.subscribe.tsx
index 2cee1b7..9037b61 100644
--- a/src/routes/_authenticated.subscribe.tsx
+++ b/src/routes/_authenticated.subscribe.tsx
@@ -1,5 +1,5 @@
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
-import { AutumnProvider, useCustomer } from "autumn-js/react";
+import { useCustomer } from "autumn-js/react";
import { useEffect, useState } from "react";
import { ArrowRight, Settings, User } from "lucide-react";
import { ThemePreferenceMenuItems } from "@/client/components/ThemePreferenceMenuItems";
@@ -41,14 +41,6 @@ export const Route = createFileRoute("/_authenticated/subscribe")({
});
function SubscribePage() {
- return (
-
-
-
- );
-}
-
-function SubscribePageContent() {
const navigate = useNavigate();
const { upgrade: isUpgradeFlow, redirect } = Route.useSearch();
const { data: session } = useSession();
diff --git a/src/server/features/rank-tracking/services/RankTrackingService.ts b/src/server/features/rank-tracking/services/RankTrackingService.ts
index 1956ca5..717c9bb 100644
--- a/src/server/features/rank-tracking/services/RankTrackingService.ts
+++ b/src/server/features/rank-tracking/services/RankTrackingService.ts
@@ -269,8 +269,10 @@ async function refreshKeywordMetrics(
projectId: string,
billingCustomer: BillingCustomerContext,
): Promise<{ updated: number }> {
- const config = await getValidatedConfig(configId, projectId);
- const keywords = await RankTrackingRepository.getKeywordsForConfig(configId);
+ const [config, keywords] = await Promise.all([
+ getValidatedConfig(configId, projectId),
+ RankTrackingRepository.getKeywordsForConfig(configId),
+ ]);
if (keywords.length === 0) return { updated: 0 };
const client = createDataforseoClient(billingCustomer);
diff --git a/src/server/features/rank-tracking/services/rankTrackingResults.ts b/src/server/features/rank-tracking/services/rankTrackingResults.ts
index 0fbaa8d..5ae51e0 100644
--- a/src/server/features/rank-tracking/services/rankTrackingResults.ts
+++ b/src/server/features/rank-tracking/services/rankTrackingResults.ts
@@ -26,29 +26,28 @@ export async function getLatestResults(
rows: RankTrackingRow[];
run: { id: string; lastCheckedAt: string } | null;
}> {
- const config = await RankTrackingRepository.getConfigById({
- configId,
- projectId,
- });
- if (!config) {
- throw new AppError("INTERNAL_ERROR", "Rank tracking config not found");
- }
-
- const activeKeywords =
- await RankTrackingRepository.getKeywordsForConfig(configId);
-
- // Get the latest snapshot per keyword per device (across all completed runs)
- const currentSnapshots =
- await RankTrackingRepository.getLatestSnapshotsForKeywords(configId);
-
- // Get comparison snapshots from before the target date
const days = PERIOD_DAYS[comparePeriod];
const targetDate = toSqliteTimestamp(
new Date(Date.now() - days * 24 * 60 * 60 * 1000),
);
- const comparisonSnapshots =
- await RankTrackingRepository.getSnapshotsBeforeDate(configId, targetDate);
+ // All four reads key off the inputs alone, so run them in one parallel round
+ // trip instead of four sequential ones — this endpoint is hot and the DB may
+ // be a continent away. The project-scoped config lookup doubles as the
+ // authorization gate for the configId-keyed reads racing alongside it: when
+ // config is null, throw without returning anything from the other reads.
+ const [config, activeKeywords, currentSnapshots, comparisonSnapshots] =
+ await Promise.all([
+ RankTrackingRepository.getConfigById({ configId, projectId }),
+ RankTrackingRepository.getKeywordsForConfig(configId),
+ // Latest snapshot per keyword per device (across all completed runs)
+ RankTrackingRepository.getLatestSnapshotsForKeywords(configId),
+ // Comparison snapshots from before the target date
+ RankTrackingRepository.getSnapshotsBeforeDate(configId, targetDate),
+ ]);
+ if (!config) {
+ throw new AppError("INTERNAL_ERROR", "Rank tracking config not found");
+ }
const previousPositions = new Map();
for (const snap of comparisonSnapshots) {
diff --git a/wrangler.jsonc b/wrangler.jsonc
index 6beef23..1eacb3c 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -6,6 +6,18 @@
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"observability": {
"enabled": true,
+ // Spans are billed as observability events (free tier 200k/day); add a
+ // traces head_sampling_rate below 1 if span volume becomes a cost concern.
+ "traces": {
+ "enabled": true,
+ },
+ },
+ // Smart Placement relocates only the `fetch` handler (cron + Workflow runs
+ // stay at the edge) toward external subrequests — Postgres via Hyperdrive,
+ // SEO APIs — not D1/KV/R2 bindings, so it's a DB no-op on the D1 default.
+ // Must live here: a dashboard-only toggle is wiped by `wrangler deploy`.
+ "placement": {
+ "mode": "smart",
},
// Uncomment when deploying to the openseo.so Cloudflare account. Self-hosters
// should leave this commented (or replace with their own zone) — otherwise