Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/convex/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

.env.local
37 changes: 37 additions & 0 deletions examples/convex/app/action/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { useAction } from "convex/react";
import { useState } from "react";
import { api } from "@/convex/_generated/api";
import { useUser } from "@stackframe/stack";
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";

export default function Page() {
const myAction = useAction(api.myActions.myAction);
const user = useUser({ or: "redirect" });
const [data, setData] = useState<string | null>(null);


return (
<div className="flex flex-col gap-8 max-w-lg mx-auto pt-10">
<div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md">
<h2 className="text-xl font-bold">User read-only metadata</h2>
<code>
<pre>{JSON.stringify(user.clientReadOnlyMetadata, null, 2)}</pre>
</code>
</div>
<input type="text" placeholder="test 123" className="border border-slate-300 rounded-md p-2" onChange={(e) => setData(e.target.value)} />
<button
className="bg-foreground text-background text-sm px-4 py-2 rounded-md"
onClick={() => {
runAsynchronouslyWithAlert(async () => {
await myAction({ testMetadata: data ?? "" })
alert("User's client read-only metadata updated, refresh to see changes")
})
}}
>
My Action
</button>
</div>
)
}
34 changes: 29 additions & 5 deletions examples/convex/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UserButton, useUser } from "@stackframe/stack";
import { useMutation, useQuery } from "convex/react";
import Link from "next/link";
import { api } from "../convex/_generated/api";
import { getUserInfoConvexClient, getUserInfoConvexHttpClient } from "./user-info";

export default function Home() {
const user = useUser();
Expand Down Expand Up @@ -41,11 +42,27 @@ function Content() {
return (
<div className="flex flex-col gap-8 max-w-lg mx-auto">
<p>Welcome {viewer ?? "Anonymous"}!</p>
<button className="bg-foreground text-background text-sm px-4 py-2 rounded-md" onClick={() => {
alert(getUserInfo);
}}>
View user info
</button>
<div className="flex flex-col gap-2">
<button className="bg-foreground text-background text-sm px-4 py-2 rounded-md" onClick={() => {
alert(getUserInfo);
}}>
View user info
</button>
<button className="bg-foreground text-background text-sm px-4 py-2 rounded-md" onClick={() => {
getUserInfoConvexHttpClient().then(userInfo => {
alert(userInfo);
});
}}>
View user info (http client)
</button>
<button className="bg-foreground text-background text-sm px-4 py-2 rounded-md" onClick={() => {
getUserInfoConvexClient().then(userInfo => {
alert(userInfo);
});
}}>
View user info (js client)
</button>
</div>
<p>
Click the button below and open this page in another window - this data
is persisted in the Convex cloud database!
Expand Down Expand Up @@ -87,6 +104,13 @@ function Content() {
</Link>{" "}
for an example of loading data in a server component
</p>
<p>
See the{" "}
<Link href="/action" className="underline hover:no-underline">
/action route
</Link>{" "}
for an example of using a convex action to update user data in stack auth
</p>
<div className="flex flex-col">
<p className="text-lg font-bold">Useful resources:</p>
<div className="flex gap-2">
Expand Down
13 changes: 13 additions & 0 deletions examples/convex/app/server/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Home from "./inner";
import { preloadQuery, preloadedQueryResult } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import { ConvexHttpClient } from "convex/browser";
import { stackServerApp } from "@/stack/server";

export default async function ServerPage() {
const preloaded = await preloadQuery(api.myFunctions.listNumbers, {
Expand All @@ -9,9 +11,20 @@ export default async function ServerPage() {

const data = preloadedQueryResult(preloaded);

const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const token = await stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
convex.setAuth(token);
const userInfo = await convex.query(api.myFunctions.getUserInfo, {});
Comment on lines +14 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add error handling for authentication and query operations.

The token retrieval, authentication setup, and query execution lack error handling. If the token is empty (due to the change in getConvexHttpClientAuth) or if the query fails, the page will crash without a user-friendly error message.

Apply this diff to add error handling:

-  const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
-  const token = await stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
-  convex.setAuth(token);
-  const userInfo = await convex.query(api.myFunctions.getUserInfo, {});
+  const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
+  const token = await stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
+  
+  if (!token) {
+    throw new Error("Failed to obtain authentication token");
+  }
+  
+  convex.setAuth(token);
+  const userInfo = await convex.query(api.myFunctions.getUserInfo, {});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const token = await stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
convex.setAuth(token);
const userInfo = await convex.query(api.myFunctions.getUserInfo, {});
const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const token = await stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
if (!token) {
throw new Error("Failed to obtain authentication token");
}
convex.setAuth(token);
const userInfo = await convex.query(api.myFunctions.getUserInfo, {});
🤖 Prompt for AI Agents
In examples/convex/app/server/page.tsx around lines 14 to 17, add proper error
handling for token retrieval, auth setup, and the query: wrap the call to
stackServerApp.getConvexHttpClientAuth and convex.query in try/catch blocks,
verify the returned token is non-empty before calling convex.setAuth (and handle
the empty-token case by logging the situation and returning a safe fallback or
rendering an error state), and catch/query errors to log them and return a
fallback userInfo or an error response so the page doesn't crash.


return (
<main className="p-8 flex flex-col gap-4 mx-auto max-w-2xl">
<h1 className="text-4xl font-bold text-center">Convex + Next.js</h1>
<div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md">
<h2 className="text-xl font-bold">User info</h2>
<code>
<pre>{JSON.stringify(JSON.parse(userInfo), null, 2)}</pre>
</code>
</div>
<div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md">
<h2 className="text-xl font-bold">Non-reactive server-loaded data</h2>
<code>
Expand Down
21 changes: 21 additions & 0 deletions examples/convex/app/user-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { api } from "@/convex/_generated/api";
import { stackClientApp } from "@/stack/client";
import { ConvexHttpClient, ConvexClient } from "convex/browser";

const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential issue: Module-level environment variable access.

Creating ConvexHttpClient at module level using process.env.NEXT_PUBLIC_CONVEX_URL! may cause issues in Next.js. Environment variables might not be available during module initialization in certain contexts (e.g., static generation), and the non-null assertion bypasses validation.

Consider moving client creation into the function or adding validation:

-const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
-
 export async function getUserInfoConvexHttpClient() {
+  const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
   const token = await stackClientApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
   convexHttpClient.setAuth(token);
   const userInfo = await convexHttpClient.query(api.myFunctions.getUserInfo, {});
   return userInfo;
 }

Or add validation if keeping module-level creation:

+if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
+  throw new Error("NEXT_PUBLIC_CONVEX_URL environment variable is required");
+}
+
 const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export async function getUserInfoConvexHttpClient() {
const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const token = await stackClientApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
convexHttpClient.setAuth(token);
const userInfo = await convexHttpClient.query(api.myFunctions.getUserInfo, {});
return userInfo;
}
🤖 Prompt for AI Agents
In examples/convex/app/user-info.ts around line 5, creating ConvexHttpClient at
module initialization with process.env.NEXT_PUBLIC_CONVEX_URL! is unsafe; move
the client creation into the function that uses it (so the env var is read at
request/runtime) or, if you must keep module-level creation, validate the
environment variable first (check for undefined and throw a clear error or
fallback) and remove the non-null assertion to avoid runtime surprises.


export async function getUserInfoConvexHttpClient() {
const token = await stackClientApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
convexHttpClient.setAuth(token);
const userInfo = await convexHttpClient.query(api.myFunctions.getUserInfo, {});
return userInfo;
}


const convexClient = new ConvexClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
convexClient.setAuth(stackClientApp.getConvexClientAuth({ tokenStore: "nextjs-cookie" }))

export async function getUserInfoConvexClient() {
const userInfo = await convexClient.query(api.myFunctions.getUserInfo, {});
return userInfo;
}
2 changes: 2 additions & 0 deletions examples/convex/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @module
*/

import type * as myActions from "../myActions.js";
import type * as myFunctions from "../myFunctions.js";

import type {
Expand All @@ -25,6 +26,7 @@ import type {
* ```
*/
declare const fullApi: ApiFromModules<{
myActions: typeof myActions;
myFunctions: typeof myFunctions;
}>;
declare const fullApiWithMounts: typeof fullApi;
Expand Down
27 changes: 27 additions & 0 deletions examples/convex/convex/myActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use node"

import { v } from "convex/values";
import { stackServerApp } from "../stack/server";
import { action } from "./_generated/server";


export const myAction = action({
args: {
testMetadata: v.string(),
},

handler: async (ctx, args) => {
const partialUser = await stackServerApp.getPartialUser({ from: "convex", ctx });
if (!partialUser) {
return null;
}
const user = await stackServerApp.getUser(partialUser?.id);
if (!user) {
return null;
}
await user.setClientReadOnlyMetadata({
test: args.testMetadata,
})
},
});

29 changes: 1 addition & 28 deletions examples/convex/convex/myFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import { v } from "convex/values";
import { stackClientApp } from "../stack/client";
import { stackServerApp } from "../stack/server";
import { api } from "./_generated/api";
import { action, mutation, query } from "./_generated/server";


Expand Down Expand Up @@ -65,30 +65,3 @@ export const addNumber = mutation({
},
});

// You can fetch data from and send data to third-party APIs via an action:
export const myAction = action({
// Validators for arguments.
args: {
first: v.number(),
second: v.string(),
},

// Action implementation.
handler: async (ctx, args) => {
//// Use the browser-like `fetch` API to send HTTP requests.
//// See https://docs.convex.dev/functions/actions#calling-third-party-apis-and-using-npm-packages.
// const response = await ctx.fetch("https://api.thirdpartyservice.com");
// const data = await response.json();

//// Query data by running Convex queries.
const data = await ctx.runQuery(api.myFunctions.listNumbers, {
count: 10,
});
console.log(data);

//// Write data by running Convex mutations.
await ctx.runMutation(api.myFunctions.addNumber, {
value: args.first,
});
},
});
8 changes: 5 additions & 3 deletions examples/convex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"dependencies": {
"@stackframe/stack": "workspace:*",
"@stackframe/stack-shared": "workspace:*",
"convex": "^1.27.0",
"next": "15.2.3",
"react": "^19.0.0",
Expand All @@ -31,11 +32,12 @@
"@types/react-dom": "^19",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"rimraf": "^5.0.5",
"typescript": "5.3.3",
"npm-run-all": "^4.1.5",
"postcss": "^8",
"prettier": "^3.5.3",
"rimraf": "^5.0.5",
"tailwindcss": "^4",
"postcss": "^8"
"typescript": "5.3.3",
"wait-on": "^8.0.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1763,7 +1763,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
async getConvexHttpClientAuth(options: { tokenStore: TokenStoreInit }): Promise<string> {
const session = await this._getSession(options.tokenStore);
const tokens = await session.getOrFetchLikelyValidTokens(20_000);
return tokens?.accessToken.token ?? throwErr("No access token available");
return tokens?.accessToken.token ?? "";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return tokens?.accessToken.token ?? "";
return tokens?.accessToken.token ?? throwErr("No access token available");

Changed error handling behavior from throwing an error to returning empty string when no access token is available, which can cause silent failures in server-side authentication.

View Details

Analysis

Silent authentication failure in getConvexHttpClientAuth() method

What fails: getConvexHttpClientAuth() in packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts returns empty string instead of throwing error when no access token is available, causing silent authentication failures

How to reproduce:

  1. Call stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" }) when user is not authenticated
  2. Method returns "" instead of throwing error
  3. Empty string is passed to convex.setAuth(token) in server-side code like examples/convex/app/server/page.tsx
  4. Subsequent convex.query() calls operate without authentication instead of failing explicitly

Result: Authentication failures become silent - instead of clear error indicating missing token, Convex receives empty string and treats user as unauthenticated without obvious indication

Expected: Method should throw clear error "No access token available" as originally implemented, making authentication issues immediately apparent for debugging

Evidence: Git commit 6d0bab553ff198ec97e6ad70e2322bda4fa2dc15 changed behavior from throwErr("No access token available") to return "" on 2025-10-10

}
Comment on lines 1763 to 1767
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that all callers handle empty auth strings correctly.

This change modifies error signaling from throwing an exception to returning an empty string when no access token is available. Callers that previously relied on catching exceptions for missing tokens will now receive an empty string, which could lead to silent authentication failures if not properly handled downstream.

Run the following script to find all usages of getConvexHttpClientAuth and verify they handle empty string returns:

Additionally, check if Convex clients properly handle empty auth strings or if they require non-empty tokens:


🏁 Script executed:

#!/bin/bash
# Search for all call sites of getConvexHttpClientAuth
rg -n -C5 'getConvexHttpClientAuth\s*\(' --type=ts --type=tsx

Length of output: 27


🌐 Web query:

Does ConvexHttpClient.setAuth() handle empty strings gracefully, or does it require a valid token?

💡 Result:

Short answer: setAuth accepts a string but an empty string is not treated as "no token" — it will result in an Authorization: Bearer header (and the backend will reject it). Use clearAuth() (or pass null/undefined where supported by that API) to remove auth. [1][2]

Sources:

  • ConvexHttpClient API (setAuth(value: string) signature). [1]
  • Convex auth debugging docs (notes that null/undefined → auth skipped; implies empty string is different). [2]

#!/bin/bash
# Search for all call sites of getConvexHttpClientAuth across the repo
rg -n -C5 'getConvexHttpClientAuth\s*\(' .

#!/bin/bash
# Search for all call sites of getConvexHttpClientAuth across the repo
rg -n -C5 'getConvexHttpClientAuth\s*\(' .

🏁 Script executed:

#!/bin/bash
# Search for all call sites of getConvexHttpClientAuth across the repo
rg -n -C5 'getConvexHttpClientAuth\s*\(' .

Length of output: 6283


Handle empty auth strings in all callers
Returning "" on missing tokens no longer throws, so examples (user-info.ts, server/page.tsx) and tests must check for "" and either throw a descriptive error or call clearAuth() instead of setAuth(""). Otherwise the backend will reject the request silently and E2E tests will fail.

🤖 Prompt for AI Agents
In packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
around lines 1686–1690, getConvexHttpClientAuth currently returns an empty
string when tokens are missing; update all callers (at minimum examples
user-info.ts and server/page.tsx and any E2E tests) to treat "" as a
missing-auth condition: after calling getConvexHttpClientAuth, check if the
returned string is empty and either throw a descriptive error (e.g., "No auth
token available; please sign in") or call clearAuth() instead of calling
setAuth("") so the backend won’t silently reject requests; make the same
explicit handling in tests (assert that callers throw or call clearAuth) and
update any example flows to show the correct behavior.


protected async _updateClientUser(update: UserUpdateOptions, session: InternalSession) {
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading