#What is the best way to create a signup flow after a user is created?

11 messages · Page 1 of 1 (latest)

bitter osprey
#

I am using Next.js and Convex Auth. I want to create a stripe customer on signup. I tried the following logic:

//page.ts
onSubmit={async (e) => {
          e.preventDefault();
          const formData = new FormData(e.target as HTMLFormElement);
          formData.set("flow", flow);
          await void signIn("password", formData)
            .catch((error) => {
              setError(error.message);
            })
            .then(() => {
              onSignUp();
            })
            .then(() => {
              router.push("/");
            });
        }}

And then onSignUp:

export const onSignUp = action({
  args: {},
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("User not found");
    // Rest of the logic goes here
  },
});

The issue is that the users is not authenticated by the time I want to invoke the onSignUp Action.

Am I implementing it wrong, or do I need to go about it a different way? What is the best way to create a user flow upon a user signing up?

sterile shoreBOT
#

Hi @bitter osprey. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

You're encountering a common challenge with Convex Auth and Next.js: after a user signs up (using the password provider), the authentication state may not be immediately available for subsequent actions (like creating a Stripe customer) that require the user to be authenticated. This is due to the asynchronous nature of authentication state propagation between the client and Convex backend.

Why This Happens

When you call signIn("password", formData) with the "signUp" flow, Convex Auth automatically signs the user in after sign up. However, the authentication state on the client may not be updated instantly, so if you immediately call an action (like onSignUp) that depends on getAuthUserId(ctx), it may return null because the session isn't established yet. This is a race condition between the sign-in process and your follow-up logic.

This issue is discussed in the Convex community, and there isn't a built-in, perfectly reliable way to guarantee the session is available immediately after sign up. Some have considered using a timeout as a workaround, but this is not robust or recommended for production use. The core of the problem is that Convex Auth does not provide a dedicated signUp function separate from signIn, and the session state update is asynchronous on the client side. See the discussion here.

Recommended Approaches

1. Trigger Post-Signup Logic After Authentication is Confirmed

Instead of calling onSignUp immediately after signIn, wait until the authentication state is confirmed on the client. You can do this by listening for the authenticated state in your React component and then triggering your post-signup logic (e.g., creating a Stripe customer).

#

For example, use the useConvexAuth() hook to check when the user is authenticated, and only then call your action:

import { useConvexAuth } from "convex/react";
import { useEffect } from "react";
import { api } from "../convex/_generated/api";
import { useMutation } from "convex/react";

const onSignUp = useMutation(api.onSignUp);

function SignupFlow() {
  const { isAuthenticated } = useConvexAuth();

  useEffect(() => {
    if (isAuthenticated) {
      onSignUp();
    }
  }, [isAuthenticated, onSignUp]);

  // ...rest of your component
}

This ensures that your action only runs when the user is actually authenticated, avoiding the race condition.

2. Customizing the Auth Provider (Advanced)

If you need more control (e.g., to prevent automatic sign-in after sign-up), you could fork the Convex Auth password provider and customize its behavior, as discussed by Convex support. This is more advanced and requires maintaining your own provider logic, but it allows you to decouple sign-up from sign-in if needed. See the discussion here.

3. Accept the Race and Retry

Some developers use a retry mechanism or a short delay (e.g., setTimeout) before calling the post-signup action, but this is not robust and can lead to flaky behavior. It's better to rely on state-based triggers as shown above.


Summary:
The best practice is to trigger your post-signup logic (like creating a Stripe customer) only after the authentication state is confirmed on the client, not immediately after the sign-in promise resolves. Use the useConvexAuth() hook to detect when the user is authenticated, and then call your action. This avoids the race condition and ensures the user is available in Convex Auth when you need them.

#

@bitter osprey Kapa.ai is still learning and improving, please let me know how I did by reacting below.

bitter osprey
#

@sterile shore So from what I can tell your solution actually conflicts with the middleware:

import {
  convexAuthNextjsMiddleware,
  createRouteMatcher,
  nextjsMiddlewareRedirect,
} from "@convex-dev/auth/nextjs/server";

const isSignInPage = createRouteMatcher(["/signin"]);
const isProtectedRoute = createRouteMatcher(["/", "/server"]);

export default convexAuthNextjsMiddleware(async (request, { convexAuth }) => {
  if (isSignInPage(request) && (await convexAuth.isAuthenticated())) {
    return nextjsMiddlewareRedirect(request, "/");
  }
  if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) {
    return nextjsMiddlewareRedirect(request, "/signin");
  }
});

export const config = {
  // The following matcher runs middleware on all routes
  // except static assets.
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

Does that sound right to you?

It looks like other options might be?

  1. Don't use middleware
  2. Use on .push('/?signUp=true') and handle it on the the next page.

Unless I am wrong about middleware and/or you have a better alternative

sterile shoreBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You're correct that using middleware to protect routes can introduce a race condition immediately after sign-up/sign-in, because the authentication state may not be established when the user is redirected to a protected route. This can result in the user being redirected back to the sign-in page, even though they just signed up or signed in. This issue is well-documented in the Convex community and is a common challenge with Next.js + Convex Auth flows.

Key points from the knowledge sources:

  • Middleware and Race Conditions:
    Middleware like your example is the recommended way to protect routes in Next.js with Convex Auth. However, as discussed in the community, there is a race condition: after sign-in, the authentication state may not be available immediately, so the middleware may treat the user as unauthenticated and redirect them away from protected routes. This is not a bug in your code, but a limitation of how authentication state propagates between the client and server in Next.js and Convex Auth. The Convex team has acknowledged this issue and suggested that you must wait for the authentication state to be established before redirecting to protected routes or making authenticated queries. This is typically done by checking isAuthenticated from useConvexAuth() before proceeding with sensitive actions or navigation [Authentication Best Practices: Convex, Clerk and NextJs].
#
  • Alternatives:
    1. Don't use middleware:
      You can remove server-side auth entirely and rely on client-side checks, as suggested by Convex support. This is possible, but you lose the security and UX benefits of server-side route protection [Discord: Not using middleware in nextjs].
    2. Redirect with a flag (e.g., /?signUp=true):
      After sign-up, redirect to a public route with a flag (like /?signUp=true) and handle the post-signup logic on that page, waiting for isAuthenticated to be true before running any actions. This avoids the race condition with middleware, since the landing page is not protected and you can safely wait for authentication before proceeding.

Summary:
Your understanding is correct: the middleware can conflict with immediate post-signup flows due to the race condition. The two main alternatives are:

  • Remove middleware and rely on client-side checks (less secure).
  • Redirect to a public route after sign-up and handle post-signup logic there, waiting for authentication to be established before proceeding.

There is no perfect workaround in the current Convex Auth + Next.js setup, but the second approach (redirecting to a public route and waiting for authentication) is the most robust and is recommended by the Convex team and community [Authentication Best Practices: Convex, Clerk and NextJs].

#

If you need to protect routes with middleware, you must ensure that any post-signup logic that requires authentication is only triggered after the client confirms the user is authenticated. Otherwise, you risk running into the race condition you described.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: