mytpen-auth
v1.1.15
Published
Better Auth integration package for MYTPEN authentication
Readme
MYTPEN Auth Package
A Better Auth integration package for MYTPEN authentication with setup.
🚀 Setup
Get authentication working in your Next.js app with these 7 steps:
Step 1: Install the Package
pnpm install mytpen-authStep 2: Create OIDC App and Get Environment Variables
- Go to https://dashboard.mytpen.app/admin/apps
- Sign in to MYTPEN account as
Admin - Create a new OIDC app
- Copy the environment values
Step 3: Set Up Redis [Use same Redis instance that the dashboard uses]
Use the same Upstash (or Redis) instance as the MYTPEN dashboard when possible so Better Auth secondary storage matches production.
The bundled auth uses @upstash/redis with Redis.fromEnv(). The usual variables are:
# From the Upstash console (REST API) — preferred for this package
UPSTASH_REDIS_REST_URL="https://your-instance.upstash.io"
UPSTASH_REDIS_REST_TOKEN=your-rest-tokenAlternative (e.g. Vercel / other hostings): some platforms only inject different names. If UPSTASH_* are not set, compatible values often work under names such as:
# Alternative naming — only if your host does not provide UPSTASH_REDIS_*
# KV_REST_API_URL=https://your-endpoint.upstash.io
# KV_REST_API_TOKEN=your-rest-api-token
# KV_URL=redis://default:password@host:port
# REDIS_URL=redis://default:password@host:portSee Upstash Redis on Vercel Marketplace or the Upstash console for creating a database and copying the REST URL and token.
Step 4: Create .env File
Create a .env file in your project root and paste the values:
MYTPEN_AUTH_SECRET=wIXyY1-qegt7qN4VOAThlwB7qhOFM7uc # use any random string as secret-key
# get these from mytpen dashboard
MYTPEN_PROVIDER_ID=your-app-id
MYTPEN_CLIENT_ID=bJgGbbqKVgqjQXZgOPyWheoMDePFGRUN
MYTPEN_CLIENT_SECRET=MMFZfWLhditpjvQVUAZpRPGLGGBpNQNv
# IdP origin OR full Better Auth base (both work — avoids doubled /api/auth paths).
# Used for server-side revoke URL resolution, and for MytpenAuthProvider billingURL / postSignOutRedirect when you set those from env.
MYTPEN_AUTH_SERVER=https://dashboard.mytpen.app
# Comma-separated origins for Better Auth (e.g. https://app.example.com,http://localhost:3000)
# MYTPEN_TRUSTED_ORIGINS=http://localhost:3000
# Base URL of this app’s auth API (Better Auth callbacks). Prefer BETTER_AUTH_URL; MYTPEN_AUTH_BASEURL is accepted as fallback.
# BETTER_AUTH_URL=http://localhost:3000
MYTPEN_AUTH_BASEURL=http://localhost:3000
# Client: must match MYTPEN_PROVIDER_ID for authClient.getAccessToken / refreshToken / OAuth calls in the browser.
# NEXT_PUBLIC_MYTPEN_PROVIDER_ID=your-app-id
# OpenAPI Scalar reference page (optional): set IS_DEBUG=true to enable default reference UI
# IS_DEBUG=true
# Redis — @upstash/redis via Redis.fromEnv() (secondary storage for Better Auth)
UPSTASH_REDIS_REST_URL="https://your-instance.upstash.io"
UPSTASH_REDIS_REST_TOKEN=
# Alternative: if your platform only exposes Vercel-style names, map or set e.g.:
# KV_REST_API_URL / KV_REST_API_TOKEN (see Step 3)For the repository demo (demo/), the dev server uses port 3001 — set MYTPEN_AUTH_BASEURL=http://localhost:3001 (and the same for BETTER_AUTH_URL if you use it) so OAuth redirects match the app origin.
Step 5: Create API Route Handler
Create app/api/auth/[...all]/route.ts:
// app/api/auth/[...all]/route.ts
import { auth } from "mytpen-auth";
import { toNextJsHandler } from 'mytpen-auth';
export const { POST, GET } = toNextJsHandler(auth);Step 6: Add Auth Provider to Your App (server component)
Update app/layout.tsx - must be server component:
// app/layout.tsx
import { MytpenAuthProvider } from "mytpen-auth/provider";
import { LogoSvg } from "mytpen-auth/components";
import { getCurrentPathname } from "mytpen-auth/nextjs";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// Pathname for callbacks: `getCurrentPathname()` reads the `x-matched-path` header when
// `mytpenMiddleware` from `mytpen-auth/nextjs` is installed; without middleware it falls back to "/".
const pathname = await getCurrentPathname();
// publicRoutes: only these paths skip the OAuth redirect when not signed in.
// Use publicRoutes={["/"]} for a minimal app where home is public.
// The repo demo lists paths like /account, /test, /exam/:id and leaves "/" protected.
return (
<html lang="en">
<body>
<MytpenAuthProvider
providerId={process.env.MYTPEN_PROVIDER_ID!}
callbackURL={pathname}
newUserUrl={pathname}
publicRoutes={["/", "/about"]}
checkSubscription={true}
billingURL={process.env.MYTPEN_AUTH_SERVER!}
postSignOutRedirect={process.env.MYTPEN_AUTH_SERVER!}
logo={<LogoSvg width={64} height={64} />}
>
{children}
</MytpenAuthProvider>
</body>
</html>
);
}Adjust publicRoutes to your app: any route not listed may trigger the OAuth flow when autoRedirect is true (default). The demo app uses a specific list (e.g. /account, /test, /exam/:id) and does not put / in publicRoutes, so the home page requires sign-in.
Step 7: Use Authentication in Components
// app/page.tsx or any component
"use client";
import { useMytpenAuth, useSession } from "mytpen-auth/provider"; // ⚡ Cached, no repeated API calls
export default function Home() {
const { session, isPending } = useSession(); // ⚡ Cached session, no network calls!
const { signOut } = useMytpenAuth();
if (isPending) return <div>Loading...</div>;
if (session) {
return (
<div>
<h1>Welcome, {session.user.name}!</h1>
<button onClick={() => signOut()}>Sign Out</button>
</div>
);
}
// On routes in `publicRoutes` only: unauthenticated users see this instead of being redirected to OAuth.
return <div>Public content — no login required on this route.</div>;
}That's it! 🎉 Your app now has authentication with:
- ✅ Auto-configured - reads from environment variables
- ✅ Public routes support - some pages don't require authentication
- ✅ Optimized session management - no repeated network calls
- ✅ Beautiful loading screens with MYTPEN logo
- ✅ Default scopes - includes
subscription:readfor MYTPEN subscription claims on the user - ✅ Subscription checking - automatic subscription validation (optional)
- ✅ Redis secondary storage — the bundled
authuses Upstash Redis (Redis.fromEnv()); set the env vars from Step 3 - ✅ TypeScript support
🚀 Redis Benefits
When you set up Redis (Step 3), Better Auth uses it as secondary storage (sessions and related keys):
- ⚡ Performance: Shared, fast storage for session-related data
- 📈 Scalability: Storage that scales with your Redis deployment
- 🔄 Durability: Data survives individual Node process restarts (subject to Redis TTL/persistence settings)
How it works: src/auth.ts wires secondaryStorage to Upstash Redis. Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (or compatible vars your host maps to them) so Redis.fromEnv() can connect.
Get access token (Dashboard / resource APIs)
Client (browser): use authClient.getAccessToken from mytpen-auth/client with your app’s OAuth providerId. The providerId must match MYTPEN_PROVIDER_ID and MytpenAuthProvider’s providerId (in client components expose it as NEXT_PUBLIC_MYTPEN_PROVIDER_ID or pass the same string as in your .env). Mismatched ids break token refresh and OAuth account lookup. Expired tokens are refreshed when possible.
"use client";
import { authClient } from "mytpen-auth/client";
async function getToken() {
const { data } = await authClient.getAccessToken({
providerId: process.env.NEXT_PUBLIC_MYTPEN_PROVIDER_ID!, // or your known app id string
});
return data?.accessToken;
}// app/api/example/route.ts — use in a Route Handler or server action
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import { auth } from "mytpen-auth";
export async function GET() {
try {
const providerId = process.env.MYTPEN_PROVIDER_ID;
if (!providerId) {
return NextResponse.json(
{ error: "MYTPEN_PROVIDER_ID environment variable not set" },
{ status: 500 },
);
}
const h = await headers();
const session = await auth.api.getSession({ headers: h });
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const accessToken = await auth.api.getAccessToken({
body: { providerId },
headers: h,
});
return NextResponse.json({ accessToken });
} catch (error) {
console.error("AccessToken error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}🔑 One-Time Tokens
Generate and verify secure temporary tokens for QR codes, email verification, etc.
Configuration: The published auth uses oneTimeToken({ expiresIn: 10, storeToken: "hashed" }). To change it, copy src/auth.ts from this package into your app and adjust the plugin options (Advanced Usage).
Client-Side Usage
import { authClient } from "mytpen-auth/client";
// Generate token
const { data } = await authClient.oneTimeToken.generate();
// Verify token
const { data } = await authClient.oneTimeToken.verify({
token: "your-token-here"
});
// POST /api/auth/one-time-token/verify — body: { token: "..." }Server-Side Usage
import { auth } from "mytpen-auth";
import { headers } from "next/headers";
// Generate token
const data = await auth.api.generateOneTimeToken({
// This endpoint requires session cookies.
headers: await headers(),
});
// Verify token
const data = await auth.api.verifyOneTimeToken({
body: {
token: "some-token", // required
},
});💳 Subscription (user fields from OIDC)
Subscription state is not loaded from a separate billing HTTP API. The MYTPEN IdP maps claims into user columns via mapProfileToUser in the server auth config (subscriptionId, hasActiveSubscription, subscriptionEndAt, trialEndAt). They appear on session.user in API and client session payloads.
How it works
- OIDC + scope: Request
subscription:read(included in the package defaults) so the provider can return subscription-related claims. - User record: Values are stored as user
additionalFieldsand returned withgetSession/useSession. - Access gate:
MytpenAuthProviderwithcheckSubscription={true}allows the user ifuserHasSubscriptionAccess(user)is true: paid flag (hasActiveSubscriptionastrue/"true"), ortrialEndAtis a valid date in the future, orsubscriptionEndAtis a valid date in the future.
Client usage
import { useSession } from "mytpen-auth/provider";
import { userHasSubscriptionAccess, userHasActiveSubscription } from "mytpen-auth";
const { session } = useSession();
if (session && userHasSubscriptionAccess(session.user)) {
// Paid, active trial, or subscription period not ended
}
// Flag-only check (ignores trial / subscription end dates):
if (session && userHasActiveSubscription(session.user.hasActiveSubscription)) {
// ...
}Server usage
import { auth, userHasSubscriptionAccess } from "mytpen-auth";
import { headers } from "next/headers";
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!userHasSubscriptionAccess(session.user)) {
return NextResponse.json({ error: "No activated plan" }, { status: 402 });
}Optional: automatic redirect to billing
<MytpenAuthProvider
providerId={process.env.MYTPEN_PROVIDER_ID!}
checkSubscription={true}
billingURL={process.env.MYTPEN_AUTH_SERVER ?? "https://dashboard.mytpen.app"}
>
{children}
</MytpenAuthProvider>- Redirects when
userHasSubscriptionAccess(user)is false (paid flag and both end dates considered). - Sends users to
{billingURL}/billing?app-id={providerId}and signs them out first.
Subscription-related user fields
| Field | Description |
|--------|-------------|
| subscriptionId | Provider subscription id (if any) |
| hasActiveSubscription | Boolean or string from IdP; userHasActiveSubscription() normalizes the flag only |
| subscriptionEndAt | ISO string or null; if in the future, userHasSubscriptionAccess() is true |
| trialEndAt | ISO string or null; if in the future, userHasSubscriptionAccess() is true |
Use userHasSubscriptionAccess(session.user) for the same rule as MytpenAuthProvider’s checkSubscription gate. For UI only, subscriptionEndDateInFuture(date) matches the trial/subscription end checks inside that helper.
🛡️ Optional: Add Middleware (Advanced)
If you need server-side route protection, add middleware:
Option 1: Simple Usage (Clerk-style)
// middleware.ts
import { mytpenMiddleware } from 'mytpen-auth/nextjs'
// Uses default configuration:
// - publicRoutes: ["/"]
// - protectedRoutes: ["/dashboard/*", "/profile/*", "/settings/*"]
// - loginUrl: "https://dashboard.mytpen.app/sign-in"
// - fallbackUrl: "/dashboard"
export default mytpenMiddleware()
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
]
}Option 2: Custom Configuration
// middleware.ts
import { mytpenMiddleware } from 'mytpen-auth/nextjs'
export default mytpenMiddleware({
publicRoutes: ["/", "/about", "/contact", "/exam/:id", "/:slug/public"],
protectedRoutes: ["/dashboard/*", "/profile/*", "/settings/:id"],
// loginUrl defaults to "https://dashboard.mytpen.app/sign-in"
fallbackUrl: "/dashboard"
})
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
]
}Option 3: Compose with Other Middleware
// middleware.ts
import { NextRequest } from 'next/server'
import { mytpenMiddleware } from 'mytpen-auth/nextjs'
import { NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
const authResponse = await mytpenMiddleware()(request);
if (authResponse) return authResponse;
// Your other middleware logic here
return NextResponse.next();
}
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
]
}Route Pattern Matching
The middleware supports three types of route patterns:
1. Exact Match
publicRoutes: ["/", "/about", "/contact"]
// Matches exactly: /about
// Does NOT match: /about/team2. Wildcard Pattern (*)
publicRoutes: ["/blog/*", "/docs/*"]
// Matches: /blog/post-1, /blog/post-2/comments
// Matches: /docs/intro, /docs/api/reference3. Dynamic Parameters (:param)
publicRoutes: [
"/exam/:id", // Matches: /exam/123, /exam/abc
"/post/:slug/view", // Matches: /post/hello/view, /post/world/view
"/:category/items", // Matches: /electronics/items, /books/items
]Example combining all patterns:
export default mytpenMiddleware({
publicRoutes: [
"/", // Exact: home page
"/about", // Exact: about page
"/blog/*", // Wildcard: all blog routes
"/exam/:id", // Dynamic: specific exam by ID
"/user/:userId/profile" // Dynamic: user profile by ID
],
protectedRoutes: [
"/dashboard/*", // Wildcard: all dashboard routes
"/settings/:section" // Dynamic: settings sections
]
})Dynamic pathname access
mytpenMiddleware sets the incoming request header x-matched-path to the current pathname. Use getCurrentPathname() from mytpen-auth/nextjs in Server Components, or read x-matched-path yourself. If middleware is not installed for a route, the header may be missing and getCurrentPathname() falls back to "/".
// app/layout.tsx
import { getCurrentPathname } from 'mytpen-auth/nextjs'
export default async function RootLayout({ children }) {
const pathname = await getCurrentPathname();
return (
<MytpenAuthProvider
providerId={process.env.MYTPEN_PROVIDER_ID!}
callbackURL={pathname} // Redirect to current page after auth
newUserUrl={pathname} // Keep new users on current page
publicRoutes={["/exam/:id", "/"]}
>
{children}
</MytpenAuthProvider>
);
}Benefits:
- ✅ Dynamic callbacks based on current route
- ✅ Better UX: Users stay on the same page after authentication
- ✅ Works with dynamic routes like
/exam/:id - ✅
getCurrentPathname()reads thex-matched-pathheader set bymytpenMiddleware
Alternative: read the header yourself
import { headers } from "next/headers";
export default async function Layout({ children }) {
const headersList = await headers();
const pathname = headersList.get("x-matched-path") || "/";
// ...
}Middleware Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| publicRoutes | string[] | ["/"] | Routes accessible without authentication. Supports exact matches, wildcards (/blog/*), and dynamic params (/exam/:id) |
| protectedRoutes | string[] | ["/dashboard/*", "/profile/*", "/settings/*"] | Routes that require authentication |
| loginUrl | string | "https://dashboard.mytpen.app/sign-in" | Where to redirect unauthenticated users |
| fallbackUrl | string | "/dashboard" | Where to redirect authenticated users who try to access the login page |
Example: Custom Login URL
If you have your own login page:
export default mytpenMiddleware({
publicRoutes: ["/", "/sign-in", "/sign-up"],
loginUrl: "/sign-in", // Your custom login page
fallbackUrl: "/dashboard"
})Example: Use MYTPEN Dashboard Login (Default)
export default mytpenMiddleware({
publicRoutes: ["/", "/about"],
// loginUrl will default to "https://dashboard.mytpen.app/sign-in"
fallbackUrl: "/dashboard"
})📦 Package exports
These match the exports field in package.json: mytpen-auth, mytpen-auth/client, mytpen-auth/server, mytpen-auth/edge, mytpen-auth/components, mytpen-auth/provider, mytpen-auth/nextjs.
| Subpath | Main exports |
|--------|----------------|
| mytpen-auth | auth; subscription helpers userHasActiveSubscription, userHasSubscriptionAccess, subscriptionEndDateInFuture; types MytpenClientAuthConfig, IMytpenSession; toNextJsHandler, getSessionCookie, getCookieCache; IdP URL helpers resolveMytpenIdpAuthApiBase, resolveMytpenIdpOAuthRevokeUrl, resolveMytpenIdpOrigin, resolveMytpenIdpUserInfoUrl |
| mytpen-auth/server | Same preconfigured auth as the root entry (alias for apps that prefer a /server import). |
| mytpen-auth/client | authClient, createMytpenAuthClient |
| mytpen-auth/edge | getSessionCookie, getCookieCache (Edge-safe; no Node-only APIs) |
| mytpen-auth/components | LogoSvg, LoaderSvg, LoadingScreen, AuthLoadingScreen |
| mytpen-auth/provider | MytpenAuthProvider, useMytpenAuth, useSession, ProtectedRoute |
| mytpen-auth/nextjs | mytpenMiddleware, getCurrentPathname; type MytpenMiddlewareConfig |
Better Auth server plugins used inside auth (OAuth revoke on sign-out, custom endpoints, etc.) live under src/plugins and are not re-exported—use import { auth } from "mytpen-auth" or fork src/auth.ts if you need a custom betterAuth setup.
For extra providers or a fully custom server config, define your own betterAuth({ ... }) (see better-auth/minimal) and copy/adapt options from this repo’s src/auth.ts.
Session hooks
useSessionfrommytpen-auth/provider— reads session from the provider context (same source asuseMytpenAuth); use this in app UI underMytpenAuthProvider.authClient.useSession()frommytpen-auth/client— Better Auth’s hook on the raw client; use when you are not inside the provider or need the client’s default behavior.
useSession return value:
const {
session, // User session object or null
isPending, // true when loading session
error // Error object if authentication failed
} = useSession();Advanced Hook - useMytpenAuth:
For advanced use cases where you need access to the auth client or signOut function:
import { useMytpenAuth } from "mytpen-auth/provider";
function MyComponent() {
const {
session, // User session
isPending, // Loading state
error, // Error state
authClient, // Raw Better Auth client
signOut // Sign out function with custom redirect
} = useMytpenAuth();
const handleSignOut = async () => {
await signOut("https://custom-redirect.com");
};
// Default redirect matches billingURL (e.g. MYTPEN_AUTH_SERVER) unless you set postSignOutRedirect.
const handleSignOutWithDefaults = async () => {
await signOut();
};
return <button onClick={handleSignOut}>Sign Out</button>;
}OAuth2 revoke on sign-out: Keep client_secret on the server only. The bundled auth runs a before hook on POST /sign-out: it reads the encrypted refresh token from the account row, sends a best-effort RFC 7009 revoke to {MYTPEN_AUTH_SERVER}/api/auth/oauth2/revoke (default host: https://dashboard.mytpen.app), then clears OAuth fields locally. The browser only calls signOut() / authClient.signOut(); refresh tokens are not exposed to the client.
Optional: set MYTPEN_DEBUG_IDP_REVOKE=true on the server to log revoke URL / skip reasons.
🎯 Key Features
- 🚀 Ultra-Minimal Setup: Seven steps to get authentication working
- 🤖 Auto-Configuration: Reads from environment variables - no manual config needed
- 🎨 Beautiful UI: Built-in loading screens with MYTPEN branding (fully customizable)
- ⚡ Performance: Cached session management prevents repeated API calls
- 🛡️ Flexible Protection: Support for public routes and optional middleware
- 🔧 Clerk-style API: Familiar patterns if you've used Clerk
- 📱 TypeScript: Full type safety out of the box
- 🌐 OIDC Standard: Works with MYTPEN's OpenID Connect implementation
- 🎛️ Smart Defaults: Default scopes, auto-detected base URLs, and more
- 🎭 Customizable: Custom loading screens, messages, and callbacks
🎨 Customization
Custom Loading Screen
You can customize the loading screen that appears during authentication:
// app/layout.tsx
import { MytpenAuthProvider } from "mytpen-auth/provider";
import { LogoSvg } from "mytpen-auth/components";
// Option 1: Custom messages
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<MytpenAuthProvider
providerId={process.env.MYTPEN_PROVIDER_ID!}
callbackURL="/dashboard"
publicRoutes={["/"]}
logo={<LogoSvg width={64} height={64} />}
pendingMessage="Checking authentication..."
redirectingLoadingMessage="Checking your sign-in status..."
>
{children}
</MytpenAuthProvider>
</body>
</html>
);
}
// Option 2: Completely custom loading component
function CustomLoader() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1>Custom Loading Screen</h1>
<p>Please wait...</p>
</div>
</div>
);
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<MytpenAuthProvider
providerId={process.env.MYTPEN_PROVIDER_ID!}
callbackURL="/dashboard"
publicRoutes={["/"]}
loadingComponent={<CustomLoader />}
>
{children}
</MytpenAuthProvider>
</body>
</html>
);
}Protected Route Component
For fine-grained route protection, you can use the ProtectedRoute component:
// app/dashboard/page.tsx
"use client";
import { ProtectedRoute } from "mytpen-auth/provider";
export default function DashboardPage() {
return (
<ProtectedRoute
redirectTo="/login"
fallback={<div>Loading...</div>}
>
<div>
<h1>Protected Dashboard</h1>
<p>Only authenticated users can see this</p>
</div>
</ProtectedRoute>
);
}ProtectedRoute Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| redirectTo | string | "/auto-login" | Where to redirect if not authenticated |
| fallback | React.ReactNode | <AuthLoadingScreen /> | Component to show while checking auth |
| children | React.ReactNode | Required | Content to show when authenticated |
Provider Configuration Options
The MytpenAuthProvider accepts the following props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| providerId | string | Required | Your MYTPEN OAuth provider ID |
| callbackURL | string | "/dashboard" | Where to redirect after successful login |
| newUserUrl | string | "/dashboard" | Where to redirect new users |
| errorUrl | string | "/error" | Where to redirect on authentication error |
| scopes | string[] | ["openid", "profile", "email", "offline_access", "subscription:read"] | OAuth scopes to request |
| publicRoutes | string[] | ["/"] | Routes that don't require authentication |
| autoRedirect | boolean | true | Automatically redirect to login when not authenticated |
| logo | React.ReactNode | <LogoSvg /> | Logo to display on loading screen |
| loadingComponent | React.ReactNode | undefined | Custom loading component (overrides all loading UI) |
| pendingMessage | string | "Checking your sign-in status..." | Message shown while checking session |
| redirectingLoadingMessage | string | "Checking your sign-in status..." | Message shown while redirecting / loading |
| checkMytpenLoggedIn | boolean | true | On protected routes, one IdP GET /mytpen/is-logged-in check per load; signs out if the IdP flag is false |
| className | string | "" | Additional CSS classes for loading screen |
| checkSubscription | boolean | true | Redirect to billing when userHasSubscriptionAccess(user) is false |
| billingURL | string | "https://dashboard.mytpen.app" | Base URL for billing redirect (/billing?app-id=...) |
| postSignOutRedirect | string | billingURL | Redirect after signOut() when no custom URL is passed |
| onError | (error: Error) => void | undefined | Callback when authentication error occurs |
| onSuccess | (session: any) => void | undefined | Callback when authentication succeeds |
🔧 Advanced Usage
Custom client baseURL
// lib/auth-client.ts
import { createMytpenAuthClient } from 'mytpen-auth/client';
export const { authClient } = createMytpenAuthClient({
baseURL: process.env.MYTPEN_AUTH_BASEURL!,
});Use authClient.signIn.oauth2, authClient.signOut, authClient.useSession, etc. from Better Auth's client API.
Custom server auth (plugins, extra providers)
There is no createMytpenAuth factory. Either use import { auth } from "mytpen-auth" as-is, or maintain lib/auth.ts with betterAuth from better-auth/minimal by copying src/auth.ts from this package and editing plugins, trustedOrigins, or genericOAuth({ config: [...] }). See the Better Auth plugins directory for optional add-ons.
Related docs: OAuth 2.1 Provider (OIDC-compatible) · Generic OAuth
Server-side session access
import { auth } from "mytpen-auth";
import { headers } from "next/headers";
export async function GET() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return Response.json({ userId: session.user.id });
}If you use a custom lib/auth.ts, import auth from there instead.
Edge Runtime Compatibility
// For middleware or edge functions
import { getSessionCookie } from 'mytpen-auth/edge';
const session = getSessionCookie(request);📚 Demo app (demo/)
The demo Next.js app exercises the same APIs you use in production. Run it from the repo root with pnpm dev:demo (or cd demo && pnpm dev). The demo dev server listens on port 3001 by default — set MYTPEN_AUTH_BASEURL / BETTER_AUTH_URL to http://localhost:3001 for OAuth callbacks.
Environment: copy demo/.example.env to demo/.env and fill OAuth values from the MYTPEN dashboard. Redis uses UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN like the main setup steps.
Layout (app/layout.tsx): getCurrentPathname() from mytpen-auth/nextjs, MytpenAuthProvider with providerId, dynamic callbackURL / newUserUrl, publicRoutes (currently ["/account", "/test", "/exam/:id", "/home"] — / is not public), billingURL and postSignOutRedirect from MYTPEN_AUTH_SERVER, checkSubscription, and default checkMytpenLoggedIn (true unless you override it).
Client pages: useMytpenAuth, useSession, signOut, and authClient from mytpen-auth/client — e.g. getAccessToken / refreshToken with NEXT_PUBLIC_MYTPEN_PROVIDER_ID (same as MYTPEN_PROVIDER_ID), mytpen.isLoggedIn(), mytpen.oauth2.userinfo (Bearer access token), accountInfo, and oneTimeToken.generate / verify on the QR page.
Route handlers: GET /api/user uses auth.api.getSession plus userHasSubscriptionAccess, userHasActiveSubscription, subscriptionEndDateInFuture. GET /api/user-info-auth-server uses auth.api.getAccessToken and auth.api.userinfo.
The demo does not ship a middleware.ts; without middleware, getCurrentPathname() falls back to "/". Add mytpenMiddleware from mytpen-auth/nextjs if you need pathname headers and route protection.
🆘 Support
📄 License
MIT License - see LICENSE file for details.
