Skip to main content
The @utilsio/react/server package provides cryptographic signing functions for your backend. These functions prove to utilsio that requests come from your authorized application, enabling secure Superfluid subscription operations without exposing your app secret to the client.

How It Works: The Signing Flow

Understanding the authentication flow helps you implement signing correctly. Here’s the complete flow:
Why this is secure:
  • Your UTILSIO_APP_SECRET never leaves your server
  • Client can’t forge requests without the secret
  • Each request includes a fresh timestamp (prevents replay attacks)
  • HMAC-SHA256 ensures integrity of the request

Core Functions

deriveAppHashHex()

Derives a deterministic hex string from your app secret using scrypt key derivation. This is the HMAC key used for signing requests.
Why it exists: Scrypt is a memory-intensive KDF that prevents brute-force attacks on your secret. Deriving a key from your secret adds an extra security layer.
appSecret
string
required
Your app secret from the utilsio dashboard. This is sensitive and should never be exposed to the client.Keep this in UTILSIO_APP_SECRET environment variable on your backend.
salt
string
required
A hex-encoded salt string for key derivation. This is application-specific and stored in your utilsio dashboard.Keep this in UTILSIO_APP_SALT environment variable on your backend.
A 64-character hex string (256 bits, 32 bytes)
src/utilsio/sign.ts
import { deriveAppHashHex } from "@utilsio/react/server";

// Derive once at module initialization (expensive operation)
const appHashHex = deriveAppHashHex({
  appSecret: process.env.UTILSIO_APP_SECRET!,
  salt: process.env.UTILSIO_APP_SALT!,
});

// Use appHashHex for all signing operations
export function signAllRequests(deviceId: string) {
  const signature = signRequest({
    appHashHex,
    deviceId,
    appId: process.env.NEXT_PUBLIC_UTILSIO_APP_ID!,
    timestamp: nowUnixSeconds(),
  });
  return signature;
}

signRequest()

Creates an HMAC-SHA256 signature for a request. This signature is sent to utilsio to prove the request is authorized.
appHashHex
string
required
The derived app hash from deriveAppHashHex(). This is the HMAC key.
deviceId
string
required
The unique device identifier from the client. Identifies which device is making the request.This comes from the useUtilsio() hook’s deviceId property.
appId
string
required
Your utilsio App ID (public, safe to expose). This identifies your application to utilsio.
timestamp
number | string
required
Unix timestamp in seconds when the request is signed. Use nowUnixSeconds() to get the current time.This prevents replay attacks - signatures are only valid for a short time window.
additionalData
string
Optional additional data to include in the signature. Used to bind the signature to specific subscription operations.
  • For subscription creation: pass amountPerDay (in USD unit) to bind the signature to that amount
  • For subscription cancellation: pass comma-separated string containing userId and subscription IDs (sorted alphabetically)
  • For basic requests: omit this parameter
This ensures the client can’t change the operation while reusing a signature.
A 64-character hex string (HMAC-SHA256 digest)
app/actions.ts
import { signRequest } from "@utilsio/react/server";

// In your server action
const timestamp = Math.floor(Date.now() / 1000);
const signature = signRequest({
  appHashHex,
  deviceId: "user-device-123",
  appId: process.env.NEXT_PUBLIC_UTILSIO_APP_ID!,
  timestamp,
  additionalData: "0.033333", // Optional: for subscription creation
});

// signature = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2"

nowUnixSeconds()

Returns the current Unix timestamp in seconds. This is a utility function for generating the timestamp parameter for signing.
Ensures consistent time reference across requests. Using Date.now() directly could cause issues if you’re not dividing by 1000. Still, it’s up to you to use Date.now() or this function.
This function does not accept any parameters
Current Unix timestamp as an integer (seconds, not milliseconds)
import { nowUnixSeconds } from "@utilsio/react/server";

const timestamp = nowUnixSeconds();
// timestamp = 1705355234 (example)

// Equivalent to:
const manualTimestamp = Math.floor(Date.now() / 1000);
// manualTimestamp = 1705355234
Use in signing:
const signature = signRequest({
  appHashHex,
  deviceId,
  appId,
  timestamp: nowUnixSeconds(), // Always current time
  additionalData,
});

buildSignatureMessage()

Builds the message that gets signed. Useful for debugging or implementing custom signing logic.
deviceId
string
required
Device identifier
appId
string
required
Your app ID
timestamp
number | string
required
Unix timestamp in seconds
additionalData
string
Additional data to include in message
The message string that will be signed with HMAC-SHA256
import { buildSignatureMessage } from "@utilsio/react/server";

const message = buildSignatureMessage({
  deviceId: "abc123",
  appId: "my-app",
  timestamp: 1234567890,
  additionalData: "0.033333",
});

// message = "abc123-my-app-1234567890-0.033333"

// This message is then signed:
// signature = HMAC-SHA256(message, appHashHex)

Server Action Example

Next.js Server Actions provide a simpler and more secure way to sign requests compared to API routes. The server action pattern keeps your secret secure while being easier to use.
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) };
}
Usage in UtilsioProvider:
app/layout.tsx
import { UtilsioProvider } from "@utilsio/react/client";
import { getAuthHeadersAction } from "./actions";

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>
  );
}

Safari-Compatible Callback Endpoint

For Safari and browsers with strict third-party cookie blocking, you need a server-to-server callback endpoint that utilsio.dev can call to generate signatures.
Why this is needed: Safari blocks third-party cookies in iframes, preventing the SDK from reading deviceId from cookies. The callback endpoint allows utilsio.dev (running in first-party context) to request a signature from your server with the deviceId it can read.Handles both flows: This endpoint supports both subscription creation (where additionalData is amountPerDay) and cancellation (where additionalData is a sorted list containing userId and subscription IDs).
app/api/signature-callback/route.ts
import { NextRequest, NextResponse } from "next/server";
import { deriveAppHashHex, signRequest } from "@utilsio/react/server";

// 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") {
      console.error("Invalid origin header:", origin);
      return NextResponse.json({ error: "Unauthorized origin" }, { 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") {
      console.error("HTTPS required in production");
      return NextResponse.json({ error: "HTTPS required" }, { status: 403 });
    }

    // Parse and validate request body
    const body = await req.json();
    const { deviceId, appId, additionalData, timestamp } = body as {
      deviceId: string;
      appId: string;
      additionalData: string;
      timestamp: number;
    };

    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);
    const timestampAge = Math.abs(now - timestamp);
    if (timestampAge > 60) {
      console.error("Timestamp too old or in future:", { timestamp, now, age: timestampAge });
      return NextResponse.json({ error: "Invalid timestamp" }, { status: 400 });
    }

    // Verify appId matches our app
    const expectedAppId = process.env.NEXT_PUBLIC_UTILSIO_APP_ID!;
    if (appId !== expectedAppId) {
      console.error("AppId mismatch:", { expected: expectedAppId, received: appId });
      return NextResponse.json({ error: "Invalid appId" }, { status: 403 });
    }

    // Generate signature
    const signature = signRequest({
      appHashHex,
      deviceId,
      appId,
      timestamp,
      additionalData,
    });

    console.log("Generated signature for server-to-server callback:", {
      deviceId,
      appId,
      timestamp,
    });

    return NextResponse.json({
      signature,
      timestamp: String(timestamp),
    });
  } catch (error) {
    console.error("Signature callback error:", error);
    return NextResponse.json(
      { error: error instanceof Error ? error.message : "Failed to generate signature" },
      { status: 500 }
    );
  }
}
Security features:
  • Validates X-utilsio-Origin header to ensure requests come from utilsio.dev
  • Requires HTTPS in production
  • Validates timestamp is recent (within 60 seconds)
  • Verifies appId matches your application
This endpoint is automatically included in the Next.js template. If you’re using the template, you don’t need to create this file manually.

Environment Variables

When setting up your signing endpoint, you’ll need these environment variables:
UTILSIO_APP_SECRET=your-app-secret
UTILSIO_APP_SALT=your-app-salt
NEXT_PUBLIC_UTILSIO_APP_ID=your-app-id
The UTILSIO_APP_SECRET and UTILSIO_APP_SALT are sensitive and should NEVER be prefixed with NEXT_PUBLIC_. These should only be used on the backend in your signing endpoint. Only NEXT_PUBLIC_UTILSIO_APP_ID is safe to expose in the browser.
Last modified on February 17, 2026