Prerequisites
- A Next.js 13+ application with the App Router
- A utilsio account with credentials (you can get them at 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),
NEXT_PUBLIC_APP_URL is a reference for your app; changing this won’t change where your app is hosted
Then, you have 3 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 npm install
# Or pnpm install
# Copy environment variables
cp .env.example .env.local
# Edit .env.local with your utilsio credentials
nvim .env.local # Or just any editor your like
# Start the dev server
bun dev
# Server runs on http://localhost:3001
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.
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.
Step 3: Create Server Action and Safari Callback
3a. Create the signing server action
Create src/app/actions.ts:
"use server";
import { deriveAppHashHex, signRequest } from "@utilsio/react/server";
// Derive the HMAC key once at module load (expensive operation)
const appHashHex = deriveAppHashHex({
appSecret: process.env.UTILSIO_APP_SECRET!,
salt: process.env.UTILSIO_APP_SALT!,
});
export async function getAuthHeadersAction(input: {
deviceId: string;
additionalData?: string;
}) {
const timestamp = Math.floor(Date.now() / 1000);
const signature = signRequest({
appHashHex,
deviceId: input.deviceId,
appId: process.env.NEXT_PUBLIC_UTILSIO_APP_ID!,
timestamp,
additionalData: input.additionalData,
});
return { signature, timestamp: String(timestamp) };
}
3b. Create the Safari-compatible callback endpoint
For Safari and browsers that block third-party cookies in iframes, create src/app/api/signature-callback/route.ts:
src/app/api/signature-callback/route.ts
import { NextRequest, NextResponse } from "next/server";
import { deriveAppHashHex, signRequest } from "@utilsio/react/server";
const TIMESTAMP_VALIDITY_WINDOW_SECONDS = 60; // 60 seconds
// Derive the hash once at module load time
const appHashHex = deriveAppHashHex({
appSecret: process.env.UTILSIO_APP_SECRET!,
salt: process.env.UTILSIO_APP_SALT!,
});
export async function POST(req: NextRequest) {
try {
// Verify request origin
const origin = req.headers.get("X-utilsio-Origin");
if (origin !== "utilsio.dev") {
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
}
// Require HTTPS in production
const isProduction = process.env.NODE_ENV === "production";
const protocol = req.headers.get("x-forwarded-proto") || "http";
if (isProduction && protocol !== "https") {
return NextResponse.json({ error: "HTTPS required" }, { status: 403 });
}
// Parse request
const body = await req.json();
const { deviceId, appId, additionalData, timestamp } = body as {
deviceId: string;
appId: string;
additionalData: string;
timestamp: number;
};
// Validate required fields
if (!deviceId || !appId || !additionalData || !timestamp) {
return NextResponse.json(
{ error: "Missing required fields: deviceId, appId, additionalData, timestamp" },
{ status: 400 }
);
}
// Validate timestamp is recent (within 60 seconds)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > TIMESTAMP_VALIDITY_WINDOW_SECONDS) {
return NextResponse.json({ error: "Invalid timestamp" }, { status: 400 });
}
// Verify appId
if (appId !== process.env.NEXT_PUBLIC_UTILSIO_APP_ID!) {
return NextResponse.json({ error: "Invalid appId" }, { status: 403 });
}
// Generate signature
const signature = signRequest({
appHashHex,
deviceId,
appId,
timestamp,
additionalData,
});
return NextResponse.json({ signature, timestamp });
} catch (error) {
console.error("Signature callback error:", error);
return NextResponse.json(
{ error: "Failed to generate signature" },
{ status: 500 }
);
}
}
Safari Compatibility: Safari blocks third-party cookies in iframes, preventing the SDK from reading deviceId. This callback endpoint allows utilsio.dev (running in first-party context where it CAN read cookies) to request a signature from your server. The SDK automatically uses this fallback flow when deviceId is unavailable. The additionalData parameter works for both subscribe (amountPerDay) and cancel (sorted list containing userId and subscriptionIds) flows.
Step 4: Set Up UtilsioProvider in Your Layout
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/layout.tsx:
import { UtilsioProvider } from "@utilsio/react/client";
import { getAuthHeadersAction } from "./actions";
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<UtilsioProvider
utilsioBaseUrl={process.env.NEXT_PUBLIC_UTILSIO_APP_URL!}
appId={process.env.NEXT_PUBLIC_UTILSIO_APP_ID!}
getAuthHeadersAction={getAuthHeadersAction}
>
{children}
</UtilsioProvider>
</body>
</html>
);
}
Server Actions: The getAuthHeadersAction is a server action that generates signatures server-side. This keeps your app secret secure and works seamlessly with the SDK.
Step 5: Create Your First Page
Now create src/app/page.tsx to use the SDK:
"use client";
import { useUtilsio } from "@utilsio/react/client";
import { useCallback, useState } from "react";
function SubscribeButton() {
const {
loading,
user,
currentSubscription,
error,
redirectToConfirm,
cancelSubscription,
} = useUtilsio();
const [cancelError, setCancelError] = useState<string | null>(null);
const [cancelling, setCancelling] = useState(false);
const handleSubscribe = useCallback(() => {
const appId = process.env.NEXT_PUBLIC_UTILSIO_APP_ID!;
const appUrl = process.env.NEXT_PUBLIC_APP_URL!;
redirectToConfirm({
appId,
appName: "Demo App",
amountPerDay: "0.033333",
appUrl,
nextSuccess: `${appUrl}/success`,
nextCancelled: `${appUrl}/cancelled`,
});
}, [redirectToConfirm]);
const handleCancel = useCallback(async () => {
if (!currentSubscription) return;
if (!confirm("Are you sure you want to cancel your subscription?")) return;
setCancelError(null);
setCancelling(true);
try {
const appUrl = process.env.NEXT_PUBLIC_APP_URL!;
await cancelSubscription([currentSubscription.id], appUrl);
} catch (err) {
setCancelError(err instanceof Error ? err.message : String(err));
} finally {
setCancelling(false);
}
}, [currentSubscription, cancelSubscription]);
if (currentSubscription) {
return (
<div>
<div>Active Subscription: {currentSubscription.amountPerDay} USD/day</div>
<button onClick={handleCancel} disabled={cancelling}>
{cancelling ? "Cancelling..." : "Cancel Subscription"}
</button>
{cancelError && <p>{cancelError}</p>}
</div>
);
}
return (
<div>
<button onClick={handleSubscribe}>Subscribe (1 USD/month)</button>
</div>
);
}
export default function HomePage() {
return <SubscribeButton />;
}
Key points:
UtilsioProvider is now in layout.tsx and wraps your entire app
page.tsx components can directly use useUtilsio() without wrapping in the provider
getAuthHeadersAction is called automatically when the SDK needs to authenticate
useUtilsio() provides access to user state, subscription data, and actions
Safari & Privacy Extensions: The user object from useUtilsio() may be null even when users are logged into utilsio.dev. This happens in browsers with strict third-party cookie blocking (Safari, Brave) or privacy extensions.This is expected behavior. Don’t block your UI if user is null - just show your subscribe button anyway. When users click subscribe, they’ll be redirected to utilsio.dev which will handle authentication automatically. The user object is provided for convenience (e.g., displaying user email), not as a gate to functionality.
Step 5: Add Success and Cancelled Pages
Create src/app/success/page.tsx for successful subscriptions:
export default function SuccessPage() {
return "Subscription Successful";
}
Create src/app/cancelled/page.tsx for cancelled subscriptions:
src/app/cancelled/page.tsx
export default function CancelledPage() {
return "Subscription Cancelled";
}
Step 6: Run Your App
# Start development server
bun dev
Option 3: Use an AI Agent (5-15 minutes)
See instructions here: Using AI Agents →
Resources