Add authentication and analytics to your Next.js app.
Next.js Integration
This guide covers the @ascendkit/nextjs SDK with Next.js 14+ (App Router). For Python backends, see Python SDK. For other React frameworks, see Other React Frameworks at the bottom of this page.
Auth setup
After installing the SDK and initializing the CLI, follow these four steps:
1. Create the auth runtime
// lib/auth.ts
import { createAscendKitAuthRuntime } from "@ascendkit/nextjs/server";
export const authRuntime = createAscendKitAuthRuntime();
The runtime reads ASCENDKIT_ENV_KEY, ASCENDKIT_SECRET_KEY, and ASCENDKIT_API_URL from your environment automatically. We recommend setting APP_URL in your env file to your app's origin (e.g., http://localhost:3000 for local dev) — the SDK can infer it from requests, but an explicit value avoids "Invalid origin" errors with social login. If you prefer explicit configuration:
export const authRuntime = createAscendKitAuthRuntime({
publicKey: process.env.ASCENDKIT_ENV_KEY!,
});
Options
| Option | Type | Description |
|---|---|---|
publicKey | string | Environment public key (default: ASCENDKIT_ENV_KEY env var) |
secretKey | string | Environment secret key (default: ASCENDKIT_SECRET_KEY env var) |
authSecret | string | Better Auth session secret (default: BETTER_AUTH_SECRET, then AUTH_SECRET, then ASCENDKIT_SECRET_KEY) |
apiUrl | string | AscendKit API URL (default: ASCENDKIT_API_URL or https://api.ascendkit.dev) |
appBaseUrl | string | Public app origin used for auth callbacks and redirects |
allowedHosts | string[] | Optional allowlist for dynamic callback host resolution |
trustedProxyHeaders | boolean | Trust x-forwarded-* headers when deriving the request origin |
waitlistRedirectPath | string | Redirect path for waitlisted users after social login (e.g. "/waitlist") |
rejectedRedirectPath | string | Redirect path for rejected users after social login (e.g. "/rejected") |
Example with waitlist redirect:
export const authRuntime = createAscendKitAuthRuntime({
waitlistRedirectPath: "/waitlist",
rejectedRedirectPath: "/rejected",
});
If your app has a fixed public URL, configure it through AscendKit:
export const authRuntime = createAscendKitAuthRuntime({
appBaseUrl: process.env.APP_URL,
});
APP_URL should be the origin where your app is running — http://localhost:3000 for local development, or https://app.yourdomain.com for production. You can leave it blank and let the SDK infer the origin from the request, but an explicit value is more reliable.
If you use preview deployments, allow dynamic hosts explicitly:
export const authRuntime = createAscendKitAuthRuntime({
allowedHosts: ["localhost:3000", "*.vercel.app", "app.example.com"],
});
When a user signs up via social login (Google, GitHub, etc.) and lands on the waitlist, they are redirected to waitlistRedirectPath instead of receiving a ?error=waitlist_pending query param. The same applies to rejected users. If these options are not set, the default behavior appends ?error=waitlist_pending or ?error=waitlist_rejected to the callback URL.
2. Add the API route
// app/api/auth/[...all]/route.ts
import { authRuntime } from "@/lib/auth";
import { createAuthRouteHandlers } from "@ascendkit/nextjs/server";
export const { GET, POST } = createAuthRouteHandlers(authRuntime);
3. Add the provider
// app/layout.tsx
import { AscendKitProvider } from "@ascendkit/nextjs";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AscendKitProvider>
{children}
</AscendKitProvider>
</body>
</html>
);
}
The provider reads NEXT_PUBLIC_ASCENDKIT_ENV_KEY and NEXT_PUBLIC_ASCENDKIT_API_URL from the environment. If you prefer explicit configuration:
<AscendKitProvider publicKey={process.env.NEXT_PUBLIC_ASCENDKIT_ENV_KEY!}>
For development diagnostics, enable debug mode:
<AscendKitProvider debug>
{children}
</AscendKitProvider>
You can also set NEXT_PUBLIC_ASCENDKIT_DEBUG=1. Debug mode logs auth state transitions, settings resolution, redirect decisions, and component initialization failures. Secrets and tokens are redacted.
4. Add sign-in and sign-up
<AscendKitProvider> already mounts a global auth modal for you — you don't need to render one yourself. You have three UX patterns to choose from. Pick the one that fits your app; you can mix them.
Which option should I use?
| Scenario | Recommended option |
|---|---|
| Marketing site or landing page — user clicks "Sign in" in the header and stays on the page | Modal — <SignInButton /> / <SignUpButton /> (default mode="modal") |
Dedicated /login and /signup routes with the same styling everywhere | Dedicated page (all-in-one) — <AscendKitAuthCard /> |
| Fully custom layouts or marketing content next to a login form | Dedicated page (split) — <Login /> and <SignUp /> |
| App with a custom nav button that should open auth without navigating | Hook — useAuthModal() |
| Gate protected content until the user signs in | Wrap with <SignedIn> / <SignedOut> and trigger the modal from the fallback |
Option A — Modal (no auth page needed)
Drop buttons anywhere in your layout. AscendKitProvider handles everything else.
// app/layout.tsx (inside the provider)
import { SignInButton, SignUpButton, SignedIn, SignedOut, AscendKitUserButton } from "@ascendkit/nextjs";
<header>
<SignedOut>
<SignInButton />
<SignUpButton />
</SignedOut>
<SignedIn>
<AscendKitUserButton />
</SignedIn>
</header>
Want to use your own button component? Pass asChild:
import { Button } from "@/components/ui/button";
<SignInButton asChild>
<Button variant="ghost">Log in</Button>
</SignInButton>
Need to open the modal programmatically (e.g. after a pricing CTA or analytics event)? Use the hook:
import { useAuthModal } from "@ascendkit/nextjs";
function UpgradeCta() {
const { open } = useAuthModal();
return <button onClick={() => open("sign-up")}>Get started</button>;
}
Option B — Dedicated page with AscendKitAuthCard
Use this when you want a real /login route (e.g. for SEO, email deep links, or password managers). One component handles sign-in, sign-up, and forgot-password views.
// app/login/page.tsx
import { AscendKitAuthCard } from "@ascendkit/nextjs";
export default function AuthPage() {
return <AscendKitAuthCard />;
}
Pin a specific view if you have separate routes for sign-in and sign-up:
// app/signup/page.tsx
<AscendKitAuthCard view="SIGN_UP" />
Views: "SIGN_IN", "SIGN_UP", "FORGOT_PASSWORD".
Option C — Separate Login / SignUp components
Use this when you want full control of each page (e.g. marketing copy beside the form, different layouts for sign-in vs sign-up).
// app/login/page.tsx
import { Login } from "@ascendkit/nextjs";
export default function LoginPage() {
return (
<Login
onSuccess={() => { window.location.href = "/dashboard"; }}
onSwitchToSignUp={() => { window.location.href = "/signup"; }}
onForgotPassword={() => { window.location.href = "/forgot-password"; }}
/>
);
}
// app/signup/page.tsx
import { SignUp } from "@ascendkit/nextjs";
export default function SignUpPage() {
return (
<SignUp
onSuccess={() => { window.location.href = "/dashboard"; }}
onSwitchToLogin={() => { window.location.href = "/login"; }}
/>
);
}
Heads up: you don't need
<AscendKitAuthCard />,<Login />, or<SignUp />if you're only usingSignInButton/SignUpButton/useAuthModal()— the provider's built-in modal already renders the forms. Pick one pattern per route and stick with it.
Components
All components render inside <AscendKitProvider>. Social buttons appear automatically when providers are enabled via the CLI or portal.
The SDK wraps Better Auth and Better Auth UI internally. You do not need to import those packages directly, but your app must provide the peer dependencies declared by @ascendkit/nextjs: React, React DOM, and Next.js for server-side auth runtime routes. Non-Next React apps can use client hooks and components, but createAscendKitAuthRuntime and createAuthRouteHandlers are Next.js App Router utilities.
Login / SignUp
| Prop | Type | Description |
|---|---|---|
onSuccess | () => void | Called after success |
onError | (error: string) => void | Called on failure |
callbackURL | string | Redirect after social auth |
onSwitchToSignUp | () => void | (Login) Show "Sign Up" link in footer |
onSwitchToLogin | () => void | (SignUp) Show "Sign In" link in footer |
onForgotPassword | () => void | (Login) Show "Forgot password?" link |
onBackToLogin | () => void | (ForgotPassword) Show "Back to sign in" link |
AscendKitAuthCard
Single component for all auth views. Use instead of separate Login/SignUp pages.
import { AscendKitAuthCard } from "@ascendkit/nextjs";
<AscendKitAuthCard />
<AscendKitAuthCard view="SIGN_UP" />
Views: "SIGN_IN", "SIGN_UP", "FORGOT_PASSWORD"
SocialButton
import { SocialButton } from "@ascendkit/nextjs";
<SocialButton provider="google" />
<SocialButton provider="github" label="Sign in with GitHub" />
AscendKitUserButton
User avatar with sign-out dropdown for your app header or sidebar.
import { AscendKitUserButton } from "@ascendkit/nextjs";
<AscendKitUserButton />
Top nav:
<AscendKitUserButton layout="top-nav" />
Expanded vertical nav:
<AscendKitUserButton layout="vertical-nav" />
Collapsed vertical nav:
<AscendKitUserButton collapsed />
SignInButton / SignUpButton
Convenience buttons that open the auth modal or redirect to an auth page.
import { SignInButton, SignUpButton } from "@ascendkit/nextjs";
<SignInButton />
<SignUpButton mode="redirect" redirectUrl="/auth/sign-up" />
| Prop | Type | Default | Description |
|---|---|---|---|
mode | "modal" \</td><td className="px-3 py-2 text-muted-foreground">"redirect" | "modal" | Open modal or redirect |
redirectUrl | string | "/auth/sign-in" or "/auth/sign-up" | Path for redirect mode |
asChild | boolean | false | Delegate rendering to child element (see contract below) |
className | string | "ak-sign-in-button" | Class for the fallback <button> |
children | ReactNode | "Sign in" / "Sign up" | Button label |
Any other <button> attribute (e.g. disabled, aria-*, data-*) is forwarded to the fallback <button>. So you can style and configure the button directly without asChild:
<SignInButton className="btn btn-primary" disabled={loading}>Log in</SignInButton>
Using asChild with your own components
When asChild is true, the button delegates its onClick handler to the child element instead of wrapping in its own <button>. This avoids nested <button> elements when using UI libraries like shadcn/ui:
import { SignInButton } from "@ascendkit/nextjs";
import { Button } from "@/components/ui/button";
<SignInButton asChild>
<Button variant="ghost">Log in</Button>
</SignInButton>
Contract: the child must be a single React element that renders the underlying DOM button. If you pass plain text, multiple children, or a non-element,
asChildis ignored — the component logs a dev warning and falls back to its own<button>rather than double-wrapping (which would produce invalid nested<button>DOM). If you don't have a custom component to delegate to, dropasChildand style the built-in button viaclassNameinstead.
Using the useAuthModal hook directly
For full control (e.g. combining analytics with auth), use the hook instead:
import { useAuthModal } from "@ascendkit/nextjs";
import { Button } from "@/components/ui/button";
function NavAuth() {
const { open } = useAuthModal();
return (
<Button onClick={() => open("sign-in")}>Log in</Button>
);
}
SignedIn / SignedOut / AuthLoading
import { SignedIn, SignedOut, AuthLoading } from "@ascendkit/nextjs";
<AuthLoading><p>Loading...</p></AuthLoading>
<SignedIn><AscendKitUserButton /></SignedIn>
<SignedOut><a href="/login">Sign in</a></SignedOut>
Hooks
useAscendKit
const {
user,
isLoaded,
isSessionReady,
isAuthenticated,
isWaitlisted,
isRejected,
authStatus,
signOut,
} = useAscendKit();
| Return | Type | Description |
|---|---|---|
user | User or null | Current user |
isLoaded | boolean | Session and AscendKit profile resolution completed |
isSessionReady | boolean | Same readiness signal as isLoaded; safe to use before redirects |
authStatus | "loading" \</td><td className="px-3 py-2 text-muted-foreground">"signed_out" \</td><td className="px-3 py-2 text-muted-foreground">"authenticated" \</td><td className="px-3 py-2 text-muted-foreground">"waitlisted" \</td><td className="px-3 py-2 text-muted-foreground">"rejected" \</td><td className="px-3 py-2 text-muted-foreground">"profile_error" | Explicit auth state |
isAuthenticated | boolean | Signed in and allowed into the app |
isWaitlisted | boolean | Signed in but pending waitlist approval |
isRejected | boolean | Signed in but rejected from the waitlist |
signOut | () => Promise<void> | Sign out |
Protected routes
Use isSessionReady before redirecting. Do not redirect only because user is temporarily null; the SDK may still be resolving the session profile.
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useAscendKit } from "@ascendkit/nextjs";
export function ProtectedPage({ children }: { children: React.ReactNode }) {
const router = useRouter();
const { isSessionReady, isAuthenticated, isWaitlisted, isRejected } = useAscendKit();
useEffect(() => {
if (!isSessionReady) return;
if (isWaitlisted) router.replace("/waitlist");
else if (isRejected) router.replace("/access-denied");
else if (!isAuthenticated) router.replace("/login");
}, [isSessionReady, isAuthenticated, isWaitlisted, isRejected, router]);
if (!isSessionReady) return null;
if (!isAuthenticated) return null;
return children;
}
Waitlisted users are intentionally distinct from fully authenticated users. If waitlist is enabled, guard normal app pages with isAuthenticated; use isWaitlisted for waitlist status pages.
Host configuration precedence
Auth callbacks resolve hosts in this order:
appBaseUrlpassed tocreateAscendKitAuthRuntimeAPP_URL/ASCENDKIT_APP_URL- request origin, if allowed by
allowedHosts - forwarded host headers when
trustedProxyHeadersis enabled
Set APP_URL for stable local and production behavior. Use allowedHosts for preview deployments such as *.vercel.app.
useAuthModal
const { isOpen, view, open, close } = useAuthModal();
| Return | Type | Description |
|---|---|---|
isOpen | boolean | Modal open |
view | "sign-in" or "sign-up" | Current view |
open | (view?) => void | Open modal |
close | () => void | Close modal |
Access Tokens (frontend → backend)
If your app has a separate backend (Python, Node.js, etc.), use access tokens to authenticate API calls from the browser. AscendKit issues short-lived RS256 JWTs that any backend can verify using JWKS — no shared secrets needed.
1. Add the access token route
// app/api/access-token/route.ts
import { authRuntime } from "@/lib/auth";
import { createAccessTokenHandler } from "@ascendkit/nextjs/server";
export const GET = createAccessTokenHandler({ authRuntime });
This exchanges the user's session cookie for a short-lived access token. The route must be in the same Next.js app as your auth runtime.
2. Get tokens from React
import { useAccessToken } from "@ascendkit/nextjs";
function Dashboard() {
const { getAccessToken } = useAccessToken();
async function fetchData() {
const token = await getAccessToken();
const res = await fetch("https://api.example.com/data", {
headers: { Authorization: `Bearer ${token}` },
});
return res.json();
}
return <button onClick={fetchData}>Load data</button>;
}
getAccessToken() caches the token in memory and automatically refreshes it when it nears expiry (within 5 minutes). Concurrent calls are deduplicated — only one network request is made.
3. Verify on your backend
The access token is a standard RS256 JWT. Verify it using AscendKit's JWKS endpoint with any JWT library, or use the Python SDK:
from ascendkit import AccessTokenVerifier
verifier = AccessTokenVerifier()
claims = await verifier.verify_async(token)
print(claims["sub"]) # usr_...
print(claims["email"])
See Python SDK for FastAPI, Flask, and Django integration examples.
useAccessToken
const { getAccessToken } = useAccessToken();
const token = await getAccessToken();
| Return | Type | Description |
|---|---|---|
getAccessToken | () => Promise<string> | Returns a cached or fresh access token |
Throws if no authenticated session exists. Pass a custom path if your route isn't at /api/access-token:
const { getAccessToken } = useAccessToken("/api/custom-token-path");
Auth Lifecycle
AscendKit handles email verification and waitlist flows automatically. The SDK shows in-modal notifications for each state transition — no extra pages or code required.
Default behavior (zero-config)
Credentials (email/password):
| Event | What the user sees |
|---|---|
| Signup (verification enabled) | Success banner: "Account created! Please check your email to verify your account." |
| Signup (waitlist enabled) | Success banner: "You are on the waitlist. We will notify you when your account is approved." |
| Signup (rejected) | Error banner: "Your account application was not approved." |
| Login (email not verified) | Error banner: "Email not verified" |
| Login (waitlisted) | Error banner: "Your account is pending approval." |
| Login (rejected) | Error banner: "Your account application was not approved." |
Email verification redirect:
| Event | What the user sees |
|---|---|
| Clicks verification link | Redirected to your app with modal open + banner: "Email verified! You can now sign in." |
| Clicks verification link (waitlist enabled) | Redirected to your app with info banner: "Email verified! Your account is pending approval. We'll notify you when it's ready." Sign-in modal does not open. |
Social login (Google, GitHub, etc.):
| Event | What the user sees |
|---|---|
| Social login success | Signed in and redirected to callbackURL |
Social login (waitlisted, waitlistRedirectPath set) | Redirected to waitlistRedirectPath (e.g. /waitlist) |
| Social login (waitlisted, no redirect path) | Redirected to callbackURL with ?error=waitlist_pending |
Social login (rejected, rejectedRedirectPath set) | Redirected to rejectedRedirectPath (e.g. /rejected) |
| Social login (rejected, no redirect path) | Redirected to callbackURL with ?error=waitlist_rejected |
Magic link:
| Event | What the user sees |
|---|---|
| Magic link submit | Banner: "Check your email for the sign-in link." |
| Clicks magic link | User is signed in directly — no verification or password required. |
All credential and magic link flows work identically whether the user is signing in through the provider-managed modal (SignInButton / useAuthModal), <AscendKitAuthCard />, or the split <Login /> / <SignUp /> components. Social login redirects are handled server-side by the OAuth proxy plugin — see auth runtime options for waitlistRedirectPath and rejectedRedirectPath.
Custom override: notification handling
Read authNotification from context to render messages in your own UI:
import { useAscendKitContext } from "@ascendkit/nextjs";
function MyCustomAuthPage() {
const { authNotification, clearAuthNotification } = useAscendKitContext();
// { variant: "success" | "error" | ..., message: string } or null
if (authNotification) {
return (
<div className={authNotification.variant === "error" ? "error" : "success"}>
<p>{authNotification.message}</p>
<button onClick={clearAuthNotification}>Dismiss</button>
</div>
);
}
return <YourSignInForm />;
}
Custom override: email verification landing page
After clicking the verification link, the user is redirected with ?verified=true. If waitlist is also enabled, the URL includes ?verified=true&waitlisted=true. The SDK auto-detects these params and shows the appropriate banner.
To handle it yourself instead, create a page that reads the query params:
// app/verified/page.tsx
"use client";
import { useSearchParams } from "next/navigation";
import { useAuthModal } from "@ascendkit/nextjs";
export default function VerifiedPage() {
const params = useSearchParams();
const { open } = useAuthModal();
const verified = params.get("verified") === "true";
const waitlisted = params.get("waitlisted") === "true";
if (verified && waitlisted) {
return (
<div>
<h1>Email verified!</h1>
<p>Your account is pending approval. We'll notify you when it's ready.</p>
</div>
);
}
if (verified) {
return (
<div>
<h1>Email verified!</h1>
<p>Your email has been confirmed.</p>
<button onClick={() => open("sign-in")}>Sign in</button>
</div>
);
}
return null;
}
Custom override: waitlist gating
After login, check user.waitlistStatus to gate access:
import { useAscendKit } from "@ascendkit/nextjs";
function Dashboard() {
const { user, isLoaded } = useAscendKit();
if (!isLoaded) return <p>Loading...</p>;
if (user?.waitlistStatus === "pending") {
return (
<div>
<h1>You're on the waitlist</h1>
<p>We'll notify you when your account is approved.</p>
</div>
);
}
if (user?.waitlistStatus === "rejected") {
return <p>Your application was not approved.</p>;
}
return <YourDashboard />;
}
Signals reference
| Signal | Where | Type |
|---|---|---|
authNotification | useAscendKitContext() | { variant, message } \</td><td className="px-3 py-2 text-muted-foreground">null |
clearAuthNotification | useAscendKitContext() | () => void |
?verified=true | URL query param | Auto-detected on mount |
?waitlisted=true | URL query param | Auto-detected on mount (with ?verified=true) |
user.emailVerified | useAscendKit() | boolean |
user.waitlistStatus | useAscendKit() | "pending" \</td><td className="px-3 py-2 text-muted-foreground">"approved" \</td><td className="px-3 py-2 text-muted-foreground">"rejected" \</td><td className="px-3 py-2 text-muted-foreground">undefined |
Require Terms of Service acceptance
You can require new users to accept your Terms of Service before an account is created. When enabled, the SDK signup UI renders a required checkbox linking to your terms, and the backend rejects any signup that doesn't include valid acceptance.
This is opt-in per environment and off by default. It gates signup only — login has no ToS prompt.
Enable it
Set a Terms of Service URL on the environment's auth settings. The URL must be an absolute https:// URL, max 2048 characters. Setting a URL turns the feature on; clearing it (empty string) turns it off.
Three ways to set it:
| Surface | How |
|---|---|
| Portal | Dashboard → your project → environment → Auth → Terms of Service URL field |
| CLI | ascendkit auth update --tos-url https://your-app.com/legal/terms (pass "" to clear) — see CLI Auth |
| MCP / AI agent | auth_tos_set tool with tosUrl (use auth_tos_get to read the current value) |
| API | PATCH the environment auth settings, setting the tosUrl field to your https:// terms URL |
The URL is validated server-side. Non-https schemes, relative URLs, or values over 2048 characters are rejected. No SDK code change is required — the SDK reads tosUrl from the environment config at runtime.
v1 limitation: changing the URL does not retroactively re-prompt existing users. Acceptance is captured at signup time only.
Signup experience
When a ToS URL is configured, <SignUp /> (and <AscendKitAuthCard view="SIGN_UP" />) renders a required checkbox: "I have read and agree to the Terms of Service", with the text linking to your URL in a new tab.
- Email/password signup — submitting without checking the box shows
You must accept the Terms of Service.and blocks the request. - Social / OAuth signup — provider buttons are disabled (dimmed) until the box is checked.
On submit, the SDK sends acceptance proof (a fingerprint of the current ToS URL) alongside the signup. The backend records acceptance — timestamp, IP, and user agent — against the new user and in an immutable audit trail.
Login experience
None. Login (<Login />) never shows a ToS prompt. The feature only affects account creation.
Error states
The backend enforces acceptance independently of the UI, so direct API signups are also gated. Two errors surface through the signup form:
| Error | Meaning | UI message |
|---|---|---|
tos_acceptance_required | Signup arrived without acceptance proof | You must accept the Terms of Service to create an account. |
tos_version_mismatch | The ToS URL changed after the page loaded | The Terms of Service have been updated. Please refresh the page and try again. |
A version mismatch means the proof the browser sent no longer matches the current configured URL. Refreshing the page reloads the current terms and lets the user re-accept.
Theming & Dark Mode
AscendKit auth UI adapts to dark mode automatically with no configuration.
How it works
- Tailwind class-based (preferred): If your app sets
class="dark"on<html>, all AscendKit components switch to dark mode. - System preference fallback: If no Tailwind dark class is detected, components follow the user's OS
prefers-color-schemesetting.
Both modes work simultaneously. Tailwind class takes priority when present.
CSS custom properties
Override these variables on any ancestor element to match your brand.
Modal:
| Variable | Light default | Dark default |
|---|---|---|
--ak-modal-bg | #fff | #0a0a0a |
--ak-modal-color | #1a1a1a | #f5f5f5 |
--ak-modal-muted | #888 | #888 |
--ak-modal-hover-bg | rgba(0,0,0,0.05) | rgba(255,255,255,0.1) |
Notification banners:
| Variable | Light default | Dark default |
|---|---|---|
--ak-notif-success-bg | #f0fdf4 | rgba(34,197,94,0.1) |
--ak-notif-success-color | #166534 | #86efac |
--ak-notif-success-border | #bbf7d0 | rgba(34,197,94,0.2) |
--ak-notif-error-bg | #fef2f2 | rgba(239,68,68,0.1) |
--ak-notif-error-color | #991b1b | #fca5a5 |
--ak-notif-error-border | #fecaca | rgba(239,68,68,0.2) |
Example: custom brand colors
:root {
--ak-modal-bg: #fafaf9;
--ak-notif-success-bg: #ecfdf5;
--ak-notif-success-color: #065f46;
}
html.dark {
--ak-modal-bg: #111111;
--ak-notif-success-bg: rgba(16, 185, 129, 0.1);
--ak-notif-success-color: #6ee7b7;
}
Auth form styling
The auth forms (sign-in, sign-up, forgot password) are rendered by Better Auth UI. They inherit your app's Tailwind theme and can be styled via its theming API. AscendKit components use ak- prefixed CSS classes so they never conflict with your existing styles.
Analytics
Client-side (React)
Track user events from the browser. Browser context (userAgent, locale, screen size, referrer, URL) is captured automatically. Requires the user to be authenticated.
import { useAnalytics } from "@ascendkit/nextjs";
function CheckoutButton() {
const { track, flush } = useAnalytics();
return (
<button
onClick={async () => {
track("checkout.completed", { total: 99.99 });
await flush();
}}
>
Buy
</button>
);
}
Events batch in memory and flush every 30 seconds, when the batch fills (10 events), or on page hide.
flush() returns a promise, so await flush() when you need the queued events sent before navigation or other follow-up work.
Server-side (Node.js)
Track events from your backend with trusted identity (secret key auth).
import { Analytics } from "@ascendkit/nextjs/server";
const analytics = new Analytics();
analytics.track("usr_456", "checkout.completed", { total: 99.99 });
The constructor reads ASCENDKIT_SECRET_KEY and ASCENDKIT_ENV_KEY from your environment. If either is missing, it throws at initialization.
Events batch in memory and flush every 30 seconds, when the batch fills (10 events), or on shutdown(). Call analytics.shutdown() for graceful cleanup (e.g. in process.on("beforeExit")).
See Analytics for the full guide including event naming, Python server-side tracking, and how analytics powers journey automations.
Other React Frameworks
The React hooks and components from @ascendkit/nextjs work in any React 18+ framework (Vite, Remix, Astro, etc.):
<AscendKitProvider>,useAscendKit(),useAuthModal(),useAnalytics()— all work<Login>,<SignUp>,<AscendKitAuthCard>,<SocialButton>— all work<SignedIn>,<SignedOut>,<AuthLoading>— all work
What does not work outside Next.js:
createAscendKitAuthRuntime()andcreateAuthRouteHandlers()— these require Next.js for server-side session management and OAuth callbacks- Server-side analytics (
Analyticsclass) — requires a Node.js server, which you can use with any framework that has a server
For non-Next.js React apps, you need to proxy auth requests to a separate backend that handles the AscendKit auth runtime, or use AscendKit's hosted auth endpoints directly.