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

@synqauth/nextjs

v0.3.0

Published

Synq for Next.js: a backend-for-frontend session, so the browser never holds a Synq token.

Readme

@synqauth/nextjs

Synq for Next.js: a backend-for-frontend session, so the browser never holds a Synq token.

Synq is customer identity for businesses: an OpenID Connect issuer for each of your brands, with Solana wallet, social, email and passkey sign-in. The documentation is at docs.synq.build.

This package is at 0.x, so a minor release can change its API: see its CHANGELOG.md.

Quick start

Register the app in the Synq dashboard as a web app, with https://<your app>/api/auth/callback as its redirect URI and https://<your app>/ as its post-logout URI. Then set five environment variables:

SYNQ_ISSUER=https://<your brand>.auth.synq.build
SYNQ_CLIENT_ID=app_...
SYNQ_CLIENT_SECRET=...
SYNQ_APP_URL=https://app.example.com
SYNQ_SESSION_SECRET=$(openssl rand -base64 32)

One configuration, which the server and the proxy share:

// synq.config.ts
import { defineSynqConfig } from '@synqauth/nextjs/config'

export const synqConfig = defineSynqConfig({})
// lib/synq.ts
import { synqAuth } from '@synqauth/nextjs'
import { synqConfig } from '../synq.config'

export const synq = synqAuth(synqConfig)
// app/api/auth/[...synq]/route.ts
import { synq } from '@/lib/synq'

export const { GET, POST, PATCH, DELETE } = synq.handlers
// proxy.ts
import { synqProxy } from '@synqauth/nextjs/proxy'
import { synqConfig } from './synq.config'

export const proxy = synqProxy(synqConfig, { protect: ['/dashboard'] })
// Pages only: not Next's files, nor anything with an extension.
export const config = { matcher: ['/((?!_next/|.*\\..*).*)'] }

To add a way to sign in to someone's account (Google, a wallet, a passkey, anything the brand offers), POST { method, returnTo } as JSON to /api/auth/link from your own pages and send the browser to the redirect it answers, or call useAuth().link('google') from @synqauth/react: Synq asks them to sign in again, then adds it.

To sign people in on your own page instead of the brand's (AERO-1411), turn on own-UI sign-in for the app in the dashboard and use <SignInForm> or useSignIn() from @synqauth/react. They post each step to /api/auth/sign-in/flow, which takes it to Synq as your app. Pass clientAddress to defineSynqConfig when your host doesn't set x-real-ip, so Synq can limit tries per address. Read a header your own proxy sets, never one the browser could send:

export const synqConfig = defineSynqConfig({
  // Behind Cloudflare
  clientAddress: (request) => request.headers.get('cf-connecting-ip') ?? undefined,
})

In a server component, await synq.me() is the signed-in person or null, and await synq.getAccessToken() is a token for your API (resource). Link synq.signInPath({ returnTo: '/dashboard' }) to sign in, and sign out with a form that POSTs to synq.signOutPath.

When a sign-in doesn't finish

Synq's own dashboard can resume an unverified email address with /auth/sign-in?intent=verify-email&returnTo=/onboard. The intent defaults to prompt=select_account, so an existing GitHub-only account can prove its email without signing in again or creating another account. An explicit prompt and max_age are preserved: verification never replaces a request for fresh authentication. A maximum-age request takes the normal sign-in flow; an incompatible explicit prompt is refused rather than replaced. This verification flow applies only where the issuer requires unproved email addresses to be verified; tenants keep their existing behavior.

When Synq refuses a sign-in (the person cancelled, their account can't use the app, a token gate they went back from) or the callback fails, the route mount sends the browser back to returnTo with synq_error=<code>. The proxy never sends such a page to sign in again: with the same session at the brand, that would land on the same refusal, round and round. Without a session, it shows why instead, with "Try again" and "Use another account" (prompt=select_account). Give it a page of your own with signInError, which it never protects:

export const proxy = synqProxy(synqConfig, { protect: () => true, signInError: '/sign-in-error' })
// app/sign-in-error/page.tsx
export default async function SignInError({ searchParams }: PageProps<'/sign-in-error'>) {
  const failed = synq.signInError(await searchParams)
  if (!failed) redirect('/')
  return (
    <main>
      <h1>Sign-in didn’t finish</h1>
      <p>{failed.message}</p>
      <a href={failed.retryPath}>Try again</a> <a href={failed.anotherAccountPath}>Use another account</a>
    </main>
  )
}

A page that isn't protected gets synq_error as it is: synq.signInError(await searchParams, '/where-to-come-back') reads it there too.

Next 16 runs proxy.ts on Node. The proxy imports nothing only Node has, so it works on the Edge runtime too.

More than one server process: use a store

By default a session lives in a sealed, HttpOnly cookie, and only one refresh runs at a time within one server process. Synq rotates refresh tokens and ends a session whose old one comes back, so an app on several processes (Vercel and other serverless hosts, replicas) must keep sessions in a store they all share, which gives each session one lock:

import { keyValueSessionStore } from '@synqauth/nextjs'

export const synqConfig = defineSynqConfig({
  store: keyValueSessionStore({
    get: (key) => redis.get(key),
    set: async (key, value, { ttlMs, onlyIfAbsent }) =>
      (await redis.set(key, value, { PX: ttlMs, NX: onlyIfAbsent })) === 'OK',
    delete: async (key) => void (await redis.del(key)),
  }),
})

synqAuth() refuses cookie sessions where VERCEL, AWS_LAMBDA_FUNCTION_NAME, NETLIFY, K_SERVICE or KUBERNETES_SERVICE_HOST is set, unless allowSingleProcessCookieMode says the app truly runs one process. A store also lets your server end a session; a cookie can't be taken back.

To rotate SYNQ_SESSION_SECRET, move the old secret to SYNQ_SESSION_SECRET_PREVIOUS (comma-separated), which opens cookies but never seals them.

Several sites from one deployment

One app can sign people in on many hosts, each with its own app, at its own brand or a shared one: a site per chapter, say. Pass hosts, every host you serve (*. and a domain for any one label under it), and resolve, which answers each request's site from its host. It may be async and read the client secret from your own store:

export const synqConfig = defineSynqConfig({
  hosts: ['*.acmeguild.com'],
  async resolve({ host }) {
    const chapter = await chapterByHost(host)
    if (!chapter) return null
    return {
      issuer: chapter.issuer,
      clientId: chapter.clientId,
      clientSecret: await chapter.clientSecret(),
      storePrefix: `chapter-${chapter.slug}:`,
    }
  },
})

The handlers, the proxy, me(), getSession() and getAccessToken() then pick the request's site per request. The host comes from X-Forwarded-Host, else Host, and the site's app URL is https://<host>. A host off hosts, or one resolve answers null for, is refused with unknown_host: never given another site's configuration. Each site's core is kept, and its cookies are sealed for it alone, so sites never share a session. The guide is at docs.synq.build/guides/several-sites.

License

MIT