This guide shows how to integrate utilsio into a Next.js application. Youâll have a functional subscription system where users authenticate via Supabase and manage subscriptions through utilsio.
You have three options to get started. Pick the one that works best for you:
Option 1: Clone the Template (5-10 minutes)
Clone the pre-configured template with everything set up and working.
# Clone the Next.js template
git clone https://github.com/utilsio/templates
cd templates/nextjs
# Install dependencies with Bun (recommended)
bun install
# Or use npm
npm install
# Copy environment variables
cp .env.example .env.local
# Edit .env.local with your utilsio credentials
# NEXT_PUBLIC_UTILSIO_APP_ID=your_app_id
# UTILSIO_APP_SECRET=your_app_secret
# UTILSIO_APP_SALT=your_app_salt
# NEXT_PUBLIC_UTILSIO_APP_URL=https://utilsio.dev
# NEXT_PUBLIC_APP_URL=http://localhost:3001
# Start the dev server
bun dev
# Server runs on http://localhost:3001
What you get:
- â
Full working example with all features
- â
Best practices for security and performance
- â
Real-world error handling and edge cases
- â
Production-ready patterns
- â
Reference for all documentation
This is your best resource. Use the template code alongside this documentation to understand how everything works together.
Option 2: Manual Setup (15-20 minutes)
Follow these steps to integrate utilsio into an existing Next.js application.
Prerequisites
- A Next.js 13+ application with the App Router
- A utilsio account with credentials from https://utilsio.dev/creator/apps:
NEXT_PUBLIC_UTILSIO_APP_ID - Your public app identifier
UTILSIO_APP_SECRET - Your private app secret (backend only)
UTILSIO_APP_SALT - Your app salt (backend only)
NEXT_PUBLIC_UTILSIO_APP_URL - The utilsio service URL (default: https://utilsio.dev)
NEXT_PUBLIC_APP_URL - Your applicationâs public URL (e.g., http://localhost:3001)
Step 1: Install the SDK
Install the utilsio React SDK package:
Step 2: Set Up Environment Variables
Create a .env.local file in your project root with your utilsio credentials:
# Public environment variables (safe to expose to browser)
NEXT_PUBLIC_UTILSIO_APP_ID=your_app_id_here
NEXT_PUBLIC_UTILSIO_APP_URL=https://utilsio.dev
NEXT_PUBLIC_APP_URL=http://localhost:3001
# Secret environment variables (backend only, never expose to browser)
UTILSIO_APP_SECRET=your_app_secret_here
UTILSIO_APP_SALT=your_app_salt_here
Variables prefixed with NEXT_PUBLIC_ are accessible in the browser. Never put secrets in these variables. Your UTILSIO_APP_SECRET and UTILSIO_APP_SALT must ONLY be used on the backend.
How to get your credentials:
- Go to https://utilsio.dev/creator/apps
- Create or select your app
- Copy the App ID, App Secret, and App Salt
- Store these in your
.env.local file
Step 3: Create the Signing Route
The signing route is critical for security. Your backend signs each request with your app secret, preventing unauthorized access. The HMAC-SHA256 signature ensures that your UTILSIO_APP_SECRET never leaves your server.
Create src/app/api/sign/route.ts:
src/app/api/sign/route.ts
import { NextRequest, NextResponse } from "next/server";
import {
deriveAppHashHex,
nowUnixSeconds,
signRequest,
} from "@utilsio/react/server";
// Derive the hash once at module load (for performance)
// This is computed from your app secret and salt
const appHashHex = deriveAppHashHex({
appSecret: process.env.UTILSIO_APP_SECRET!,
salt: process.env.UTILSIO_APP_SALT!,
});
export async function POST(req: NextRequest) {
try {
// Parse the incoming request body from the client
const body = await req.json();
const { deviceId, additionalData } = body as {
deviceId: string;
additionalData?: string;
};
// Validate required parameters
if (!deviceId) {
return NextResponse.json(
{ error: "deviceId is required" },
{ status: 400 }
);
}
// Get the current timestamp (Unix seconds)
const timestamp = nowUnixSeconds();
// Create the HMAC-SHA256 signature using your app secret
// This ensures the request cannot be forged
const signature = signRequest({
appHashHex,
deviceId,
appId: process.env.NEXT_PUBLIC_UTILSIO_APP_ID!,
timestamp,
additionalData,
});
// Return the signature and timestamp to the client
// The client uses these to authenticate with utilsio
return NextResponse.json({
signature,
timestamp: String(timestamp),
});
} catch (error) {
console.error("Sign request error:", error);
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "Failed to sign request",
},
{ status: 500 }
);
}
}
Whatâs happening here:
- The client sends a
deviceId from the browser (generated by the SDK)
- Your server derives a hash from your secret and salt using scrypt KDF
- Your server signs the request with HMAC-SHA256 using the derived hash
- The signature and timestamp are sent back to the client
- The client uses the signature to authenticate with utilsioâs API
This ensures your app secret never leaves your server.
Step 4: Wrap Your App with UtilsioProvider
The UtilsioProvider initializes the SDK and makes utilsio functionality available to your components. It creates a hidden iframe that handles authentication securely via postMessage.
Create or update src/app/page.tsx:
"use client";
import { UtilsioProvider, useUtilsio } from "@utilsio/react/client";
import { useCallback, useState } from "react";
// This function calls your signing route
// It's called automatically by the SDK when needed
async function getAuthHeaders({
deviceId,
additionalData,
}: {
deviceId: string;
additionalData?: string;
}) {
const response = await fetch("/api/sign", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ deviceId, additionalData }),
});
if (!response.ok) {
throw new Error(`Failed to get auth headers: ${response.statusText}`);
}
return response.json();
}
function SubscribeButton() {
const {
loading,
user,
currentSubscription,
error,
redirectToConfirm,
cancelSubscription,
} = useUtilsio();
const [cancelError, setCancelError] = useState<string | null>(null);
const [cancelling, setCancelling] = useState(false);
// Handle subscription initiation
// This redirects to utilsio's confirmation page
const handleSubscribe = useCallback(() => {
const appId = process.env.NEXT_PUBLIC_UTILSIO_APP_ID!;
const appUrl = process.env.NEXT_PUBLIC_APP_URL!;
// Redirect to utilsio confirmation page
redirectToConfirm({
appId,
appName: "Demo App",
amountPerDay: "0.033333", // ~1 POL per month
appUrl,
nextSuccess: `${appUrl}/success`, // Where to redirect after success
nextCancelled: `${appUrl}/cancelled`, // Where to redirect if user cancels
});
}, [redirectToConfirm]);
// Handle subscription cancellation
const handleCancel = useCallback(async () => {
if (!currentSubscription) return;
if (!confirm("Are you sure you want to cancel your subscription?")) return;
setCancelError(null);
setCancelling(true);
try {
// Cancel the subscription using utilsio's API
// The SDK automatically refreshes state after cancellation
await cancelSubscription([currentSubscription.id]);
} catch (err) {
setCancelError(err instanceof Error ? err.message : String(err));
} finally {
setCancelling(false);
}
}, [currentSubscription, cancelSubscription]);
// Show loading state while SDK initializes
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="mb-4 text-lg">Loading...</div>
</div>
</div>
);
}
// Show error state if something went wrong
if (error) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="mb-4 text-lg text-red-500">Error: {error}</div>
</div>
</div>
);
}
// Show login prompt if user is not authenticated
// The hidden iframe retrieves the Supabase session
if (!user) {
const utilsioUrl = process.env.NEXT_PUBLIC_UTILSIO_APP_URL!;
const loginUrl = `${utilsioUrl}/auth?next=${encodeURIComponent(appUrl)}`;
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="mb-4 text-2xl font-bold">Please Log In</h1>
<p className="mb-4 text-gray-600">
You need to be logged in to utilsio to subscribe.
</p>
<a
href={loginUrl}
className="inline-block px-6 py-3 text-white bg-blue-600 rounded-lg hover:bg-blue-700"
>
Log In to utilsio
</a>
</div>
</div>
);
}
// Show active subscription
if (currentSubscription) {
const amountPerDay = parseFloat(currentSubscription.amountPerDay);
const amountPerMonth = (amountPerDay * 30).toFixed(6);
const startDate = new Date(currentSubscription.createdAt).toLocaleDateString();
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center max-w-md">
<h1 className="mb-6 text-2xl font-bold">Active Subscription</h1>
{user && (
<div className="mb-6 p-4 border border-white rounded-lg text-left space-y-2">
<h2 className="font-semibold text-lg mb-2">User Info</h2>
<div className="text-sm">
<span className="font-semibold">Email:</span> {user.email || "N/A"}
</div>
{user.phone && (
<div className="text-sm">
<span className="font-semibold">Phone:</span> {user.phone}
</div>
)}
<div className="text-sm">
<span className="font-semibold">User ID:</span> {user.id}...
</div>
</div>
)}
<div className="mb-6 p-4 border border-white rounded-lg text-left space-y-2">
<h2 className="font-semibold text-lg mb-2">Subscription</h2>
<div>
<span className="font-semibold">Per day:</span> {currentSubscription.amountPerDay} POL
</div>
<div>
<span className="font-semibold">Per month:</span> ~{amountPerMonth} POL
</div>
<div>
<span className="font-semibold">Started:</span> {startDate}
</div>
</div>
<button
onClick={handleCancel}
disabled={cancelling}
className="w-full px-6 py-3 text-white bg-red-600 rounded-lg hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{cancelling ? "Cancelling..." : "Cancel Subscription"}
</button>
{cancelError && (
<p className="mt-3 text-sm text-red-600">{cancelError}</p>
)}
</div>
</div>
);
}
// Show subscribe button (no active subscription)
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center max-w-md">
<h1 className="mb-6 text-3xl font-bold">utilsio React SDK Demo</h1>
{user && (
<div className="mb-6 p-4 border border-white rounded-lg text-left space-y-2">
<h2 className="font-semibold text-lg mb-2">User Info</h2>
<div className="text-sm">
<span className="font-semibold">Email:</span> {user.email || "N/A"}
</div>
{user.phone && (
<div className="text-sm">
<span className="font-semibold">Phone:</span> {user.phone}
</div>
)}
<div className="text-sm">
<span className="font-semibold">User ID:</span> {user.id}...
</div>
</div>
)}
<p className="mb-8 text-gray-600">
Subscribe to this demo app for 1 POL per month
</p>
<button
onClick={handleSubscribe}
className="px-8 py-4 text-lg font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
>
Subscribe (1 POL/month)
</button>
</div>
</div>
);
}
export default function HomePage() {
return (
<UtilsioProvider
utilsioBaseUrl={process.env.NEXT_PUBLIC_UTILSIO_APP_URL!}
appId={process.env.NEXT_PUBLIC_UTILSIO_APP_ID!}
getAuthHeadersAction={getAuthHeaders}
>
<SubscribeButton />
</UtilsioProvider>
);
}
Key points:
"use client" directive enables client-side functionality
UtilsioProvider wraps your components and creates the hidden iframe
- The provider initializes with your app ID and signing endpoint
getAuthHeadersAction is called automatically when the SDK needs to authenticate
useUtilsio() provides access to user state, subscription data, and actions
- The provider handles device ID generation automatically via the hidden iframe
Step 5: Add Success and Cancelled Pages
Create src/app/success/page.tsx for successful subscriptions:
import Link from "next/link";
export default function SuccessPage() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="mb-4 text-3xl font-bold text-green-600">
Subscription Successful!
</h1>
<p className="mb-6 text-gray-600">
Your subscription has been activated successfully.
</p>
<Link
href="/"
className="inline-block px-6 py-3 text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
>
Back to Home
</Link>
</div>
</div>
);
}
Create src/app/cancelled/page.tsx for cancelled subscriptions:
src/app/cancelled/page.tsx
import Link from "next/link";
export default function CancelledPage() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="mb-4 text-3xl font-bold text-orange-600">
Subscription Cancelled
</h1>
<p className="mb-6 text-gray-600">
You cancelled the subscription process.
</p>
<Link
href="/"
className="inline-block px-6 py-3 text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
>
Back to Home
</Link>
</div>
</div>
);
}
Step 6: Run Your App
# Start development server
npm run dev
# Your app is now available at http://localhost:3000
# (or whatever port Next.js assigns)
Visit http://localhost:3000 and test the subscription flow.
How It All Works Together
The Hidden Iframe
The UtilsioProvider creates a hidden iframe that never gets displayed. This iframe:
- Loads from
https://utilsio.dev/embed (configurable via utilsioBaseUrl)
- Is positioned off-screen (0x0 pixels, opacity 0)
- Has
tabIndex={-1} and aria-hidden="true" for accessibility
- Communicates with your app via
window.postMessage()
The iframe handles secure authentication by:
- Checking for an existing Supabase session (via cookies)
- Generating a unique device ID (stored in a cookie on the utilsio origin)
- Sending the authenticated user and device ID back to your app via postMessage
- Your app stores these values in React state via
useUtilsio()
The Request Signing Flow
When your app needs to make a request to utilsio (fetching subscriptions, canceling, etc.):
âââââââââââââââââââââââââââââââââââââââââââ
â 1. useUtilsio() calls refreshSubscriptionâ
â or redirectToConfirm() â
ââââââââââââââââââââŹâââââââââââââââââââââââ
â
â calls getAuthHeadersAction()
âź
âââââââââââââââââââââââââââââââââââââââââââ
â 2. Client POSTs to /api/sign â
â with deviceId and additionalData â
ââââââââââââââââââââŹâââââââââââââââââââââââ
â
âź
âââââââââââââââââââââââââââââââââââââââââââ
â 3. Backend signs with HMAC-SHA256 â
â using app secret (never sent to client)
ââââââââââââââââââââŹâââââââââââââââââââââââ
â
â returns signature + timestamp
âź
âââââââââââââââââââââââââââââââââââââââââââ
â 4. Client sends signature + timestamp â
â to utilsio API â
ââââââââââââââââââââŹâââââââââââââââââââââââ
â
â utilsio verifies signature
âź
âââââââââââââââââââââââââââââââââââââââââââ
â 5. Request proceeds if signature valid â
âââââââââââââââââââââââââââââââââââââââââââ
This ensures that:
- Your app secret is never exposed to the browser
- Requests cannot be forged by a malicious user
- Device ID is tied to authenticated user sessions
Common Patterns
Checking Subscription Status
const { currentSubscription, user } = useUtilsio();
if (currentSubscription) {
// User has an active subscription
console.log(`Subscribed for ${currentSubscription.amountPerDay} POL/day`);
}
Protecting Premium Features
function PremiumFeature() {
const { currentSubscription, loading } = useUtilsio();
if (loading) return <div>Loading...</div>;
if (!currentSubscription) {
return (
<div>
<p>This feature requires an active subscription</p>
<SubscribeButton />
</div>
);
}
return <YourPremiumContent />;
}
Custom Pricing
The amountPerDay parameter accepts any value. Calculate your pricing as needed:
// $10/month = 0.333333 per day (10 / 30)
let amountPerDay = (10 / 30).toFixed(6);
// 100 POL/month = 3.333333 per day
amountPerDay = (100 / 30).toFixed(6);
// Or dynamic pricing based on user
const planPrice = getPriceForUserPlan(user.id);
amountPerDay = (planPrice / 30).toFixed(6);
redirectToConfirm({
amountPerDay,
appName: "My App",
// ... other params
});
Handling Subscription Amount Display
Remember that amountPerDay uses a 30-day month approximation:
if (currentSubscription) {
const dailyAmount = parseFloat(currentSubscription.amountPerDay);
const monthlyAmount = (dailyAmount * 30).toFixed(6);
const yearlyAmount = (dailyAmount * 365).toFixed(6);
return (
<div>
<p>${monthlyAmount}/month</p>
<p>${yearlyAmount}/year (approximate)</p>
</div>
);
}
Redirect Flow Explained
Step 1: User Clicks Subscribe
User clicks âSubscribeâ button â handleSubscribe() is called â redirectToConfirm() is invoked
Step 2: Build Confirmation URL
The SDK:
- Calls
getAuthHeadersAction() to get a signature
- Constructs a URL like:
https://utilsio.dev/confirm?appId=...&signature=...×tamp=...&amountPerDay=...&nextSuccess=...&nextCancelled=...
- Sets
window.location.href to redirect the user
Step 3: Confirmation Page
The user sees utilsioâs confirmation page with:
- App name and logo
- Subscription amount
- Confirmation button
Step 4: User Confirms or Cancels
- Confirms: utilsio processes the subscription on-chain via Superfluid, then redirects to
nextSuccess URL
- Cancels: User is redirected to
nextCancelled URL
Step 5: User Returns to Your App
Success page or home page is displayed. The hidden iframe automatically refreshes subscription state, so currentSubscription will be populated with the new subscription.
Troubleshooting
âError: deviceId is requiredâ from signing endpoint
Cause: Your client isnât sending a deviceId to the signing endpoint.
Fix: Make sure getAuthHeadersAction() is receiving a deviceId parameter. Check your getAuthHeaders() function is properly implemented.
âFailed to get subscriptionâ error
Cause: Your signing endpoint is failing, or the signature is invalid.
Fix:
- Verify your
UTILSIO_APP_SECRET and UTILSIO_APP_SALT are correct
- Check that
process.env.UTILSIO_APP_SECRET! doesnât have trailing whitespace
- Ensure your signing endpoint is receiving all required parameters
- Check browser DevTools Network tab to see what error the API is returning
User stays in âLoadingâŚâ state forever
Cause: The hidden iframe isnât loading properly, or Supabase session isnât available.
Fix:
- Check browser console for errors
- Verify
utilsioBaseUrl is correct (should be https://utilsio.dev in production)
- Confirm you have a valid Supabase session (check Application tab for cookies)
- Check that the iframe is actually being created in DevTools Inspector
âUser must be authenticated to subscribeâ when user is logged in
Cause: user or deviceId is null even though the user is authenticated.
Fix:
- Check that the hidden iframe loaded successfully
- Verify Supabase session exists (Application > Cookies)
- Try refreshing the page
- Check browser console for postMessage errors
Device ID is always null
Cause: The iframe isnât setting the device ID, or the hidden iframe isnât loading.
Fix:
- Open DevTools and check the page for an iframe element
- Check that the iframe
src is correct
- Look for CORS or network errors in the Network tab
- Try opening DevTools â Network tab and reload to see if the embed page loads successfully
Cross-origin errors with iframe
Cause: utilsioBaseUrl is on a different domain with improper CORS setup.
Fix:
- Ensure
utilsioBaseUrl matches where the embed page is actually hosted
- Use HTTPS in production (HTTPS-to-HTTP cross-origin requests are blocked)
- If you need to customize
parentOrigin, set it to your appâs origin
Signature mismatch errors
Cause: Your app secret or salt is incorrect, or youâre using the wrong values.
Fix:
- Verify credentials from https://utilsio.dev/creator/apps
- Check for leading/trailing whitespace in your
.env.local
- Ensure youâre using the correct secret and salt (not reversed)
- Test your signing endpoint in isolation with a POST to
/api/sign
Option 3: Use an AI Agent (5-15 minutes)
Use specialized AI tools to set up your project automatically:
With Cursor
Cursor has built-in support for utilsio docs. You can:
- Open Cursor and press
Cmd+K (Mac) or Ctrl+K (Windows/Linux)
- Type: âSet up utilsio in my Next.js app using the template from github.com/utilsio/templates/tree/main/nextjsâ
- Cursor will clone and configure everything for you
Learn more about using Cursor â
With Claude Code
You can send Claude Code a detailed prompt:
I want to integrate utilsio into my Next.js project. Use the template from
https://github.com/utilsio/templates/tree/main/nextjs as a reference and set
up the signing route, provider, and subscription flow in my app. I have these
environment variables ready: [paste your env vars]
Learn more about using Claude Code â
With Windsurf
Windsurfâs Cascade AI can handle multi-file setups:
- Describe what you want: âSet up utilsio subscriptions in my Next.js appâ
- Point it to the template: âhttps://github.com/utilsio/templates/tree/main/nextjsâ
- Let Cascade create all necessary files
Learn more about using Windsurf â
Done with Manual Setup!
You now have:
- â
Supabase authentication setup (via the hidden iframe)
- â
Secure request signing on the backend
- â
Subscription management with
redirectToConfirm
- â
Subscription cancellation
- â
Proper error handling and loading states
- â
Success and cancellation pages
Your subscription system is ready to use!
Next Steps