All documentation

    Architecture

    Rank Pilot AI SEO is a server-rendered React application built on TanStack Start, using TanStack Router for file-based routing and TanStack Query for client-side data fetching and caching. The app is served through a single fetch-style entry point and backed by a Supabase project (Postgres, Auth, Storage, Edge Functions) for persistence, background jobs and third-party integrations.

    SSR entry point

    src/server.ts is the runtime entry used in production. It lazily imports @tanstack/react-start/server-entry and delegates each incoming request to it. Two defensive layers sit around that call:

    • Any error thrown synchronously during the SSR pass is caught and turned into a generic HTML error page (src/lib/error-page.ts), rather than surfacing an unstyled crash.
    • TanStack Start's underlying HTTP layer (h3) can swallow in-handler exceptions and return a JSON { unhandled: true, message: "HTTPError" } body with a 5xx status. normalizeCatastrophicSsrResponse detects that shape, logs the originally captured error (via src/lib/error-capture.ts), and rewrites the response as the same friendly error page.

    src/router.tsx builds the client/server-shared router instance: it wires the generated routeTree (see below) to a fresh QueryClient per request/render, exposed to routes as router context, with scroll restoration enabled and no implicit stale-time on preloads.

    Routing conventions (src/routes)

    Routes are file-based and use flat, dot-separated filenames rather than nested folders, which TanStack Router's file-based route generator (routeTree.gen.ts, auto-generated — never hand-edited) expands into a nested route tree. Conventions in use:

    • index.tsx/.
    • login.tsx, signup.tsx, dashboard.tsx, billing.tsx, etc. → simple top-level pages.
    • Dot-segmented names express nesting/grouping without folders, e.g. admin.users.tsx, admin.audit-logs.tsx, admin.cron-jobs.tsx all live under the /admin area; blog.what-is-seo-software.tsx is a post under /blog.
    • Dynamic segments use $param (e.g. admin.dev-agent.$threadId.tsx, invite.$token.tsx).
    • Bracketed filenames escape characters that would otherwise be parsed as route separators or special tokens — [.]lovable.oauth.consent.tsx maps to the literal path /.lovable/oauth/consent, used for the MCP OAuth consent screen.
    • src/routes/api/public/ holds routes that respond as plain HTTP endpoints (JSON/webhook-style) rather than rendering pages, for cases that need a stable public URL served directly by the app rather than a Supabase Edge Function.
    • Each route file exports a Route created with createFileRoute(path)({ component }); page implementations mostly live in src/pages/* and are imported into the thin route file, keeping routing declarations separate from page logic.

    Server functions vs edge functions

    Two distinct mechanisms provide server-side behaviour, and the choice between them is deliberate:

    Server functions (*.functions.ts, e.g. src/lib/billing.functions.ts, src/lib/gscKeywords.functions.ts, src/lib/gscVerification.functions.ts, src/lib/supportReply.functions.ts) are defined with TanStack Start's createServerFn. They run inside the same SSR process as the app, are called like normal async functions from React components/loaders, and are the default choice for anything that needs to read the authenticated user's session and talk to Postgres directly. They commonly:

    • attach a requireSupabaseAuth middleware that resolves the caller's Supabase session/claims from the request cookies and injects a scoped Supabase client plus userId into context;
    • validate their input with a zod schema via inputValidator;
    • delegate the actual work to a paired *.server.ts module (e.g. billing.server.ts, gscNightlySync.server.ts, mailchimp.server.ts) that contains the real implementation, keeping the callable wrapper thin.

    Supabase Edge Functions (supabase/functions/*) are separate Deno deployments, invoked over HTTP (from the client, from other edge functions, or from Postgres cron/webhooks) rather than imported into the SSR bundle. They are used for work that:

    • must run independently of a user request — scheduled jobs (scheduled-rank-check, expire-trials, check-ranks), queue processors (autopilot-*), and cron-triggered maintenance;
    • calls third-party APIs that require server-side secrets never exposed to the SSR bundle (Stripe, Ahrefs, Google Ads, Twilio, Apify-backed scrapers, GSC/GA4 OAuth exchanges);
    • handles inbound webhooks (stripe-payment-webhook, crm-ticket-webhook);
    • needs to run with the Supabase service role to bypass RLS for privileged operations (admin tooling, cross-user aggregation), always behind an explicit role check.

    A _shared/ folder under supabase/functions holds common helpers (Stripe/Twilio/Mailchimp/Google Ads clients, email templates, CORS handling for passkeys, promo-code validation) reused across functions.

    State and data layers

    • Server state (anything backed by Postgres/Supabase) flows through TanStack Query, keyed by resource and, where relevant, by user id, with explicit staleTime/enabled guards so queries don't fire before auth is resolved.
    • Client-only/UI state uses local component state and React context (src/contexts) for cross-cutting concerns such as pricing region.
    • Supabase client access is split between a browser client (src/integrations/supabase/client.ts, anon key, RLS-enforced) and a server-only client (client.server.ts) used inside server functions/edge functions where elevated or session-bound access is required.
    • Form state uses react-hook-form with zod resolvers; the same zod schemas are frequently shared between client-side validation and server function inputValidators.

    Styling

    The UI is built with Tailwind CSS (v4, via the @tailwindcss/vite plugin) and a component layer based on shadcn/ui primitives wrapping Radix UI (@radix-ui/react-*). Global styles live in src/styles.css; reusable primitives sit in src/components/ui, with feature-specific composition in src/components/* and page-level layout in src/pages.

    Build and deploy

    • npm run dev runs the app under vite dev with the TanStack Start plugin providing SSR in development.
    • npm run build first regenerates the sitemap (scripts/generate-sitemap.mjs) and then runs vite build, producing the SSR server bundle and client assets.
    • Edge functions under supabase/functions are deployed independently to the Supabase project as Deno functions and are not part of the Vite build; each has its own index.ts entry point.
    • Database schema changes are applied via timestamped SQL files in supabase/migrations, run through the Supabase migration tooling — the application code never issues raw DDL at runtime.
    • Environment-specific values (Supabase project reference, publishable keys, third-party client IDs) are read from environment variables at build or runtime; secrets used by edge functions are configured as Supabase function secrets, not embedded in the client bundle.