User Utils
@stacknet/userutils is the authentication and session package for Stacknet. It provides everything you need to connect users to your Stack, Login, session management, and billing hooks.
PromptMe
Copy the prompt for your framework and give it to your AI coding agent (Claude Code, Cursor, Copilot, etc.) to scaffold Stacknet authentication into your app. Replace stk_REPLACE_ME with your Stack ID.
Next.js
## Task
Integrate Stacknet authentication into this Next.js app using `@stacknet/userutils`. The app should let users log in with wallet or OAuth, persist sessions via cookies, and protect pages behind an auth gate.
## Stack details
- Stack ID: `stk_REPLACE_ME`
- Stacknet URL: `https://stacknet.magma-rpc.com`
## Steps
### 1. Install
```bash
npm install @stacknet/userutils
```
### 2. Environment variables
Add to `.env.local`:
```
NEXT_PUBLIC_STACK_ID=stk_REPLACE_ME
NEXT_PUBLIC_STACKNET_URL=https://stacknet.magma-rpc.com
AUTH_SECRET=generate-a-random-32-char-string
STACKNET_JWT_SECRET=generate-another-random-32-char-string
```
### 3. Create API routes
Create these three Next.js App Router API routes:
**`app/api/auth/callback/route.ts`**
```ts
import { createAuthCallback } from '@stacknet/userutils/server'
const handler = createAuthCallback({
authSecret: process.env.AUTH_SECRET || '',
stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL || 'https://stacknet.magma-rpc.com',
stackId: process.env.NEXT_PUBLIC_STACK_ID || '',
stacknetJwtSecret: process.env.STACKNET_JWT_SECRET,
secureCookies: process.env.NODE_ENV === 'production',
sessionMaxAge: 2592000,
jwtExpiry: 86400,
})
export const POST = handler
```
**`app/api/auth/session/route.ts`**
```ts
import { createSessionHandler } from '@stacknet/userutils/server'
const handler = createSessionHandler({
authSecret: process.env.AUTH_SECRET || '',
secureCookies: process.env.NODE_ENV === 'production',
})
export async function GET(request: Request) {
return handler(request)
}
```
**`app/api/auth/logout/route.ts`**
```ts
import { createLogoutHandler } from '@stacknet/userutils/server'
const handler = createLogoutHandler({
stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL || 'https://stacknet.magma-rpc.com',
secureCookies: process.env.NODE_ENV === 'production',
})
export async function POST(request: Request) {
return handler(request)
}
```
### 4. Create auth provider
Create `components/auth-provider.tsx`:
```tsx
'use client'
import { createContext, useContext, ReactNode } from 'react'
import { UserUtilsProvider, ConnectWidget } from '@stacknet/userutils/components'
import { useSession, useStackAuth } from '@stacknet/userutils/hooks'
import type { UserUtilsConfig, PublicSession } from '@stacknet/userutils'
const STACKNET_CONFIG: UserUtilsConfig = {
apiBaseUrl: '',
stackId: process.env.NEXT_PUBLIC_STACK_ID || '',
stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL || 'https://stacknet.magma-rpc.com',
theme: 'dark',
}
interface AuthContextValue {
session: PublicSession | null
isAuthenticated: boolean
loading: boolean
logout: () => Promise<void>
refresh: () => Promise<unknown>
config: UserUtilsConfig
}
const AuthContext = createContext<AuthContextValue>({
session: null,
isAuthenticated: false,
loading: true,
logout: async () => {},
refresh: async () => {},
config: STACKNET_CONFIG,
})
export function useAuth() {
return useContext(AuthContext)
}
function AuthInner({ children }: { children: ReactNode }) {
const { session, isAuthenticated, loading, logout, refresh } = useStackAuth({
...STACKNET_CONFIG,
autoConnect: false,
})
return (
<AuthContext.Provider value={{ session, isAuthenticated, loading, logout, refresh, config: STACKNET_CONFIG }}>
{children}
</AuthContext.Provider>
)
}
export function AuthProvider({ children }: { children: ReactNode }) {
return (
<UserUtilsProvider config={STACKNET_CONFIG}>
<AuthInner>{children}</AuthInner>
</UserUtilsProvider>
)
}
export function LoginWidget({ onSuccess }: { onSuccess?: () => void }) {
return <ConnectWidget config={STACKNET_CONFIG} onSuccess={onSuccess} showWallets={['phantom', 'metamask']} />
}
export { STACKNET_CONFIG }
```
### 5. Wrap your layout
In `app/layout.tsx`, wrap your app with the `AuthProvider`:
```tsx
import { AuthProvider } from '../components/auth-provider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
)
}
```
### 6. Create a login page
Create `app/login/page.tsx`:
```tsx
'use client'
import { useEffect } from 'react'
import { ConnectWidget } from '@stacknet/userutils/components'
import { useAuth, STACKNET_CONFIG } from '../../components/auth-provider'
export default function LoginPage() {
const { isAuthenticated } = useAuth()
useEffect(() => {
if (isAuthenticated) window.location.href = '/'
}, [isAuthenticated])
if (isAuthenticated) return null
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh' }}>
<ConnectWidget
config={STACKNET_CONFIG}
onSuccess={() => { window.location.href = '/' }}
showWallets={['phantom', 'metamask']}
/>
</div>
)
}
```
### 7. Protect pages with auth
Use the `useAuth` hook to gate content:
```tsx
'use client'
import { useAuth } from '../components/auth-provider'
export default function Dashboard() {
const { session, isAuthenticated, loading } = useAuth()
if (loading) return <p>Loading...</p>
if (!isAuthenticated) {
window.location.href = '/login'
return null
}
return <h1>Welcome, {session?.address}</h1>
}
```
## Style guidelines
Follow the Stacknet visual language:
- **Background**: `#191919` (page), `#262625` (cards/panels)
- **Text**: `#FAFAF7` (primary), `#91918D` (secondary), `#666663` (muted)
- **Accent**: `#165DFC` (blue, links and primary actions)
- **Danger**: `#BF4D43` (destructive actions)
- **Borders**: `#40403E`
- **Font**: `'Courier New', monospace` for headings and code; system sans-serif for body text
- **Theme**: always dark — set `theme: 'dark'` in `UserUtilsConfig`
## Result
After completing these steps you will have:
- Wallet + OAuth login via Stacknet
- Server-side session cookies (30-day max age, 24h JWT rotation)
- A reusable `useAuth()` hook for reading session state anywhere
- A login page with the `ConnectWidget`
- Auth-gated pages that redirect unauthenticated usersClient side
Wrap your app with UserUtilsProvider and use hooks to read auth state:
import { UserUtilsProvider, ConnectWidget } from '@stacknet/userutils/components'
import { useSession, useStackAuth } from '@stacknet/userutils/hooks'
function App() {
return (
<UserUtilsProvider config={{
apiBaseUrl: '/api',
stackId: 'stk_YOUR_STACK_ID',
stacknetUrl: 'https://stacknet.magma-rpc.com',
theme: 'dark',
}}>
<MyApp />
</UserUtilsProvider>
)
}
function MyApp() {
const { session, isAuthenticated } = useSession()
if (!isAuthenticated) return <ConnectWidget config={config} />
return <p>Welcome, {session.address}</p>
}Server side (Next.js API routes)
Three route handlers cover the full auth lifecycle:
/api/auth/callback — verifies the signed challenge and sets the session cookie
import { createAuthCallback } from '@stacknet/userutils/server'
const handler = createAuthCallback({
authSecret: process.env.AUTH_SECRET,
stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL || 'https://stacknet.magma-rpc.com',
stackId: process.env.NEXT_PUBLIC_STACK_ID,
stacknetJwtSecret: process.env.STACKNET_JWT_SECRET,
secureCookies: process.env.NODE_ENV === 'production',
sessionMaxAge: 2592000, // 30 days
jwtExpiry: 86400, // 24 hours
})
export const POST = handler/api/auth/session — returns current session state
import { createSessionHandler } from '@stacknet/userutils/server'
const handler = createSessionHandler({
authSecret: process.env.AUTH_SECRET,
secureCookies: process.env.NODE_ENV === 'production',
})
export async function GET(request: Request) {
return handler(request)
}/api/auth/logout — clears the session cookie
import { createLogoutHandler } from '@stacknet/userutils/server'
const handler = createLogoutHandler({
stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL || 'https://stacknet.magma-rpc.com',
secureCookies: process.env.NODE_ENV === 'production',
})
export async function POST(request: Request) {
return handler(request)
}Hooks reference
| Hook | Returns | Description |
|---|---|---|
useSession | { session, isAuthenticated } | Read-only session state — user ID, address, expiry |
useStackAuth | { session, wallet, authenticateSolana, authenticateEVM, logout, refresh } | Full auth control — connect wallets, sign challenges, manage session |
useWeb3Wallet | { connected, address, chain, provider } | Wallet connection state and provider detection |
useAuthBridge | { ready, known, identity, identityCount, resolvedStackId } | Cross-domain SSO — detects identities across Stacks |
usePlans | { plans, loading } | List billing plans available on the Stack |
useSubscription | { subscription, active, plan } | Current user subscription state |
useUsage | { usage, remaining, limit } | Token usage against current plan |
useBillingHistory | { records, loading } | Past billing and payment records |
usePrepaidCheckout | { checkout, processing } | Initiate credit purchases |
Components reference
| Component | Props | Description |
|---|---|---|
UserUtilsProvider | config: UserUtilsConfig | Required context provider — wrap your app root |
ConnectWidget | config, onSuccess, showWallets, showOTP, title | Drop-in login UI with wallet and OAuth options |
Environment variables
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_STACK_ID | Yes | Your Stack ID (e.g. stk_abc123) |
NEXT_PUBLIC_STACKNET_URL | Yes | Stacknet backend URL (https://stacknet.magma-rpc.com) |
AUTH_SECRET | Yes | Secret for signing session cookies (min 32 chars) |
STACKNET_JWT_SECRET | Yes | Secret for re-signing JWTs to Stacknet backend |
Cross-domain SSO
The useAuthBridge hook enables single sign-on across multiple Stacks. When a user is already authenticated on one Stack, the bridge detects their identity and can resolve which Stack they last used:
import { useAuthBridge } from '@stacknet/userutils/hooks'
function LoginPage() {
const bridge = useAuthBridge()
if (bridge.ready && bridge.known) {
return <p>Welcome back — {bridge.identity.address}</p>
}
return <ConnectWidget config={config} />
}