-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Integrate Stack Auth #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,9 @@ | ||
| "use server"; | ||
|
|
||
| import { getSignInUrl, getSignUpUrl } from "@workos-inc/authkit-nextjs"; | ||
|
|
||
| export async function getSignInUrlAction() { | ||
| return await getSignInUrl(); | ||
| return "/handler/sign-in"; | ||
| } | ||
|
|
||
| export async function getSignUpUrlAction() { | ||
| return await getSignUpUrl(); | ||
| return "/handler/sign-up"; | ||
| } | ||
|
|
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { StackHandler } from "@stackframe/stack"; | ||
| import { stackTheme } from "@/stack-theme"; | ||
|
|
||
| export default function StackHandlerPage() { | ||
| return <StackHandler app={stackTheme} />; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import { useAuth } from "@workos-inc/authkit-nextjs/components"; | ||||||||||||||||||||||||||
| import { useUser } from "@stackframe/stack"; | ||||||||||||||||||||||||||
| import { useRouter } from "next/navigation"; | ||||||||||||||||||||||||||
| import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; | ||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||
|
|
@@ -19,24 +19,24 @@ interface Props { | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export const UserControl = ({ showName }: Props) => { | ||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||
| const { user, signOut } = useAuth(); | ||||||||||||||||||||||||||
| const user = useUser(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!user) return null; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const handleSignOut = async () => { | ||||||||||||||||||||||||||
| await signOut(); | ||||||||||||||||||||||||||
| await user.signOut(); | ||||||||||||||||||||||||||
| router.push("/"); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email; | ||||||||||||||||||||||||||
| const displayName = user.displayName || user.primaryEmail || "User"; | ||||||||||||||||||||||||||
| const initials = displayName | ||||||||||||||||||||||||||
| ?.split(" ") | ||||||||||||||||||||||||||
| .map((n) => n[0]) | ||||||||||||||||||||||||||
| .join("") | ||||||||||||||||||||||||||
| .toUpperCase() | ||||||||||||||||||||||||||
| .slice(0, 2) || "U"; | ||||||||||||||||||||||||||
|
Comment on lines
32
to
37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix TypeScript error: Parameter 'n' implicitly has 'any' type. The map callback parameter needs an explicit type annotation to satisfy TypeScript's strict mode. Apply this diff to add the type annotation: const initials = displayName
?.split(" ")
- .map((n) => n[0])
+ .map((n: string) => n[0])
.join("")
.toUpperCase()
.slice(0, 2) || "U";📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Actions: CI[error] 34-34: TS7006: Parameter 'n' implicitly has an 'any' type. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const avatarSrc = user.profilePictureUrl || undefined; | ||||||||||||||||||||||||||
| const avatarSrc = user.profileImageUrl || undefined; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <DropdownMenu> | ||||||||||||||||||||||||||
|
|
@@ -56,7 +56,7 @@ export const UserControl = ({ showName }: Props) => { | |||||||||||||||||||||||||
| <div className="flex flex-col space-y-1"> | ||||||||||||||||||||||||||
| <p className="text-sm font-medium leading-none">{displayName}</p> | ||||||||||||||||||||||||||
| <p className="text-xs leading-none text-muted-foreground"> | ||||||||||||||||||||||||||
| {user.email} | ||||||||||||||||||||||||||
| {user.primaryEmail} | ||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </DropdownMenuLabel> | ||||||||||||||||||||||||||
|
|
@@ -65,7 +65,7 @@ export const UserControl = ({ showName }: Props) => { | |||||||||||||||||||||||||
| <User className="mr-2 h-4 w-4" /> | ||||||||||||||||||||||||||
| <span>Dashboard</span> | ||||||||||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||||||||||
| <DropdownMenuItem onClick={() => router.push("/settings")}> | ||||||||||||||||||||||||||
| <DropdownMenuItem onClick={() => router.push("/handler/account-settings")}> | ||||||||||||||||||||||||||
| <Settings className="mr-2 h-4 w-4" /> | ||||||||||||||||||||||||||
| <span>Settings</span> | ||||||||||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,45 +1,51 @@ | ||
| import { StackServerApp } from "@stackframe/stack"; | ||
| import { ConvexHttpClient } from "convex/browser"; | ||
| import { withAuth } from "@workos-inc/authkit-nextjs"; | ||
|
|
||
| /** | ||
| * Get the authenticated user from WorkOS AuthKit | ||
| * Get the authenticated user from Stack Auth | ||
| */ | ||
| export async function getUser() { | ||
| try { | ||
| const { user } = await withAuth(); | ||
| return user; | ||
| } catch (error) { | ||
| console.error("Failed to get user:", error); | ||
| return null; | ||
| } | ||
| const stack = new StackServerApp({ | ||
| tokenStore: "nextjs-cookie", | ||
| }); | ||
| const user = await stack.getUser(); | ||
|
|
||
| if (!user) return null; | ||
|
|
||
| // Map Stack user to a structure compatible with existing code where possible | ||
| // or return the Stack user extended with compatibility fields | ||
|
|
||
| const displayName = user.displayName || ""; | ||
| const parts = displayName.split(" "); | ||
| const firstName = parts[0] || ""; | ||
| const lastName = parts.slice(1).join(" ") || ""; | ||
|
|
||
| return { | ||
| ...user, | ||
| // Compatibility fields | ||
| email: user.primaryEmail, | ||
| firstName: firstName, | ||
| lastName: lastName, | ||
| // Ensure id is present (it is) | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Get the authentication token for Convex | ||
| */ | ||
| export async function getToken() { | ||
| try { | ||
| const { accessToken } = await withAuth(); | ||
| return accessToken || null; | ||
| } catch (error) { | ||
| console.error("Failed to get token:", error); | ||
| return null; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Get auth headers for API calls | ||
| */ | ||
| export async function getAuthHeaders() { | ||
| const token = await getToken(); | ||
| if (!token) return {}; | ||
| return { | ||
| Authorization: `Bearer ${token}`, | ||
| }; | ||
| return {}; | ||
| } | ||
|
Comment on lines
36
to
45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainStubbed token/auth helpers will drop server-side authorization
If that’s intentional (e.g., you’re temporarily disabling those code paths), it’d be safer to either delete these helpers or clearly guard their callers. Otherwise, they should be updated to retrieve the appropriate Stack token and call I recommend grepping for usages of these helpers to understand impact: And consult Stack Auth + Convex docs for the proper way to obtain and pass an OIDC/JWT token from Stack into Convex or your own APIs. Also applies to: 57-66 🏁 Script executed: #!/bin/bash
rg -n "getToken\\(" -S
rg -n "getAuthHeaders\\(" -S
rg -n "getConvexClientWithAuth\\(" -SLength of output: 1676 🌐 Web query: 💡 Result: Short answer — use the Stack Next.js server SDK to read the user’s session server‑side (via the StackServerApp / Next.js cookie token store), extract the JWT (ID or access token) and forward it to Convex (or your backend) as a Bearer token. Convex expects an OIDC ID token (or a JWT that satisfies your convex/auth.config.ts) and will validate iss/aud/jwks. [1][2] How (high level)
Concrete pointers / reading
If you want, tell me:
Sources I need to examine the actual usage in the API routes and check how Stack Auth is initialized in the codebase. 🏁 Script executed: #!/bin/bash
# View the API routes that call getConvexClientWithAuth
echo "=== fix-errors/route.ts ===" && cat src/app/api/fix-errors/route.ts
echo ""
echo "=== messages/update/route.ts ===" && cat src/app/api/messages/update/route.tsLength of output: 4715 🏁 Script executed: #!/bin/bash
# Search for Stack Auth initialization or StackServerApp
rg -n "StackServerApp|StackAuth|initializeStackAuth" -C3Length of output: 3375 🏁 Script executed: #!/bin/bash
# Check if there's any conditional logic around auth helpers or TODOs
rg -n "TODO|FIXME|disable|stub" src/lib/auth-server.ts -C2Length of output: 266 🏁 Script executed: #!/bin/bash
# Look for convex/auth.config.ts or Stack Auth configuration
fd -t f -n "auth\.config\|auth\.ts\|\.env" | head -20Length of output: 230 Let me view the complete 🏁 Script executed: #!/bin/bash
# View the full auth-server.ts file to see getConvexClientWithAuth
cat src/lib/auth-server.tsLength of output: 1811 🏁 Script executed: #!/bin/bash
# Search for convex auth config
find . -type f -name "auth.config.*" 2>/dev/null | head -10Length of output: 23 🏁 Script executed: #!/bin/bash
# View the convex auth config to understand what token is expected
cat convex/auth.config.tsLength of output: 188 🏁 Script executed: #!/bin/bash
# Search for how tokens are accessed from Stack in the codebase
rg -n "accessToken|idToken|getToken|forceRefreshToken" src/lib/auth-server.ts -C3
rg -n "accessToken|idToken" -C2 --type=ts --type=tsx | head -40Length of output: 153 Implement server-side authentication bridge between Stack Auth and Convex The review comment is validated. The codebase has
This matches 🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Create a Convex HTTP client with WorkOS authentication | ||
| * Create a Convex HTTP client with Stack Auth authentication | ||
| * Use this in API routes that need to call Convex | ||
| */ | ||
| export async function getConvexClientWithAuth() { | ||
|
|
@@ -50,11 +56,12 @@ export async function getConvexClientWithAuth() { | |
|
|
||
| const httpClient = new ConvexHttpClient(convexUrl); | ||
|
|
||
| const { accessToken } = await withAuth(); | ||
| // We need to properly authenticate the Convex client | ||
| // Stack Auth usually uses the OIDC token for Convex | ||
| // The Convex HTTP client setAuth(token) expects a token. | ||
|
|
||
| if (accessToken) { | ||
| httpClient.setAuth(accessToken); | ||
| } | ||
| // TODO: Retrieve the OIDC token for Convex from Stack Auth if available server-side | ||
| // For now, we return the client. If queries are protected, they might fail if we don't setAuth. | ||
|
|
||
| return httpClient; | ||
|
Comment on lines
+63
to
66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Stack migration leaves Useful? React with 👍 / 👎. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,6 @@ | ||
| import { authkitMiddleware } from '@workos-inc/authkit-nextjs'; | ||
| import { stackMiddlewares } from "@stackframe/stack/next"; | ||
|
|
||
| export default authkitMiddleware({ | ||
| redirectUri: process.env.WORKOS_REDIRECT_URI || ( | ||
| process.env.NODE_ENV === "production" | ||
| ? "https://zapdev.link/auth/callback" | ||
| : "http://localhost:3000/auth/callback" | ||
| ), | ||
| }); | ||
| export default stackMiddlewares; | ||
|
Comment on lines
+1
to
+3
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainFix missing CI cannot resolve You can quickly verify dependency wiring with: 🏁 Script executed: #!/bin/bash
# From repo root
if [ -f package.json ]; then
echo "Dependency entry for @stackframe/stack:"
grep -n "\"@stackframe/stack\"" package.json || echo "Not found in package.json"
fi
echo
echo "All imports of @stackframe/stack in the codebase:"
rg -n "@stackframe/stack" -S || echo "No imports found"Length of output: 2681 Length of output: 2271 Add The 🧰 Tools🪛 GitHub Actions: CI[error] 1-1: Cannot find module '@stackframe/stack/next' or its corresponding type declarations. 🤖 Prompt for AI Agents |
||
|
|
||
| export const config = { | ||
| matcher: [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Tighten Convex auth adapter: handle loading state and
forceRefreshTokenThe
useAuthImplshape matches whatConvexProviderWithAuthexpects, but two details are worth tightening:isLoadingis alwaysfalse, so Convex never sees an auth‑loading phase. If Stack Auth’s user state can be “initializing”, you may want to reflect that here to avoid early unauthenticated queries.forceRefreshTokenis ignored infetchAccessToken. Convex passes this flag when it explicitly needs a fresh JWT; ifapp.getAccessToken()internally caches tokens, you may need to thread the flag through (e.g., via an options object) so forced refreshes actually happen.These aren’t immediate correctness bugs if
getAccessTokenis already always fresh anduseUseris effectively synchronous in your setup, but aligning with Convex’s custom provider pattern will make future auth/debugging much easier.Please double‑check both sides in the docs:
Also applies to: 61-62
🌐 Web query:
💡 Result:
Recommended pattern — summary
Wrap your app with the provider from your identity SDK (e.g. Clerk, WorkOS, Auth0) and inside that tree call a small adapter hook that you pass to Convex's ConvexProviderWithAuth. Don’t pass the identity-provider hook itself as a plain value; call it inside a component so you obey the Rules of Hooks. [1][2]
The adapter must return { isLoading, isAuthenticated, fetchAccessToken } where fetchAccessToken({ forceRefreshToken }) fetches the ID/JWT for Convex and, when forceRefreshToken is true, forces a fresh token (i.e. bypasses any client cache). Map forceRefreshToken to the provider’s equivalent (examples: ignoreCache / skipCache / { forceRefresh: true } etc.). Memoize the returned object and the fetchAccessToken callback. [1][2]
Minimal example (conceptual)
Concrete references
Sources
[1] Convex docs — Custom OIDC / custom auth integration.
[2] Convex repo / ConvexProviderWithClerk implementation (Clerk adapter example).
Verify Stack Auth's token refresh API and loading state exposure
The web search confirms that Convex's recommended pattern explicitly requires:
forceRefreshTokenis true, force a fresh token (bypass client cache) and map it to the provider's equivalent optionisLoadingstate reflecting the provider's auth initializationYour code has two confirmed gaps:
forceRefreshTokenignored: The parameter is accepted but not passed toapp.getAccessToken(). You need to check whether Stack's token method accepts an options parameter (e.g.,{ forceRefresh: true }or similar) and thread the flag through.isLoadinghardcoded tofalse: Check whetheruseStackApp()oruseUser()exposes a loading/initializing state you should reflect here.Verify Stack Auth's API surface (getAccessToken signature, loading state availability) and apply the mapping pattern shown in the Convex Clerk adapter example.
🤖 Prompt for AI Agents