
Research
/Security News
Miasma Mini Shai-Hulud Hits ImmobiliareLabs npm Packages
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.
mytpen-auth
Advanced tools
A Better Auth integration package for MYTPEN authentication with setup.
Get authentication working in your Next.js app with these 7 steps:
pnpm install mytpen-auth
Admin[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-token
Alternative (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:port
See Upstash Redis on Vercel Marketplace or the Upstash console for creating a database and copying the REST URL and token.
.env FileCreate 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.
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);
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.
// 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:
subscription:read for MYTPEN subscription claims on the userauth uses Upstash Redis (Redis.fromEnv()); set the env vars from Step 3When you set up Redis (Step 3), Better Auth uses it as secondary storage (sessions and related keys):
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.
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 },
);
}
}
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).
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: "..." }
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 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.
subscription:read (included in the package defaults) so the provider can return subscription-related claims.additionalFields and returned with getSession / useSession.MytpenAuthProvider with checkSubscription={true} allows the user if userHasSubscriptionAccess(user) is true: paid flag (hasActiveSubscription as true / "true"), or trialEndAt is a valid date in the future, or subscriptionEndAt is a valid date in the future.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)) {
// ...
}
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 });
}
<MytpenAuthProvider
providerId={process.env.MYTPEN_PROVIDER_ID!}
checkSubscription={true}
billingURL={process.env.MYTPEN_AUTH_SERVER ?? "https://dashboard.mytpen.app"}
>
{children}
</MytpenAuthProvider>
userHasSubscriptionAccess(user) is false (paid flag and both end dates considered).{billingURL}/billing?app-id={providerId} and signs them out first.| 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.
If you need server-side route protection, add middleware:
// 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)(.*)',
]
}
// 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)(.*)',
]
}
// 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)(.*)',
]
}
The middleware supports three types of route patterns:
publicRoutes: ["/", "/about", "/contact"]
// Matches exactly: /about
// Does NOT match: /about/team
*)publicRoutes: ["/blog/*", "/docs/*"]
// Matches: /blog/post-1, /blog/post-2/comments
// Matches: /docs/intro, /docs/api/reference
: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
]
})
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:
/exam/:idgetCurrentPathname() reads the x-matched-path header set by mytpenMiddlewareAlternative: 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") || "/";
// ...
}
| 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"
})
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.
useSession from mytpen-auth/provider — reads session from the provider context (same source as useMytpenAuth); use this in app UI under MytpenAuthProvider.authClient.useSession() from mytpen-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.
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>
);
}
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 |
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 |
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.
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
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.
// For middleware or edge functions
import { getSessionCookie } from 'mytpen-auth/edge';
const session = getSessionCookie(request);
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.
MIT License - see LICENSE file for details.
FAQs
Better Auth integration package for MYTPEN authentication
We found that mytpen-auth demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.

Security News
Rolldown paused Rust React Compiler integration after a 5MB binary size increase raised concerns about shipping React-specific code to all Vite users.

Security News
/Research
Mini Shai-Hulud expands into the Go ecosystem after hitting LeoPlatform npm packages and targeting GitHub Actions workflows.