Skip to main content
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:
bun add @utilsio/react

Step 2: Set Up Environment Variables

Create a .env.local file in your project root with your utilsio credentials:
.env.local
# 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:
  1. Go to https://utilsio.dev/creator/apps
  2. Create or select your app
  3. Copy the App ID, App Secret, and App Salt
  4. 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:
  1. The client sends a deviceId from the browser (generated by the SDK)
  2. Your server derives a hash from your secret and salt using scrypt KDF
  3. Your server signs the request with HMAC-SHA256 using the derived hash
  4. The signature and timestamp are sent back to the client
  5. 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:
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:
src/app/success/page.tsx
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:
  1. Checking for an existing Supabase session (via cookies)
  2. Generating a unique device ID (stored in a cookie on the utilsio origin)
  3. Sending the authenticated user and device ID back to your app via postMessage
  4. 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:
  1. Calls getAuthHeadersAction() to get a signature
  2. Constructs a URL like: https://utilsio.dev/confirm?appId=...&signature=...&timestamp=...&amountPerDay=...&nextSuccess=...&nextCancelled=...
  3. 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:
  1. Verify your UTILSIO_APP_SECRET and UTILSIO_APP_SALT are correct
  2. Check that process.env.UTILSIO_APP_SECRET! doesn’t have trailing whitespace
  3. Ensure your signing endpoint is receiving all required parameters
  4. 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:
  1. Check browser console for errors
  2. Verify utilsioBaseUrl is correct (should be https://utilsio.dev in production)
  3. Confirm you have a valid Supabase session (check Application tab for cookies)
  4. 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:
  1. Check that the hidden iframe loaded successfully
  2. Verify Supabase session exists (Application > Cookies)
  3. Try refreshing the page
  4. 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:
  1. Open DevTools and check the page for an iframe element
  2. Check that the iframe src is correct
  3. Look for CORS or network errors in the Network tab
  4. 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:
  1. Ensure utilsioBaseUrl matches where the embed page is actually hosted
  2. Use HTTPS in production (HTTPS-to-HTTP cross-origin requests are blocked)
  3. 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:
  1. Verify credentials from https://utilsio.dev/creator/apps
  2. Check for leading/trailing whitespace in your .env.local
  3. Ensure you’re using the correct secret and salt (not reversed)
  4. 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:
  1. Open Cursor and press Cmd+K (Mac) or Ctrl+K (Windows/Linux)
  2. Type: “Set up utilsio in my Next.js app using the template from github.com/utilsio/templates/tree/main/nextjs”
  3. 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:
  1. Describe what you want: “Set up utilsio subscriptions in my Next.js app”
  2. Point it to the template: “https://github.com/utilsio/templates/tree/main/nextjs”
  3. 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

Last modified on January 15, 2026