Skip to main content

Server adapters

WIP

Mount duck-auth on Express, Hono, Next.js, Fastify, Koa, Elysia, NestJS, gRPC, or any Web-Fetch runtime.

What mounts where

Every server adapter exposes the same four routes:

RouteHandler
POST /<prefix>/signinauthMountSignIn(auth) - completes provider sign-in.
POST /<prefix>/signoutauthMountSignOut(auth) - revokes the current session.
GET /<prefix>/sessionauthMountSession(auth) - returns the current session + identity.
POST /<prefix>/providers/:providerId/beginauthMountProviderBegin(auth, providerId) - initiates a flow (OAuth redirect, magic-link send).

The actual Express / Hono / Next functions are named consistently: authMountSignIn / authHonoSignIn / authNextSignIn etc. - they all return a handler appropriate to the framework.

Shipped adapters

AdapterImportNotes
Express@gentleduck/auth/server/expressExpress 4+. Use express.json() upstream.
Hono@gentleduck/auth/server/honoHono 4+. Works on Bun, Node, Cloudflare Workers, Deno.
Next.js@gentleduck/auth/server/nextApp router. Catch-all [...auth]/route.ts.
Fastify@gentleduck/auth/server/fastifyFastify 4+. As a plugin.
Koa@gentleduck/auth/server/koaKoa 2+. As middleware.
Elysia@gentleduck/auth/server/elysiaElysia (Bun). As handlers.
NestJS@gentleduck/auth/server/nestjsGuard + module + controller pair.
gRPC@gentleduck/auth/server/grpcInterceptor for @grpc/grpc-js.
Generic@gentleduck/auth/server/generic(request: Request) -> Promise<Response> for any Web-Fetch runtime.

The intent executor

Every adapter is a thin wrapper around the same core executor:

import { authExecuteIntents } from '@gentleduck/auth/server/generic'

const intents = await auth.flows.signIn({ providerId, input })
const response = authExecuteIntents(intents)
// -> web standard Response object with the right cookies / redirects / body

The intent executor handles:

  • startSession / rotateSession -> Set-Cookie and / or Authorization header.
  • revokeSession -> Set-Cookie: ...; Max-Age=0.
  • redirect -> Location header + 302/303.
  • json -> application/json body + status.
  • setCookie / header -> raw additions.

You can implement an adapter for an unsupported framework by calling authExecuteIntents() and mapping the resulting Response to your runtime's primitives.

Path prefix

The mount helpers don't care about path - you mount at any prefix:

// Express
app.post('/auth/signin', authMountSignIn(auth))
app.post('/auth/signout', authMountSignOut(auth))
app.get('/auth/session', authMountSession(auth))
app.post('/auth/providers/:providerId/begin', authMountProviderBegin(auth, req.params.providerId))

// Or:
app.post('/api/v1/auth/signin', authMountSignIn(auth))

The OpenAPI generator uses the prefix config (default /auth) to emit the right paths in the spec:

import { authBuildOpenApiSpec } from '@gentleduck/auth/openapi'

const spec = authBuildOpenApiSpec({
  baseUrl: 'https://api.example.com',
  prefix: '/api/v1/auth',
})

Resolving the session in handlers

Most handlers will call auth.resolveSession(req) to gate access:

import { AuthError } from '@gentleduck/auth/core'

app.get('/api/me', async (req, res) => {
  const ctx = await auth.resolveSession(req)
  if (!ctx) throw new AuthError('AUTH/UNAUTHENTICATED')
  res.json(ctx.identity)
})

For Next.js (server components / route handlers), the API is the same

  • auth.resolveSession(request) works on a Request or a Next NextRequest.