Stirling-Tools/Stirling-PDF · error · Error

No SaaS session

Error message

No SaaS session

What it means

Plain Error('No SaaS session') thrown by invokeEdge() in procurement.ts when getSupabaseClient() returns a falsy value. invokeEdge() drives Supabase Edge Functions (issue/accept-procurement-quote) via supabase.functions.invoke. A demo/MSW response is consulted first; only when no demo matches AND there is no Supabase client does this throw.

Source

Thrown at frontend/editor/src/portal/api/procurement.ts:371

 * shared project — pressing "Generate quote" in dev billed nothing but left real objects behind.
 */
async function demoEdgeResponse(
  fn: string,
  quoteId: number,
): Promise<Response | undefined> {
  const base = saasApiBase();
  if (!base) return undefined;
  return resolveDemoResponse(new URL(`${base}/functions/v1/${fn}`), {
    method: "POST",
    body: { quote_id: quoteId },
  });
}

async function invokeEdge<T>(fn: string, quoteId: number): Promise<T> {
  const demo = await demoEdgeResponse(fn, quoteId);
  if (demo) return (await demo.json()) as T;
  const supabase = getSupabaseClient();
  if (!supabase) throw new Error("No SaaS session");
  const { data, error } = await supabase.functions.invoke<T>(fn, {
    body: { quote_id: quoteId },
  });
  if (error) throw error;
  if (data == null) throw new Error(`${fn} returned no data`);
  return data;
}

/** Turn a draft into an issued Stripe Quote (finalized → gets a number + PDF, shareable). */
export function issueQuote(quoteId: number): Promise<QuoteResult> {
  return invokeEdge<QuoteResult>("issue-procurement-quote", quoteId);
}

/** Accept an issued quote → Stripe creates the committed subscription + first invoice. */
export function acceptQuote(quoteId: number): Promise<AcceptResult> {
  return invokeEdge<AcceptResult>("accept-procurement-quote", quoteId);
}

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Ensure the Supabase client is initialised (SaaS auth bootstrap) before procurement actions are reachable — gate the quote UI on session presence.
  2. Catch the error and route the user to SaaS login/link.
  3. In tests, mock getSupabaseClient to return a stub client (the existing test pattern).
  4. Replace the plain Error with SaasNotLinkedError for consistency with http.ts so callers handle one error type.

Example fix

// before — plain Error, inconsistent with http.ts SaasNotLinkedError
const supabase = getSupabaseClient();
if (!supabase) throw new Error("No SaaS session");

// after — reuse the named error type so UI handling is uniform
import { SaasNotLinkedError } from "@portal/api/http";
const supabase = getSupabaseClient();
if (!supabase) throw new SaasNotLinkedError();
Defensive patterns

Strategy: validation

Validate before calling

import { getSupabaseClient } from "@app/auth/supabase/supabaseClient";
function hasSaasSession(): boolean { return getSupabaseClient() != null; }
// before quote actions: if (!hasSaasSession()) promptSaasLogin();

Type guard

function isNoSaasSession(e: unknown): boolean {
  return e instanceof Error && e.message === "No SaaS session";
}

Try / catch

try { return await issueQuote(quoteId); }
catch (e) { if (isNoSaasSession(e)) { navigate('/portal/link'); return; } throw e; }

Prevention

When it happens

Trigger: issueQuote() or acceptQuote() called when the Supabase client is not initialised (SaaS auth not bootstrapped, or running in a flavour/context that never created the client), and no demo handler intercepts the edge-function URL.

Common situations: Calling procurement quote actions in a self-hosted/non-SaaS context where the Supabase client was never created; the auth bootstrap that initialises the client has not run yet; a test forgot to mock getSupabaseClient.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/b1407fe4c81dd2b0. Report an issue: GitHub.