> ## Documentation Index
> Fetch the complete documentation index at: https://utilsio.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Get utilsio running in your Next.js app. Choose your preferred setup method: clone template, manual setup, or use an AI agent.

## 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](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`),

    <Note>
      `NEXT_PUBLIC_APP_URL` is a reference for your app; changing this won't change where your app is hosted
    </Note>

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.

```bash theme={null}
# 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:

<CodeGroup>
  ```bash bun theme={null}
  bun add @utilsio/react
  ```

  ```bash npm theme={null}
  npm install @utilsio/react
  ```

  ```bash yarn theme={null}
  yarn add @utilsio/react
  ```

  ```bash pnpm theme={null}
  pnpm install @utilsio/react
  ```
</CodeGroup>

### Step 2: Set Up Environment Variables

Create a `.env.local` file in your project root with your utilsio credentials:

```env .env.local theme={null}
# 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
```

<Note>
  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.
</Note>

### Step 3: Create Server Action and Safari Callback

#### 3a. Create the signing server action

Create `src/app/actions.ts`:

```typescript src/app/actions.ts theme={null}
"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`:

```typescript src/app/api/signature-callback/route.ts theme={null}
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 }
    );
  }
}
```

<Note>
  **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.
</Note>

### 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`:

```typescript src/app/layout.tsx theme={null}
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>
  );
}
```

<Note>
  **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.
</Note>

### Step 5: Create Your First Page

Now create `src/app/page.tsx` to use the SDK:

```typescript src/app/page.tsx theme={null}
"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.01", // 0.01 USD/day (~0.30 USD/month)
      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 (0.01 USD/day)</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

<Warning>
  **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.
</Warning>

### Step 5: Add Success and Cancelled Pages

Create `src/app/success/page.tsx` for successful subscriptions:

```typescript src/app/success/page.tsx theme={null}
export default function SuccessPage() {
  return "Subscription Successful";
}
```

Create `src/app/cancelled/page.tsx` for cancelled subscriptions:

```typescript src/app/cancelled/page.tsx theme={null}
export default function CancelledPage() {
  return "Subscription Cancelled";
}
```

### Step 6: Run Your App

```bash theme={null}
# Start development server
bun dev
```

## Option 3: Use an AI Agent (5-15 minutes)

See instructions here: [Using AI Agents →](/guides#using-ai-agents)

## Resources

* **Learn the SDK:** [Client SDK Reference →](/sdks/react/client)
* **Understand security:** [Server SDK Reference →](/sdks/react/server)
* **Explore the API:** [API Reference →](/api-reference)
* **Clone the template:** [github.com/utilsio/templates](https://github.com/utilsio/templates)
