npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@baseauth/nextjs

v0.1.2

Published

Next.js SDK for baseauth — offline token verification, single-flight refresh, org-scoped sessions. Plain Request/Response handlers; imports neither react nor next.

Readme

@baseauth/nextjs

Next.js SDK for baseauth.

Access tokens are verified offline against the published JWKS — baseauth is never in the request path of a normal API call, only at login, refresh, and logout.

Install

npm install @baseauth/nextjs

Configure

# .env.local
BASEAUTH_ISSUER=https://auth.example.com
BASEAUTH_CLIENT_ID=app_7c31…
BASEAUTH_CLIENT_SECRET=…          # confidential client — the server exchanges the code
BASEAUTH_REDIRECT_URI=https://yourapp.com/api/auth/callback
BASEAUTH_COOKIE_SECRET=…          # encrypts the refresh-token cookie at rest

# Optional
BASEAUTH_ORGANIZATION=org_…       # required if your issuer is multi-tenant
BASEAUTH_POST_LOGOUT_REDIRECT_URI=https://yourapp.com/

None of these is NEXT_PUBLIC_*, and none should be. They are read on the server; putting the issuer or the client secret in the browser bundle is how a wrong value gets compiled in and shipped.

Send someone to sign up rather than sign in

/api/auth/login opens the sign-in form. Pass intent=signup for the create-account form instead, and return_to for a same-origin path to land on afterwards:

/api/auth/login?intent=signup&return_to=%2Fwelcome

return_to is rejected unless it is a relative path — an absolute one would make the login route an open redirect, which is a credible phishing primitive because the domain in the address bar is genuinely yours right up until the bounce.

Wire it up

// app/api/auth/[...baseauth]/route.ts
import { createHandlers, configFromEnv } from '@baseauth/nextjs'

const handlers = createHandlers(configFromEnv(process.env))
export const GET = handlers.GET
export const POST = handlers.POST

That serves /login, /callback and /logout. Refresh is not a route — it happens inside getSession when the access token is close to expiry.

Constructing at module scope like this is safe: configuration is validated on the first request, not here. next build evaluates route modules while collecting page data, so a constructor that threw would fail the build rather than the request — and your image could not be built without production secrets.

Read a session

import { createServerClient, configFromEnv } from '@baseauth/nextjs/server'

// Also safe at module scope, for the same reason.
const auth = createServerClient(configFromEnv(process.env))

// null when signed out
const session = await auth.getSession(request)

// throws rather than returning null
const session = await auth.requireSession(request, { orgId: tenant.baseauthOrgId })

The three rules that matter

Always pass orgId. A valid signature is not an authorization decision. An access token minted for tenant A is perfectly well-signed when presented to tenant B's route, and requireSession({ orgId }) is what makes that a rejection instead of a cross-tenant leak:

await auth.requireSession(request, { orgId: tenant.baseauthOrgId })
// throws OrgMismatchError

Write back the cookies a refresh produced. getSession may rotate the session transparently; the new cookie only reaches the browser if you attach it:

const response = NextResponse.next()
for (const cookie of auth.pendingCookies()) {
  response.headers.append('set-cookie', cookie)
}

Revocation is not instant. Offline verification means baseauth never sees your request, so killing a session takes effect within one access-token lifetime — up to ten minutes. Do not build a flow that assumes a killed session stops working on the very next request.

What it handles for you

  • Single-flight refresh. A page rendering five components that each need a token would otherwise fire five refreshes; because refresh tokens rotate, four of them present an already-spent token and look exactly like theft to the server, which then revokes the family. The SDK collapses them into one.
  • Offline JWKS verification, with a re-fetch on an unknown kid and the accepted algorithms pinned by configuration rather than read from the token header.
  • PKCE and state, bound to the browser that started the flow.
  • Cookie encryption for the refresh token, which never reaches JavaScript.

Two pages your app has to serve

baseauth hosts sign-in, sign-up and "forgot your password". It does not host the two pages its emails link into, because those links have to land on the domain your users already recognise — asking someone to type a new password on an auth domain they have never seen is the shape of a phishing mail.

| Path | Reached from | Posts to | | --- | --- | --- | | /reset-password?token=… | the password-reset email | POST {issuer}/api/v1/account/password/reset with { token, password } | | /verify-email?token=… | the verification email | POST {issuer}/api/v1/account/email/verify with { token } |

Both endpoints are unauthenticated and CORS-enabled for your origin, so a browser can call them directly. Proxying through your own server instead keeps the issuer's URL out of the bundle, which is what the rest of this SDK is for.

Two behaviours worth building for on the reset page. weak_password comes back with a message naming the policy and does not consume the token, so keep the form on screen; every other error means the link is spent or expired and the person needs a new one. And a completed reset revokes every session on the account including the current browser, so end on "sign in again" rather than assuming they are still signed in.

Without these pages the emails are dead ends, and a forgotten password becomes the end of the account.

Hosting the sign-in page yourself

baseauth's hosted pages are the default and stay right for most apps. If your product's visual identity will not tolerate a sign-in screen on somebody else's domain, an organization can name a page of its own:

PATCH {issuer}/api/v1/organizations/{org id}
Authorization: Bearer {m2m access token}

{ "interaction_url": "https://yourapp.com/auth" }

Set BASEAUTH_CROSS_SITE_INTERACTION=1 on the auth server as well. Your form is on your domain and posts to the issuer, which is cross-site, so the interaction cookie has to be SameSite=None; Secure to survive the trip. Both sides must then be https; the server refuses to boot with the flag set and an http issuer rather than failing on every request.

What moves is the HTML and nothing else. Your form posts back to the same endpoints the hosted page posts to, so rate limiting, account lockout, the MFA step, federated connections, the audit trail and the enumeration guarantees all still apply. You write a form; baseauth still decides what a submission means. null puts it back on the hosted pages.

Your page is reached by redirect with these query parameters:

| Parameter | Meaning | | --- | --- | | uid | The interaction id. Post it back in the URL. | | prompt | login, signup, or mfa — which form to render. | | error | Present only after a failed attempt. See below. | | login_hint | An address to pre-fill, when the request carried one. |

Post to {issuer}/interaction/{uid}/login, /signup or /mfa, matching the prompt. ?intent=signup on /api/auth/login arrives as prompt=signup, so the create-account button works the same as it does on the hosted pages.

The error codes, which are all your page needs to render a message:

| Code | Prompt | Means | | --- | --- | --- | | invalid_credentials | login | Wrong address or password — or too many attempts. Deliberately the same code: distinguishing them tells an attacker which addresses exist. | | invalid_code | mfa | The second factor did not match. | | too_many_attempts | mfa | Second-factor attempts are throttled. | | email_invalid | signup | Not a usable address. | | email_taken | signup | Already registered — offer sign-in. | | weak_password | signup | Fails the password policy. | | rate_limited | signup | Too many accounts created from this address or IP. |

Write your own copy for each: the codes are stable, the wording is yours. Anything else in error should render as a generic failure rather than being echoed to the page.

One limitation worth knowing before you build: forgot-password stays on the hosted page. Your "forgot your password?" link points at {issuer}/interaction/{uid}/forgot, and the person sees baseauth's page for that step. The two pages the reset email links into are yours to serve, and are described above.

Runtime

App Router first; Pages Router works too. Runs on both the Edge and Node runtimes — the verifier uses Web Crypto and the package deliberately compiles without Node built-ins, so a stray node:* import fails the build rather than the deploy.

Full guide

See INTEGRATION.md for multi-tenant provisioning, the Management API, and per-platform notes.

License

MIT