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

# Frontend with Backend

> Integrate a Next.js application with Sonar, using server-side token management for improved security.

<Card title="Example Repository" icon="github" href="https://github.com/sunrisedotdev/sonar-example-nextjs">
  A complete Next.js implementation showing this integration approach.
</Card>

This guide covers integrating Sonar with a backend that stores OAuth tokens server-side.
All Sonar API calls are made via server actions, so tokens never reach the browser.

<Note>
  For SPAs without a backend, see the simpler [Frontend-only](/sonar/integration-guides/frontend-only) approach.
</Note>

## Backend access model

<Warning>
  There is no server-side, shared, or dev API key. Every entity-scoped Sonar API call is authenticated with the end user's OAuth access token. This is the single most common misunderstanding when building a backend against Sonar.
</Warning>

Entity-scoped data can only be read with the token of the user it belongs to. This includes:

* Registration and KYC status
* Eligibility and pre-purchase checks
* Entity information
* Generating purchase permits

Because there is no shared key, your backend must persist each user's OAuth token and refresh it when it expires. See [Token storage](#token-storage) for persistence and [Defining server actions](#defining-server-actions) for the refresh flow (the [example app](https://github.com/sunrisedotdev/sonar-example-nextjs) demonstrates refreshing expired tokens).

### What needs a token, and what doesn't

Committed amounts live on the sale contract and can be read directly from the chain without any token. See [Reading commitment data](/sonar/integration-guides/reading-commitment-data).

Registration, KYC, and eligibility status are only available through the authenticated Sonar API, so they must be fetched per user with that user's (refreshed) token. See [Authentication](/sonar/core-features/authentication) for how tokens are obtained.

## How it works

```mermaid theme={null}
sequenceDiagram
    participant U as User
    participant B as Browser
    participant Y as Your Server
    participant E as Echo OAuth
    participant S as Sonar API
    participant C as Sale Contract

    Note over U,E: OAuth Authentication
    U->>B: Click "Connect with Sonar"
    B->>Y: Server Action: getSonarAuthorizationUrl()
    Y->>Y: Generate PKCE params
    Y->>B: Return redirect URL
    B->>E: Navigate to OAuth
    E->>E: User authenticates
    E->>B: Redirect with code
    B->>Y: Server Action: handleSonarCallback(code, state)
    Y->>E: Exchange code for tokens
    E->>Y: Return tokens
    Y->>Y: Store tokens server-side
    Y->>B: Success

    Note over U,S: Purchase Flow (via Server Actions)
    U->>B: Click "Purchase"
    B->>Y: Server Action: generatePurchasePermit()
    Y->>Y: Refresh token if needed
    Y->>S: POST /purchase-permit
    S->>Y: Return signed permit
    Y->>B: Return permit

    Note over U,C: Contract Interaction
    B->>C: purchase(amount, permit, signature)
    C->>C: Verify permit signature
    C->>C: Execute purchase
    C->>B: Transaction receipt
```

## Installation

```bash theme={null}
npm install @echoxyz/sonar-core
```

Note: Unlike the frontend-only approach, you don't need `@echoxyz/sonar-react` since you'll be making API calls through your backend.

## Example storage interfaces

### Token storage

You'll need a way to persist OAuth tokens on the server, keyed by user ID.

In production, you should use a database. For demonstration purposes, the example below uses in-memory storage.

```typescript theme={null}
// lib/token-store.ts

export interface SonarTokens {
  accessToken: string;
  refreshToken: string;
  expiresAt: number; // Unix timestamp in seconds
}

/**
 * In-memory token store implementation.
 * Tokens are stored in a Map and will be lost on server restart.
 * This can be easily swapped for a database-backed implementation.
 */
class InMemoryTokenStore {
  private tokens: Map<string, SonarTokens> = new Map();

  setTokens(userId: string, tokens: SonarTokens): void {
    this.tokens.set(userId, tokens);
  }

  getTokens(userId: string): SonarTokens | null {
    return this.tokens.get(userId) || null;
  }

  clearTokens(userId: string): void {
    this.tokens.delete(userId);
  }
}

// Singleton instance
let tokenStoreInstance: InMemoryTokenStore | null = null;

export function getTokenStore(): InMemoryTokenStore {
  if (!tokenStoreInstance) {
    tokenStoreInstance = new InMemoryTokenStore();
  }
  return tokenStoreInstance;
}
```

See the example app [token-store.ts](https://github.com/sunrisedotdev/sonar-example-nextjs/blob/main/src/lib/token-store.ts) for the full implementation.

### PKCE verifier storage

During the OAuth flow, you need to store the PKCE code verifier between the initial redirect and the callback.
This must be stored server-side and associated with the OAuth state parameter, along with the user ID to verify session consistency.

```typescript theme={null}
// lib/pkce-store.ts

interface PKCEEntry {
  userId: string;
  codeVerifier: string;
}

// WARNING: In-memory storage is for demonstration only.
// In production, use a database or session storage.
const pkceStore = new Map<string, PKCEEntry>();

// Store the verifier and user ID when starting the OAuth flow
export function setPKCEVerifier(state: string, userId: string, codeVerifier: string): void {
  pkceStore.set(state, { userId, codeVerifier });
}

// Retrieve the verifier when completing the OAuth flow
export function getPKCEVerifier(state: string): PKCEEntry | null {
  return pkceStore.get(state) || null;
}

// Clear the verifier after successful token exchange
export function clearPKCEVerifier(state: string): void {
  pkceStore.delete(state);
}
```

See the example app [pkce-store.ts](https://github.com/sunrisedotdev/sonar-example-nextjs/blob/main/src/lib/pkce-store.ts) for the full implementation with expiry handling.

## Implementing the OAuth flow

The OAuth flow uses two server actions: one to generate the authorization URL, and one to handle the callback.

<Note>
  The `createSonarClient` helper used below is defined in the [Server actions](#server-actions-for-sonar-api-requests) section.
</Note>

### Starting the OAuth flow

When the user clicks "Connect with Sonar", a server action generates the authorization URL with PKCE parameters:

```typescript theme={null}
// app/actions/auth.ts
"use server";

import { generatePKCEParams, buildAuthorizationUrl } from "@echoxyz/sonar-core";
import { getSession } from "@/lib/session";
import { setPKCEVerifier } from "@/lib/pkce-store";

/**
 * Generate Sonar OAuth authorization URL with PKCE
 */
export async function getSonarAuthorizationUrl(): Promise<string> {
  const session = await getSession();
  if (!session) {
    throw new Error("Unauthorized");
  }

  // Generate PKCE parameters (code verifier, challenge, and state)
  const { codeVerifier, codeChallenge, state } = await generatePKCEParams();

  // Store code verifier linked to state token (will be retrieved in callback)
  setPKCEVerifier(state, session.userId, codeVerifier);

  // Build the authorization URL
  const authorizationUrl = buildAuthorizationUrl({
    clientUUID: "YOUR_OAUTH_CLIENT_UUID",
    redirectURI: "YOUR_REDIRECT_URI",
    state,
    codeChallenge,
  });

  return authorizationUrl.toString();
}
```

### Handling the OAuth callback

After the user authenticates with Echo, they're redirected to a callback page. This page extracts the `code` and `state` parameters and calls a server action to complete the flow:

```typescript theme={null}
// app/actions/auth.ts (continued)

import { createSonarClient } from "@/lib/sonar";
import { getPKCEVerifier, clearPKCEVerifier } from "@/lib/pkce-store";
import { getTokenStore, SonarTokens } from "@/lib/token-store";

/**
 * Handle OAuth callback - exchange authorization code for tokens
 */
export async function handleSonarCallback(code: string, state: string): Promise<void> {
  const session = await getSession();
  if (!session) {
    throw new Error("Unauthorized: No active session");
  }

  // Retrieve the PKCE verifier using the state token
  const stateData = getPKCEVerifier(state);
  if (!stateData) {
    throw new Error("Invalid state: OAuth state token not found or expired");
  }

  // Verify the state belongs to the current session
  if (stateData.userId !== session.userId) {
    throw new Error("Invalid session: State token does not match current session");
  }

  // Exchange the authorization code for tokens
  const client = createSonarClient(session.userId);
  const tokenData = await client.exchangeAuthorizationCode({
    code,
    codeVerifier: stateData.codeVerifier,
    redirectURI: "YOUR_REDIRECT_URI",
  });

  // Store the tokens
  const expiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;
  const sonarTokens: SonarTokens = {
    accessToken: tokenData.access_token,
    refreshToken: tokenData.refresh_token,
    expiresAt,
  };
  getTokenStore().setTokens(session.userId, sonarTokens);

  // Clean up the PKCE verifier
  clearPKCEVerifier(state);
}

/**
 * Disconnect Sonar account (remove stored tokens)
 */
export async function disconnectSonar(): Promise<void> {
  const session = await getSession();
  if (!session) {
    throw new Error("Unauthorized");
  }
  getTokenStore().clearTokens(session.userId);
}
```

The callback page itself is a client component that calls the server action:

```tsx theme={null}
// app/oauth/callback/page.tsx
"use client";

import { Suspense, useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { handleSonarCallback } from "@/app/actions/auth";

function OAuthCallbackContent() {
  const searchParams = useSearchParams();
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const processCallback = async () => {
      const code = searchParams.get("code");
      const state = searchParams.get("state");
      const oauthError = searchParams.get("error");

      if (oauthError) {
        setError(`OAuth error: ${oauthError}`);
        return;
      }

      if (!code || !state) {
        setError("Missing authorization code or state");
        return;
      }

      try {
        await handleSonarCallback(code, state);
        // Success - redirect to home
        window.location.href = "/";
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to process OAuth callback");
      }
    };

    processCallback();
  }, [searchParams]);

  if (error) {
    return <p className="text-red-600">{error}</p>;
  }

  return <p>Connecting to Sonar...</p>;
}

export default function OAuthCallback() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <OAuthCallbackContent />
    </Suspense>
  );
}
```

See the example app [auth.ts](https://github.com/sunrisedotdev/sonar-example-nextjs/blob/main/src/app/actions/auth.ts) for the full implementation.

## Server actions for Sonar API requests

All Sonar API requests go through server actions so that access tokens are never exposed to the browser.

### Sonar client factory

Create helper functions to instantiate and refresh the Sonar client:

```typescript theme={null}
// lib/sonar.ts
import { getTokenStore, SonarTokens } from "@/lib/token-store";
import { createClient, SonarClient } from "@echoxyz/sonar-core";

/**
 * Create a SonarClient instance for a specific user.
 * Sets the access token from our server-side token store.
 */
export function createSonarClient(userId: string): SonarClient {
  const client = createClient({
    onUnauthorized: () => {
      getTokenStore().clearTokens(userId);
    },
  });

  const tokens = getTokenStore().getTokens(userId);
  if (tokens?.accessToken) {
    client.setToken(tokens.accessToken);
  }

  return client;
}
```

### Defining server actions

Create a `createSonarServerAction` wrapper that handles session authentication and automatic token refresh:

```typescript theme={null}
// lib/sonar.ts (continued)
import { getSession } from "@/lib/session";

type ServerActionHandler<I, O> = (client: SonarClient, input: I) => Promise<O>;

/**
 * Creates a Sonar server action with authentication and automatic token refresh.
 */
export function createSonarServerAction<I, O>(
  handler: ServerActionHandler<I, O>
): (input: I) => Promise<O> {
  return async (input: I) => {
    // Check session authentication
    const session = await getSession();
    if (!session) {
      throw new Error("Unauthorized");
    }

    // Get tokens from store
    let tokens = getTokenStore().getTokens(session.userId);
    if (!tokens) {
      throw new Error("Sonar account not connected");
    }

    // Check if token needs refresh (within 5 minutes of expiry)
    const now = Math.floor(Date.now() / 1000);
    if (tokens.expiresAt - now < 300) {
      tokens = await refreshSonarToken(session.userId, tokens.refreshToken);
      getTokenStore().setTokens(session.userId, tokens);
    }

    const client = createSonarClient(session.userId);
    return handler(client, input);
  };
}

/**
 * Refresh Sonar access token using refresh token.
 * NOTE: Consider how to prevent multiple concurrent refresh attempts for the
 * same user. The example app uses promise coalescing as one solution.
 */
export async function refreshSonarToken(userId: string, refreshToken: string): Promise<SonarTokens> {
  const client = createClient()
  const tokenData = await client.refreshToken({ refreshToken });
  const expiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;

  return {
    accessToken: tokenData.access_token,
    refreshToken: tokenData.refresh_token || refreshToken,
    expiresAt,
  };
}
```

With the wrapper in place, each server action is simple to define:

```typescript theme={null}
// app/actions/sonar.ts
"use server";

import { createSonarServerAction } from "@/lib/sonar";
import {
  ListAvailableEntitiesResponse,
  PrePurchaseCheckResponse,
  GeneratePurchasePermitResponse,
  APIError,
} from "@echoxyz/sonar-core";

type ListAvailableEntitiesInput = { saleUUID: string };

/**
 * Fetch all entities for the authenticated user
 */
export const getEntities = createSonarServerAction<ListAvailableEntitiesInput, ListAvailableEntitiesResponse>(
  async (client, { saleUUID }) => {
    if (!saleUUID) {
      throw new Error("Missing saleUUID");
    }
    return client.listAvailableEntities({ saleUUID });
  }
);

type PrePurchaseCheckInput = { saleUUID: string; entityID: string; walletAddress: string };

/**
 * Perform pre-purchase check for an entity
 */
export const prePurchaseCheck = createSonarServerAction<
  PrePurchaseCheckInput,
  PrePurchaseCheckResponse
>(async (client, { saleUUID, entityID, walletAddress }) => {
  if (!saleUUID || !entityID || !walletAddress) {
    throw new Error("Missing required parameters");
  }
  return client.prePurchaseCheck({ saleUUID, entityID, walletAddress });
});

type GeneratePurchasePermitInput = { saleUUID: string; entityID: string; walletAddress: string };

/**
 * Generate a purchase permit for an entity
 */
export const generatePurchasePermit = createSonarServerAction<
  GeneratePurchasePermitInput,
  GeneratePurchasePermitResponse
>(async (client, { saleUUID, entityID, walletAddress }) => {
  if (!saleUUID || !entityID || !walletAddress) {
    throw new Error("Missing required parameters");
  }
  return client.generatePurchasePermit({ saleUUID, entityID, walletAddress });
});
```

See the example app [sonar.ts](https://github.com/sunrisedotdev/sonar-example-nextjs/blob/main/src/app/actions/sonar.ts) for the full implementation.

## Frontend implementation

With the server actions in place, the client-side components can be implemented as follows:

### Authentication button

The auth button calls the `getSonarAuthorizationUrl` server action and redirects to the returned URL:

```tsx theme={null}
// components/SonarAuthButton.tsx
"use client";

import { getSonarAuthorizationUrl, disconnectSonar } from "@/app/actions/auth";

export function SonarAuthButton({ sonarConnected }: { sonarConnected: boolean }) {
  const handleConnect = async () => {
    try {
      const url = await getSonarAuthorizationUrl();
      window.location.href = url;
    } catch (error) {
      console.error("Failed to get Sonar authorization URL:", error);
    }
  };

  const handleDisconnect = async () => {
    try {
      await disconnectSonar();
      window.location.reload();
    } catch (error) {
      console.error("Failed to disconnect Sonar:", error);
    }
  };

  return (
    <button onClick={sonarConnected ? handleDisconnect : handleConnect}>
      {sonarConnected ? "Disconnect from Sonar" : "Connect with Sonar"}
    </button>
  );
}
```

### Fetching the state of a user's Sonar entities

For the other Sonar API calls, we recommend wrapping these in React hooks.
Then these hooks can be called in a very similar way to if you were calling the Sonar API directly via the `@echoxyz/sonar-react` library.
See [the frontend-only guide](/sonar/integration-guides/frontend-only#fetching-state-of-a-user’s-sonar-entities) for an example.

```typescript theme={null}
// hooks/use-sonar-entities.ts
"use client";

import { ListAvailableEntitiesResponse } from "@echoxyz/sonar-core";
import { saleUUID } from "@/lib/config";
import { useSonarQuery } from "./use-sonar-query";
import { getEntities } from "@/app/actions/sonar";

export function useSonarEntities() {
  const { loading, data, error } = useSonarQuery<{ saleUUID: string }, ListAvailableEntitiesResponse>(getEntities, {
    saleUUID,
  });

  return {
    loading,
    entities: data?.Entities,
    error,
  };
}
```

### Running pre-purchase checks and generating purchase permits

Also create a hook for the purchase flow:

```typescript theme={null}
// hooks/use-sonar-purchase.ts
"use client";

import { useCallback, useState, useEffect } from "react";
import { prePurchaseCheck, generatePurchasePermit } from "@/app/actions/sonar";
import { GeneratePurchasePermitResponse, PrePurchaseFailureReason } from "@echoxyz/sonar-core";

export function useSonarPurchase(args: {
  saleUUID: string;
  entityID: string;
  walletAddress: string;
}) {
  const [loading, setLoading] = useState(true);
  const [readyToPurchase, setReadyToPurchase] = useState(false);
  const [failureReason, setFailureReason] = useState<PrePurchaseFailureReason>();
  const [error, setError] = useState<Error>();

  useEffect(() => {
    const checkPurchase = async () => {
      setLoading(true);
      try {
        const result = await prePurchaseCheck(args);
        setReadyToPurchase(result.ReadyToPurchase);
        if (!result.ReadyToPurchase) {
          setFailureReason(result.FailureReason as PrePurchaseFailureReason);
        }
      } catch (err) {
        setError(err instanceof Error ? err : new Error(String(err)));
      } finally {
        setLoading(false);
      }
    };

    checkPurchase();
  }, [args.saleUUID, args.entityID, args.walletAddress]);

  const generatePermit = useCallback(async (): Promise<GeneratePurchasePermitResponse> => {
    return generatePurchasePermit(args);
  }, [args.saleUUID, args.entityID, args.walletAddress]);

  return { loading, readyToPurchase, failureReason, error, generatePurchasePermit: generatePermit };
}
```

### Submitting the purchase transaction

Once you have a purchase permit, submit it to your sale contract.
This is the same as the frontend-only approach since the contract interaction happens directly from the browser—see [the frontend-only guide](/sonar/integration-guides/frontend-only#generating-a-purchase-permit-to-send-to-the-sale-contract) for the full implementation.
