@scalemule/nextjs
v0.1.34
Published
ScaleMule SDK for Next.js applications - authentication, storage, and user management
Maintainers
Readme
@scalemule/nextjs
ScaleMule SDK for Next.js applications.
Server-side authentication with HTTP-only cookies, CSRF protection, webhook handling, secrets management, and client-side hooks.
Install
npm install @scalemule/nextjsAuth in 6 Lines
// app/api/auth/[...scalemule]/route.ts
import { createAuthRoutes } from '@scalemule/nextjs/server'
export const { GET, POST, DELETE, PATCH } = createAuthRoutes()This creates all auth endpoints automatically:
POST /api/auth/register— register + set HTTP-only cookiePOST /api/auth/login— login + set HTTP-only cookiePOST /api/auth/logout— logout + clear cookieGET /api/auth/me— get current userPOST /api/auth/forgot-password— request password resetPOST /api/auth/reset-password— reset passwordPOST /api/auth/verify-email— verify emailPOST /api/auth/resend-verification— resend verification (session or email-only)POST /api/auth/refresh— refresh sessionPATCH /api/auth/me— update profileDELETE /api/auth/me— delete account
API keys never reach the browser. Session tokens are HTTP-only cookies.
Need the cookie/header lifecycle or the proxy-vs-direct decision tree? See docs/AUTH_PROXY_PATTERN.md.
Environment Variables
# .env.local
SCALEMULE_API_KEY=sk_prod_xxx # server-only, no NEXT_PUBLIC_ prefix
SCALEMULE_ENV=prod # 'dev' | 'prod'
SCALEMULE_COOKIE_DOMAIN=.yourdomain.com # optional, for subdomain sharingAuth Route Options
export const { GET, POST, DELETE, PATCH } = createAuthRoutes({
// CSRF validation (recommended for production)
csrf: true,
// Cookie configuration
cookies: {
domain: '.yourdomain.com', // share across subdomains
maxAge: 30 * 24 * 60 * 60, // 30 days
},
// Lifecycle hooks
onRegister: async (user) => { /* post-registration logic */ },
onLogin: async (user) => { /* post-login logic */ },
onLogout: async () => { /* cleanup */ },
})Webhooks in 10 Lines
// app/api/webhooks/scalemule/route.ts
import { createWebhookHandler } from '@scalemule/nextjs/server/webhooks'
export const POST = createWebhookHandler({
secret: process.env.SCALEMULE_WEBHOOK_SECRET,
onEvent: {
'storage.file.uploaded': async (event) => {
console.log('File uploaded:', event.data.file_id)
},
'video.transcoding.completed': async (event) => {
console.log('Video ready:', event.data.video_id)
},
},
})HMAC-SHA256 signature verification and 5-minute replay protection included.
1-Line Auth Export
For zero-config auth (reads from env vars):
// app/api/auth/[...scalemule]/route.ts
export { GET, POST, DELETE, PATCH } from '@scalemule/nextjs/server/auth'Proxy Mode (keep API keys server-side)
If you don't want a real ScaleMule API key in browser bundles, run the SDK in proxy mode: pass the literal sentinel string 'proxy-mode' as apiKey and route all server-bound traffic through Next.js routes that hold the real SCALEMULE_API_KEY server-side. Both the auth route and the analytics route are required — without the analytics route, every trackEvent / trackPageView silently 401s against the gateway and the dashboard reads empty.
// app/api/auth/[...scalemule]/route.ts
export { GET, POST, DELETE, PATCH } from '@scalemule/nextjs/server/auth'// app/api/analytics/event/route.ts
import { NextRequest } from 'next/server'
import { createAnalyticsRoutes } from '@scalemule/nextjs/server'
const { POST: handler } = createAnalyticsRoutes({ simpleProxy: true })
export const dynamic = 'force-dynamic'
export function POST(req: NextRequest, ctx: { params: Promise<Record<string, string | string[]>> }) {
return handler(req, ctx)
}// app/layout.tsx
<ScaleMuleProvider
apiKey="proxy-mode"
applicationId={process.env.SCALEMULE_APP_ID}
environment="prod"
gatewayUrl="https://api.scalemule.com"
authProxyUrl="/api/auth"
analyticsProxyUrl="/api/analytics/event"
>
{children}
</ScaleMuleProvider>If apiKey="proxy-mode" is set without analyticsProxyUrl (and no publishableKey), useAnalytics emits a one-time dev console warning — that exact misconfig caused a silent-empty dashboard in production for one customer (Gistyo, app ID 019dda2a-c7d9-75a1-9258-12e4c48f1499).
Client-Side Provider
// app/layout.tsx
import { ScaleMuleProvider } from '@scalemule/nextjs'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ScaleMuleProvider
apiKey={process.env.NEXT_PUBLIC_SCALEMULE_API_KEY!}
environment={process.env.NEXT_PUBLIC_SCALEMULE_ENV as 'dev' | 'prod'}
>
{children}
</ScaleMuleProvider>
</body>
</html>
)
}Client-Side Hooks
useAuth()
'use client'
import { useAuth } from '@scalemule/nextjs'
function LoginPage() {
const { login, register, logout, user, loading, isAuthenticated, error } = useAuth()
const handleLogin = async () => {
await login({ email, password })
}
}useMedia() — recommended for image / video / audio uploads
One hook covers every MIME type, defaults to private, and auto-routes images
through the photo pipeline (optimization, transform breakpoints) and videos
through the video pipeline (HLS transcoding). Drop the result file_id into
a chat message or render it with <ScaleMuleMedia /> and progressive
enhancement just works.
import { useMedia, ScaleMuleMedia } from '@scalemule/nextjs'
function Composer({ conversationId }: { conversationId: string }) {
const { upload, uploading } = useMedia()
async function handleFile(file: File) {
const result = await upload(file)
// result.file_id is private by default; post the message with it.
await postMessage({ conversationId, attachments: [{ file_id: result.file_id }] })
}
return <input type="file" disabled={uploading} onChange={(e) => handleFile(e.target.files![0])} />
}
function Attachment({ fileId, conversationId, mimeType }: {
fileId: string; conversationId: string; mimeType: string
}) {
// <ScaleMuleMedia /> uses useFileStatus internally; with conversationId
// it subscribes to the conversation channel for live optimize/transcode
// updates instead of polling.
return (
<ScaleMuleMedia
fileId={fileId}
mimeType={mimeType}
conversationId={conversationId}
/>
)
}See docs/MEDIA-UPLOADS.md
for the full decision tree (which hook for which app, fast/safe policy modes,
private vs public visibility, anti-patterns).
useFileStatus() — read-side progressive enhancement
import { useFileStatus } from '@scalemule/nextjs'
// Chat surface — subscribe to the conversation channel for push updates.
const { status, isReady } = useFileStatus({ fileId, conversationId })
// Non-chat — pull-only with optional polling.
const { status, isReady } = useFileStatus({ fileId, pollIntervalMs: 2000 })useContent() — generic file uploads (legacy)
Deprecated for chat / progressive media. Use
useMedia()instead.useContent()remains available for plain non-image/video file lists (documents, downloads). It does not auto-route through the photo/video pipelines and does not enforce private-by-default for chat surfaces.
import { useContent } from '@scalemule/nextjs'
function Gallery() {
const { files, upload, loading, uploadProgress, refresh, remove } = useContent({ autoFetch: true })
const handleUpload = async (file: File) => {
await upload(file, {
onProgress: (pct) => console.log(`${pct}%`),
})
}
}useUser()
import { useUser } from '@scalemule/nextjs'
function Settings() {
const { profile, update, changePassword, changeEmail, deleteAccount, exportData } = useUser()
}useRealtime()
import { useRealtime } from '@scalemule/nextjs'
function LiveUpdates() {
const { status, subscribe } = useRealtime({ autoConnect: true })
useEffect(() => {
const unsub = subscribe('notifications', (data) => {
console.log('New notification:', data)
})
return () => unsub()
}, [subscribe])
}Server Client
For server-side operations beyond auth (API routes, server components, server actions):
import { createServerClient } from '@scalemule/nextjs/server'
const sm = createServerClient()
// Storage
const { data } = await sm.storage.getViewUrl(fileId)
// Auth (with session token)
const { data: user } = await sm.auth.me(sessionToken)
// Secrets
const secret = await sm.secrets.get('STRIPE_KEY')
// Vault bundles
const db = await sm.bundles.get('database/primary')CSRF Protection
Middleware Setup
// middleware.ts
import { generateCSRFToken, CSRF_COOKIE_NAME } from '@scalemule/nextjs/server'
import { NextResponse } from 'next/server'
export function middleware(request) {
const response = NextResponse.next()
if (!request.cookies.get(CSRF_COOKIE_NAME)) {
const token = generateCSRFToken()
response.cookies.set(CSRF_COOKIE_NAME, token, {
httpOnly: false, // must be readable by JS
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
path: '/',
})
}
return response
}Enable in Auth Routes
export const { GET, POST, DELETE, PATCH } = createAuthRoutes({
csrf: true, // validates x-csrf-token header against cookie
})Client-Side
// Read CSRF token from cookie and include in requests
const csrfToken = document.cookie.match(/sm_csrf=([^;]+)/)?.[1]
fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-csrf-token': csrfToken,
},
body: JSON.stringify({ email, password }),
})Secrets & Vault
import {
getAppSecret,
requireAppSecret,
getMySqlBundle,
getRedisBundle,
getS3Bundle,
getOAuthBundle,
getBundle,
} from '@scalemule/nextjs/server'
// Simple secrets
const apiKey = await getAppSecret('STRIPE_API_KEY')
const required = await requireAppSecret('WEBHOOK_SECRET') // throws if missing
// Typed bundles
const db = await getMySqlBundle('database/primary') // { host, port, user, password, database, connectionUrl }
const redis = await getRedisBundle('cache/main') // { host, port, password, connectionUrl }
const s3 = await getS3Bundle('storage/uploads') // { bucket, region, access_key_id, secret_access_key }
const oauth = await getOAuthBundle('google') // { client_id, client_secret, redirect_uri }
// Generic bundle
const stripe = await getBundle<{ api_key: string; webhook_secret: string }>('external/stripe')Secrets are cached for 5 minutes. Configure with configureSecrets({ cacheTtlMs, noCache }).
Testing
import { MockScaleMuleProvider, createMockUser, createMockFile } from '@scalemule/nextjs/testing'
import { render, screen } from '@testing-library/react'
test('shows user name', () => {
render(
<MockScaleMuleProvider user={createMockUser({ full_name: 'Jane' })}>
<ProfilePage />
</MockScaleMuleProvider>
)
expect(screen.getByText('Jane')).toBeInTheDocument()
})License
MIT - ScaleMule Inc.
