Quick start
Wire Logto auth into a Convex + Vite React app end to end, in six steps. Session mode, so the browser never holds a refresh token.
The Vite + React happy path, top to bottom, in session mode. Copy it as is and your Convex functions will see a signed-in user. On TanStack Router / Start, Next.js, or Expo? Steps 1 to 3 are the same; the provider and callback differ; see Integrations. If you want Logto's SDK in the browser instead, follow Bridge mode.
Before you start, rotate the Logto signing key to RSA. Convex only accepts ID
tokens signed with RS256 (or EdDSA). Logto signs with ES384 by default, which
Convex rejects without an error. Sign-in looks like it works, but
ctx.auth.getUserIdentity() returns null. In the Logto Console open Tenant
settings → OIDC configs → Rotate private keys and choose RSA. One-time, per
tenant; Logto keeps the old key during a transition, so existing sessions stay signed in.
Install
pnpm add convex-logtoconvex and react are peers you already have. There is no Logto SDK to
install; your Convex deployment is the OAuth client. For React Native, see
Expo.
1. Create a Logto app
In Logto Console → Applications → Create application → Traditional
web. Pick this type even though your frontend is a SPA. The Convex deployment
holds the app secret, and only a Traditional web app has one. A Single page
app has no secret, and a Third-party app withholds the profile /
email scopes this package requests. You can't change the type after creation.
Note the endpoint (e.g. https://auth.example.com), the App ID, and the
App Secret, and add two URLs on the app (for each environment):
- Redirect URIs →
http://localhost:5173/callback(and your prod callback) - Post sign-out redirect URIs →
http://localhost:5173(your app's origin, and your prod origin)
signIn() returns to the redirect URI and signOut() to the post-sign-out URI,
so add both.
2. Set the config
Everything goes on the Convex deployment. The frontend has no Logto env vars:
npx convex env set LOGTO_ENDPOINT https://auth.example.com
npx convex env set LOGTO_APP_ID your-app-id
npx convex env set LOGTO_CLIENT_SECRET your-app-secretThe app secret stays on the deployment and never reaches the browser.
The endpoint may include a reverse-proxy path prefix, but it must be the Logto
base URL (not the trailing /oidc issuer URL) and must not contain credentials,
a query, or a fragment. The library requires HTTPS; loopback HTTP works for
local development. If an existing self-hosted deployment can only be reached
over non-loopback HTTP, add allowInsecureHttp: true to both logtoAuthConfig
and logtoSessionApi. That option exists for compatibility. Terminate TLS
instead when you can.
3. Wire Convex
Three files. The component holds the Logto refresh tokens, auth.config.ts
tells Convex to validate Logto's ID token, and auth.ts exposes the functions
the provider calls.
import { defineApp } from "convex/server";
import logto from "convex-logto/convex.config";
const app = defineApp();
app.use(logto);
export default app;import { logtoAuthConfig } from "convex-logto";
export default { providers: [logtoAuthConfig()] };import { logtoSessionApi } from "convex-logto";
import { components } from "./_generated/api";
export const {
signIn,
callback,
refresh,
signOut,
signOutEverywhere,
listSessions,
renameSession,
revokeSession,
exchangeToken,
fetchUserInfo,
sessionValid,
} = logtoSessionApi(components.logto);Re-export all eleven with these exact names. The provider looks them up on the
module you pass it. Run npx convex dev once after adding convex.config.ts;
that regenerates _generated/api with components.logto on it.
4. Wrap your app
import { ConvexReactClient } from "convex/react";
import { ConvexLogtoSessionProvider } from "convex-logto/react-session";
import { api } from "../convex/_generated/api";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL, {
// Without this, Convex confirms the cached token and then refetches a fresh
// one at once. That is a Logto refresh grant and a session-token rotation
// on every page load. Convex still marks the option experimental.
initialAuthTokenReuse: true,
});
root.render(
<ConvexLogtoSessionProvider
client={convex}
sessionApi={api.auth}
onAuthError={(error) => console.error("auth error", error)}
>
<App />
</ConvexLogtoSessionProvider>,
);sessionApi is the module from step 3. onAuthError is where a failed sign-in
or sign-out surfaces (Logto unreachable, a stale callback, blocked storage);
wire it to a toast. One token round trip per page
load
explains initialAuthTokenReuse.
Using a router or SSR framework? Pass navigate so the post-sign-in landing is
a soft, replace-style navigation, and see Integrations
for where the provider lives in TanStack Router / Start, Next.js, and Expo.
5. Add a callback route
signIn() lands on /callback. The provider owns that path. It POSTs the code
to your callback action, stores the credentials, and replace-navigates to
afterSignIn (default /), so whatever you render there is only ever a flash.
In a plain Vite app with no router:
if (window.location.pathname === "/callback") return <p>Finishing sign in…</p>;6. Sign in, and read the user
import { useLogtoAuth } from "convex-logto/react-session";
function Header() {
const { isAuthenticated, isLoading, user, signIn, signOut } = useLogtoAuth();
if (isLoading) return null;
return isAuthenticated ? (
<button onClick={() => void signOut()}>Sign out ({user?.email ?? user?.sub})</button>
) : (
<button onClick={() => void signIn()}>Sign in</button>
);
}The void signIn() event-handler form is fine. The provider reports a failure
to onAuthError and the console before the promise rejects, so nothing fails
silently.
signOut() clears this browser, deletes the server session, then ends the
Logto session and returns the user to your origin (the Post sign-out redirect
URI from step 1). Pass signOut({ postLogoutRedirectUri: "https://app.com/bye" })
to land somewhere else; register that URI too. Don't follow it with a hard
navigation of your own (location.href = ...). signOut() resolves once it
has told the browser to leave for Logto, and a second full-page navigation
supersedes the request that ends the Logto session.
In any Convex function, the Logto identity is already there:
import { query } from "./_generated/server";
export const me = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
// identity.subject = Logto user id, plus email/name/etc. from the ID token
return { id: identity.subject, email: identity.email, name: identity.name };
},
});That is the whole auth setup. Many apps need nothing more. From here:
- Auth in your app gates and routes on auth state.
- Session mode covers what you now have: the device
list,
signOutEverywhere(), device binding, the HttpOnly cookie transport for apps with a same-site server, and how the component classifies a failed refresh. - Webhook sync mirrors users into a table and, with the component attached, revokes a deleted or suspended user's sessions within seconds.
The runnable version is
examples/vite-react-session.