diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f56ef247d..cf477b1a5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -697,6 +697,9 @@ importers:
'@fireproof/core':
specifier: 0.24.0
version: 0.24.0(react@19.2.0)(typescript@5.9.3)
+ '@fireproof/core-protocols-dashboard':
+ specifier: 0.24.0
+ version: 0.24.0(typescript@5.9.3)
'@fireproof/core-runtime':
specifier: 0.24.0
version: 0.24.0(typescript@5.9.3)
@@ -718,6 +721,9 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.19
version: 0.5.19(tailwindcss@4.1.17)
+ '@tanstack/react-query':
+ specifier: ^5.90.10
+ version: 5.90.10(react@19.2.0)
'@vibes.diy/hosting-base':
specifier: workspace:*
version: link:../../hosting/base
@@ -3678,6 +3684,14 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
+ '@tanstack/query-core@5.90.10':
+ resolution: {integrity: sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ==}
+
+ '@tanstack/react-query@5.90.10':
+ resolution: {integrity: sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==}
+ peerDependencies:
+ react: ^18 || ^19
+
'@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'}
@@ -12737,6 +12751,13 @@ snapshots:
tailwindcss: 4.1.17
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
+ '@tanstack/query-core@5.90.10': {}
+
+ '@tanstack/react-query@5.90.10(react@19.2.0)':
+ dependencies:
+ '@tanstack/query-core': 5.90.10
+ react: 19.2.0
+
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.27.1
diff --git a/token-auth-and-exchange.md b/token-auth-and-exchange.md
new file mode 100644
index 000000000..ee9503a1c
--- /dev/null
+++ b/token-auth-and-exchange.md
@@ -0,0 +1,81 @@
+# Token Auth & Exchange Flow Analysis
+
+This document describes the current observed authentication flow and the specific failure point encountered when integrating the `vibes.diy` development environment with the Fireproof Connect backend.
+
+## The Goal
+
+Authenticate a user on the `vibes.diy` local development environment using a **Clerk Development Instance** (`sincere-cheetah-30...`) and successfully exchange this token for a Fireproof Cloud token (`fp-cloud-jwt`) via the production Fireproof backend (`https://connect.fireproof.direct/api`).
+
+## The Flow
+
+1. **Client Authentication (Success)**
+ - The `vibes.diy` app uses `@clerk/clerk-react` to sign the user in.
+ - **Result:** A valid JWT is obtained.
+ - **Issuer (`iss`):** `https://sincere-cheetah-30.clerk.accounts.dev`
+ - **Key ID (`kid`):** `ins_35qNS5Jwyc7z4aJRBIS7o205yzb`
+ - **Algorithm:** `RS256`
+ - **Audience (`aud`):** `undefined` (No audience claim present).
+
+2. **Token Verification / Exchange Request (Failure)**
+ - The client calls `DashboardApi.getCloudSessionToken({})`.
+ - **Payload:** `{ auth: { type: "clerk", token: "..." } }`
+ - **Endpoint:** `PUT https://connect.fireproof.direct/api`
+
+3. **Backend Verification (Error)**
+ _ The backend receives the request and attempts to verify the token.
+ _ **Backend Configuration:** The backend is configured with `CLERK_PUB_JWT_URL` containing three issuers, including the development one:
+ _ `https://clerk.fireproof.direct`
+ _ `https://clerk.vibes.diy`
+ _ `https://sincere-cheetah-30.clerk.accounts.dev`
+ _ **Response:** HTTP 500 \* **Error Message:**
+ `json
+ {
+ "type": "error",
+ "message": "No well-known JWKS URL could verify the token:\n[
+ {
+ "type": "error",
+ "error": {
+ "reason": "token-invalid-signature"
+ },
+ "url": "https://clerk.fireproof.direct/.well-known/jwks.json"
+ },
+ {
+ "type": "error",
+ "error": {
+ "reason": "token-invalid-signature"
+ },
+ "url": "https://clerk.vibes.diy/.well-known/jwks.json"
+ },
+ {
+ "type": "error",
+ "error": {
+ "reason": "token-invalid-signature"
+ },
+ "url": "https://sincere-cheetah-30.clerk.accounts.dev/.well-known/jwks.json"
+ }
+]"
+ }
+ `
+
+## Diagnosis Findings
+
+- **✅ Issuer Match:** The client sends a token from `sincere-cheetah-30`. The backend _is_ attempting to verify against the corresponding JWKS URL.
+- **✅ Key ID Match:** The client token has `kid: 'ins_35qNS5Jwyc7z4aJRBIS7o205yzb'`. The public JWKS at `https://sincere-cheetah-30.clerk.accounts.dev/.well-known/jwks.json` currently contains this exact key.
+- **❌ Signature Verification Failure:** Despite the correct issuer and key ID, the backend reports `token-invalid-signature`.
+
+## Potential Root Causes for Review
+
+1. **Audience (`aud`) Validation:**
+ - The client token has **no `aud` claim**.
+ - Does the backend's `@hono/clerk-auth` middleware (or underlying `verifyToken` function) enforce a default audience check? If so, the lack of an audience would cause verification to fail even if the signature is valid.
+
+2. **Stale JWKS Cache:**
+ - Could the backend be caching an older version of the JWKS for `sincere-cheetah-30` that does not yet contain the key `ins_35qNS5Jwyc7z4aJRBIS7o205yzb`?
+ - _Note: This is less likely if this is a stable dev instance, but possible if keys were recently rotated._
+
+3. **Crypto/Environment Mismatch:**
+ - Is the backend environment (e.g., Cloudflare Workers) correctly handling the RS256 signature verification for this specific key?
+
+## Request for Engineering Team
+
+Please verify if the Fireproof backend enforces an **Audience (`aud`) claim** on Clerk tokens. If so, what audience value is expected? We may need to configure our Clerk development instance to include this audience in its issued tokens.
diff --git a/vibes.diy/pkg/app/config/env.ts b/vibes.diy/pkg/app/config/env.ts
index 57c1a4164..793c816d1 100644
--- a/vibes.diy/pkg/app/config/env.ts
+++ b/vibes.diy/pkg/app/config/env.ts
@@ -48,11 +48,17 @@ class vibesDiyEnv {
this.env().get("VITE_CONNECT_URL") ??
"https://connect.fireproof.direct/token",
);
- readonly CONNECT_API_URL = Lazy(
- () =>
- this.env().get("VITE_CONNECT_API_URL") ??
- "https://connect.fireproof.direct/api",
- );
+ readonly CONNECT_API_URL = Lazy(() => {
+ const envUrl = this.env().get("VITE_CONNECT_API_URL");
+ if (envUrl) {
+ return envUrl;
+ }
+ const isProduction =
+ runtimeFn().isBrowser && window.location.hostname === "vibes.diy";
+ return isProduction
+ ? "https://connect.fireproof.direct/api"
+ : "https://dev.connect.fireproof.direct/api";
+ });
readonly CLOUD_SESSION_TOKEN_PUBLIC_KEY = Lazy(
() =>
this.env().get("VITE_CLOUD_SESSION_TOKEN_PUBLIC") ??
diff --git a/vibes.diy/pkg/app/root.tsx b/vibes.diy/pkg/app/root.tsx
index 9f38be90e..c79b5b66b 100644
--- a/vibes.diy/pkg/app/root.tsx
+++ b/vibes.diy/pkg/app/root.tsx
@@ -20,6 +20,17 @@ import { ClerkProvider } from "@clerk/clerk-react";
import { CookieConsentProvider } from "./contexts/CookieConsentContext.js";
import { ThemeProvider } from "./contexts/ThemeContext.js";
import { getLibraryImportMap } from "./config/import-map.js";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+// Create a client instance for React Query
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ refetchOnWindowFocus: false,
+ },
+ },
+});
export const links: Route.LinksFunction = () => {
const rawBase = VibesDiyEnv.APP_BASENAME();
@@ -120,24 +131,26 @@ export function Layout({ children }: { children: React.ReactNode }) {
{/* TODO: Re-enable GtmNoScript when consent can be checked server-side */}
{/* */}
-
-
-
- {children}
-
-
-
-
-
-
-
-
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+