All documentation

    Security

    Authentication methods

    Rank Pilot AI SEO uses Supabase Auth as its identity provider, supporting several sign-in paths depending on the flow:

    • Password — standard email/password sign-in and sign-up (src/pages/Login.tsx, Signup.tsx), with forgot-password/reset-password routes handling recovery.
    • Magic link / email OTP — emailed links or codes are completed via supabase.auth.verifyOtp({ token_hash, type }), handled centrally in useAuth.tsx so any route can land on a verification redirect (account verification, invite acceptance) and complete the exchange consistently.
    • Passkeys (WebAuthn) — registration and authentication ceremonies are implemented client-side with @simplewebauthn/browser and backed by dedicated edge functions (passkey-register-options, passkey-register-verify, passkey-auth-options, passkey-auth-verify), with credential records stored in passkeys and short-lived challenges in passkey_challenges. A shared CORS helper (_shared/passkey-cors.ts) keeps these functions consistent with the rest of the API surface.
    • Two-factor authentication (TOTP) — an authenticator-app second factor managed through Supabase Auth's MFA API (TwoFactorCard.tsx): users enrol a TOTP factor, scan the returned QR code/secret, and confirm a 6-digit code to move the factor to a verified state before it is enforced on subsequent sign-ins.

    Together these give users a choice of password, magic-link/OTP, or passkey as a primary factor, with optional TOTP as an additional factor on top of any of them.

    Rate limiting

    Rate limiting is applied at more than one layer:

    • Client-side throttling (src/hooks/useRateLimit.ts) guards expensive, user-triggered AI/report-generation actions with a rolling window (default: 5 calls per 60 seconds) plus a minimum cooldown between calls, preventing accidental spamming of costly endpoints before a request even leaves the browser. This is a UX safeguard, not the security boundary.
    • Server-side limiting is enforced inside individual edge functions (e.g. content-brief, seo-autofix, suggest-keywords, competitor-analysis, internal-linking, submit-demo-lead, scheduled-rank-check) so that a request bypassing the client entirely is still constrained — typically by tracking recent invocations per user or per fingerprint and rejecting requests that exceed the configured threshold.
    • Runtime error logging applies its own flood protection: errorLogger.ts fingerprints each error/stack combination client-side and suppresses repeat sends of the same fingerprint within a short window, and the log-runtime-error edge function additionally dedupes and rate-limits inserts server-side per fingerprint, so a single recurring bug cannot flood runtime_error_logs.

    Audit logging

    Two audit trails exist for different purposes:

    • audit_logs is a general-purpose privileged-action log (actor, event type, affected table/row, before/after values, IP address and user agent), readable only by admins (enforced via the has_role RLS pattern — see data-model.md) and writable only by the service role, so application code cannot forge entries from the client.
    • user_role_audit_log specifically records grants and revocations of roles in user_roles, giving a dedicated, narrowly-scoped history of privilege changes independent of the general audit log.

    Admin-facing routes such as admin.audit-logs.tsx surface these logs read-only; there is no UI path to edit or delete audit history from the application.

    RLS and GRANT policy

    The platform's baseline is that every table has Row Level Security enabled, and access is expressed as the combination of a coarse GRANT (typically GRANT SELECT ON <table> TO authenticated plus GRANT ALL ON <table> TO service_role) and fine-grained CREATE POLICY statements that decide which specific rows a grant actually exposes. In practice this means:

    • The authenticated role generally gets baseline SELECT/INSERT/UPDATE grants where relevant, but RLS policies restrict rows to those owned by auth.uid() (directly, or via a parent relationship such as a website's owner).
    • Admin-only tables layer a has_role(auth.uid(), 'admin') condition into their policies on top of the same grant, rather than issuing a separate grant just for admins.
    • The service_role (used exclusively by edge functions, never shipped to the client) receives broad grants and, because Supabase's service role bypasses RLS, is trusted to do so only inside audited, purpose-built edge functions — never invoked directly from browser code.
    • Sensitive credential tables (user_api_keys, passkeys) restrict authenticated access to the owning row only, and expose no broader admin read path by default — even admin tooling reaches them, where it needs to at all, through service-role RPCs (get_user_api_key, admin_list_user_api_keys) rather than direct table grants.

    Storage bucket rules

    Supabase Storage buckets follow the same "explicit policy per operation" discipline as database tables. The SEO plan PDF bucket, for example, was iterated through several migrations to reach a least-privilege state: uploads and updates are restricted to authenticated users acting on their own files (enforced via policies scoped to a per-user folder path within the bucket), deletion is restricted to the service role, and public readability was deliberately removed once direct authenticated access was in place — the bucket itself is marked non-public, so objects are only reachable through signed access controlled by policy rather than a public URL. New buckets follow the same pattern: default to public = false, and add narrowly-scoped INSERT/SELECT/UPDATE/DELETE policies on storage.objects for the specific access each feature actually needs, rather than granting broad bucket-wide access.

    Error handling conventions

    • SSR-level failures are never allowed to reach the client as a raw stack trace or an unhandled JSON error body. src/server.ts catches synchronous SSR errors and also detects the case where TanStack Start's underlying HTTP layer swallows an in-handler exception into a generic {"unhandled":true} response, rewriting both cases into a consistent, non-revealing HTML error page (src/lib/error-page.ts).
    • Client runtime errors (uncaught exceptions and unhandled promise rejections) are captured once at the app root and forwarded to log-runtime-error, with message/stack length capped before transmission and fingerprint-based deduplication so noisy repeat errors don't flood the log or leak excessive detail.
    • Supabase Edge Function errors returned to the client (FunctionsHttpError, FunctionsRelayError, FunctionsFetchError) are treated as expected, recoverable failures handled at the call site — typically surfaced to the user as a toast — rather than being treated as crashes or forwarded to the runtime-error log, keeping that log focused on genuine unexpected failures.
    • Server functions (*.functions.ts) wrap their handler logic in try/catch, log the error server-side with a consistent console.error('[functionName]', e) prefix, and return a safe, user-facing fallback payload (e.g. an empty result set plus a generic error string) rather than propagating internal error details or stack traces to the browser.
    • Across both edge functions and server functions, error responses avoid echoing back secrets, connection strings or other configuration values, even when logging the underlying cause server-side for diagnosis.