← Files InsForgeARCHIVED FILE

skills/insforge/auth/ssr-integration.md

12.1 KB · Oct 5, 2026 · 18:29 UTC

↓ Download file

# SSR Authentication Integration

Use this reference for Next.js SSR auth. The same cookie/session concepts can be adapted to Remix, SvelteKit, Nuxt, or other SSR frameworks, but the examples below use Next.js route handlers, server actions, and Proxy/Middleware.

## Recommended Pattern

- Use `@insforge/sdk/ssr` as the standard SSR auth entrypoint for browser,
  server, refresh-route, and cookie helpers.
- Use `@insforge/sdk/ssr/middleware` for Proxy/Middleware `updateSession()`;
  this keeps the middleware bundle from pulling in the full SDK client.
- Let the SDK helpers manage the InsForge auth cookie names and expiration.
- Keep `insforge_refresh_token` httpOnly and server-owned.
- Allow `insforge_access_token` to be browser-readable so browser SDK calls and Realtime can authenticate.
- Use `/api/auth/refresh` as the app refresh endpoint unless the user already has a clear project convention.
- Use Proxy/Middleware to refresh before Server Components render.

Default cookies:

| Cookie | Visibility | Purpose |
|--------|------------|---------|
| `insforge_access_token` | `httpOnly: false` | Short-lived bearer token for Server Components, Client Components, Storage, and Realtime |
| `insforge_refresh_token` | `httpOnly: true` | Long-lived server-owned refresh credential |

Both cookies should expire at the JWT `exp`; the SDK helpers do this when tokens include `exp`.

`insforge_access_token` is intentionally readable by JavaScript so browser SDK calls, Storage, and Realtime can authenticate directly. Keep access-token TTL short. If a project requires fully httpOnly auth tokens, proxy Storage and Realtime calls through server-side routes instead of direct browser SDK calls.

## Environment Variables

For Next.js, prefer:

```bash
NEXT_PUBLIC_INSFORGE_URL=https://your-project.insforge.app
NEXT_PUBLIC_INSFORGE_ANON_KEY=...
NEXT_PUBLIC_APP_URL=https://your-app.example
```

The SSR helpers use explicit `baseUrl` / `anonKey` when provided. Otherwise they read `NEXT_PUBLIC_INSFORGE_URL` / `NEXT_PUBLIC_INSFORGE_ANON_KEY` in both browser and server code. `NEXT_PUBLIC_APP_URL` is the public app origin used by OAuth redirect examples. Missing config throws a clear error.

## Browser Client

Use this in Client Components and browser-only modules:

```typescript
// app/lib/insforge/client.ts
import { createBrowserClient } from '@insforge/sdk/ssr'

export const insforge = createBrowserClient()
```

`createBrowserClient()` reads `insforge_access_token`, uses it for SDK calls and Realtime, and refreshes through `/api/auth/refresh` when the access token is missing, expired, near expiry, or rejected with an auth-expired response. Its TypeScript auth surface is read-only (`getCurrentUser()`, `getProfile()`, and `getPublicAuthConfig()`); perform sign-in, sign-up, sign-out, OAuth initiation/exchange, ID-token sign-in, and email verification on the server with `createAuthActions()`.

## Server Client

Use this in Server Components, Route Handlers, and Server Actions:

```typescript
// app/lib/insforge/server.ts
import { cookies } from 'next/headers'
import { createServerClient } from '@insforge/sdk/ssr'

export async function createInsForgeServerClient() {
  return createServerClient({
    cookies: await cookies()
  })
}
```

`createServerClient()` reads the access-token cookie and passes it as the per-request bearer token. The refresh token remains server-owned.

## Refresh Route

Create the default app refresh endpoint:

```typescript
// app/api/auth/refresh/route.ts
import { createRefreshAuthRouter } from '@insforge/sdk/ssr'

export const { POST } = createRefreshAuthRouter()
```

If the app needs custom side effects, use the lower-level helper:

```typescript
// app/api/auth/refresh/route.ts
import { refreshAuth } from '@insforge/sdk/ssr'

export async function POST(request: Request) {
  const result = await refreshAuth({ request })
  // Optional: app-specific logging, telemetry, redirect validation, etc.
  return result.response
}
```

## Next.js Proxy / Middleware

Use `updateSession()` so Server Components see fresh cookies before rendering. Next.js 16 uses `proxy.ts`; Next.js 15 and earlier use `middleware.ts`.

```typescript
// proxy.ts on Next.js 16+
// middleware.ts on Next.js 15 and earlier
import { NextResponse, type NextRequest } from 'next/server'
import { updateSession } from '@insforge/sdk/ssr/middleware'

export async function proxy(request: NextRequest) {
  const response = NextResponse.next({ request })

  await updateSession({
    requestCookies: request.cookies,
    responseCookies: response.cookies
  })

  return response
}
```

Import `updateSession()` from `@insforge/sdk/ssr/middleware` in Proxy/Middleware
files. Keep the full `@insforge/sdk/ssr` entrypoint for `createBrowserClient()`,
`createServerClient()`, `createRefreshAuthRouter()`, `refreshAuth()`, and cookie
helpers in route handlers or server/client modules.

For `middleware.ts`, export the same handler body as `middleware`.

## Sign-In Route Or Server Action

Because the refresh token is httpOnly, sign-in, sign-up, sign-out, OAuth initiation/exchange, ID-token sign-in, and email verification flows that establish or clear a session should run where cookies can be written. Prefer `createAuthActions()` for these auth mutations. Return only safe app data from Server Actions; do not return token-bearing low-level auth responses.

For Next.js 14+ Server Actions:

```typescript
// app/actions.ts
'use server'

import { cookies } from 'next/headers'
import { createAuthActions } from '@insforge/sdk/ssr'

export async function signIn(formData: FormData) {
  const auth = createAuthActions({ cookies: await cookies() })
  const { data, error } = await auth.signInWithPassword({
    email: String(formData.get('email')),
    password: String(formData.get('password'))
  })

  return { user: data?.user ?? null, error }
}

export async function signOut() {
  const auth = createAuthActions({ cookies: await cookies() })
  return auth.signOut()
}
```

For Route Handlers, pass separate request and response cookie stores:

```typescript
// app/api/auth/sign-in/route.ts
import { NextResponse, type NextRequest } from 'next/server'
import { createAuthActions } from '@insforge/sdk/ssr'

export async function POST(request: NextRequest) {
  const response = NextResponse.json({ ok: true })
  const auth = createAuthActions({
    requestCookies: request.cookies,
    responseCookies: response.cookies
  })

  const { data, error } = await auth.signInWithPassword(await request.json())
  if (error || !data?.user) {
    return NextResponse.json(
      { error: error?.error ?? 'AUTH_UNAUTHORIZED', message: error?.message ?? 'Sign in failed' },
      { status: error?.statusCode ?? 401 }
    )
  }

  return NextResponse.json({ user: data.user }, { headers: response.headers })
}
```

`createAuthActions()` wraps `createServerClient()` and writes or clears `insforge_access_token` / `insforge_refresh_token` using the cookie stores you pass. Keep refresh separate: `/api/auth/refresh` should still use `createRefreshAuthRouter()` or `refreshAuth()`.

## OAuth In Next.js

The browser SDK auto-detects `insforge_code` for SPA flows. In SSR apps, handle OAuth on the server so the refresh token lands in an httpOnly cookie.

### Step 1: Start OAuth

```typescript
'use server'

import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { createAuthActions } from '@insforge/sdk/ssr'

export async function initiateOAuth(provider: string) {
  const cookieStore = await cookies()
  const auth = createAuthActions({ cookies: cookieStore })
  const { data, error } = await auth.signInWithOAuth(provider, {
    redirectTo: new URL('/api/auth/callback', process.env.NEXT_PUBLIC_APP_URL).toString(),
    // additionalParams: { prompt: 'select_account' }, // optional provider-specific hints
    skipBrowserRedirect: true
  })

  if (error || !data.url || !data.codeVerifier) {
    throw new Error(error?.message ?? 'OAuth init failed')
  }

  cookieStore.set('insforge_code_verifier', data.codeVerifier, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 600
  })

  redirect(data.url)
}
```

Use `additionalParams` only for provider-specific optional hints. Do not pass server-owned OAuth fields such as `client_id`, `scope`, `redirect_uri`, `code_challenge`, `state`, or `response_type`; InsForge sets those server-side and ignores colliding client-provided keys.

Set `redirectTo` to your app URL. The backend appends `?insforge_code=<code>` and redirects there.

### Step 2: Handle Callback

```typescript
// app/api/auth/callback/route.ts
import { cookies } from 'next/headers'
import { NextResponse, type NextRequest } from 'next/server'
import { createAuthActions } from '@insforge/sdk/ssr'

export async function GET(request: NextRequest) {
  const code = request.nextUrl.searchParams.get('insforge_code')
  const oauthError = request.nextUrl.searchParams.get('error')

  if (oauthError || !code) {
    if (oauthError) {
      console.warn('OAuth callback failed', { error: oauthError })
    }
    return NextResponse.redirect(new URL('/login?error=oauth_failed', request.url))
  }

  const cookieStore = await cookies()
  const codeVerifier = cookieStore.get('insforge_code_verifier')?.value
  if (!codeVerifier) {
    return NextResponse.redirect(new URL('/login?error=missing_verifier', request.url))
  }

  const response = NextResponse.redirect(new URL('/dashboard', request.url))
  const auth = createAuthActions({
    requestCookies: request.cookies,
    responseCookies: response.cookies
  })
  const { data, error } = await auth.exchangeOAuthCode(code, codeVerifier)
  if (error || !data?.accessToken) {
    if (error) {
      console.error('OAuth code exchange failed', error)
    }
    return NextResponse.redirect(new URL('/login?error=exchange_failed', request.url))
  }

  response.cookies.delete('insforge_code_verifier')

  return response
}
```

## Storage And Realtime

For browser uploads, downloads, and Realtime subscriptions, use `createBrowserClient()`. Keep the refresh token server-owned and route browser refresh through `/api/auth/refresh`. When the access token expires, the browser client receives a fresh access token and updates the SDK with token-refresh semantics, so active Realtime sockets stay connected and the fresh JWT is used on the next handshake. Do not call auth mutations from Client Components; use `createAuthActions()` on the server.

For server-mediated uploads, use a backend route to create a signed upload path or otherwise proxy the operation. Use direct browser SDK upload when the app wants user-scoped Storage/RLS checks and the browser has the access token cookie.

## Refresh Best Practices

- Keep the default refresh path `/api/auth/refresh` unless the app has an existing auth namespace.
- Use `createRefreshAuthRouter()` for standard apps.
- Use `refreshAuth()` only when the route needs custom side effects.
- Use `updateSession()` from `@insforge/sdk/ssr/middleware` in Proxy/Middleware to keep Server Components and browser cookies aligned without bundling the full SDK client.
- Validate post-auth redirects and only allow safe internal paths.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Creating separate cookie names across routes | Let `@insforge/sdk/ssr` helpers manage the standard auth cookie names |
| Hiding the access token from Client Components | Keep `insforge_refresh_token` httpOnly and `insforge_access_token` browser-readable |
| Missing the browser refresh route | Use `/api/auth/refresh` with `createRefreshAuthRouter()` |
| Manually guessing cookie lifetimes | Use `createAuthActions()` or `setAuthCookies()` so cookie expiry follows JWT `exp` |
| Calling auth mutations from Client Components | Use `createAuthActions()` in Server Actions or Route Handlers |
| Server Components reading stale cookies | Use `updateSession()` from `@insforge/sdk/ssr/middleware` in Proxy/Middleware before rendering |
| Client Components creating an unauthenticated SDK client | Use `createBrowserClient()` so the app refresh route can refresh access |
| Sending OAuth users back to the backend URL | Set `redirectTo` to the app URL where the user lands after auth |
| Exchanging OAuth codes in a Client Component | Initiate and exchange OAuth on the server, then set cookies |

SHA-256: 6c3cfc36fdff2e9153519e7f50858dfbfc15119930fb6ba1a89af61f07d3d7b2