Skip to main content
Learn how to configure SonamuProvider, the starting point for Sonamu frontend integration. Configure authentication, file uploads, and internationalization in one place.

Setup Overview

Auth Integration

Connect UserService.useMeLogin/logout flow

File Upload

FileService integrationAuto-connect to useTypeForm

Internationalization

SD function providerType-safe translations

Global State

Context API basedAccess from all components

Basic Setup

1. Create SonamuProvider Config File

Create src/contexts/sonamu-provider.tsx in your project.
// src/contexts/sonamu-provider.tsx
import {
  SonamuProvider as BaseSonamuProvider,
  useSonamuBaseContext,
} from "@sonamu-kit/react-components";
import { type SonamuFile } from "@sonamu-kit/react-components";
import { type BetterAuthClientOptions } from "better-auth/client";
import { inferAdditionalFields } from "better-auth/client/plugins";
import { type ReactNode } from "react";

import { type MergedDictionary } from "@/i18n/sd.generated";
import { SD } from "@/i18n/sd.generated";
import { FileService } from "@/services/services.generated";

// Define better-auth client options
const authOptions = {
  plugins: [
    inferAdditionalFields({
      user: {
        role: { type: "string" },
      },
    }),
  ],
} satisfies BetterAuthClientOptions;

// Typed useSonamuContext
export function useSonamuContext() {
  return useSonamuBaseContext<MergedDictionary, typeof authOptions>();
}

export function SonamuProvider({ children }: { children: ReactNode }) {
  const uploadMutation = FileService.useUploadMutation();

  const uploader = async (files: File[]): Promise<SonamuFile[]> => {
    if (files.length === 0) {
      return [];
    }

    const result = await uploadMutation.mutateAsync({ files });
    return result.files;
  };

  return (
    <BaseSonamuProvider<MergedDictionary> authOptions={authOptions} uploader={uploader} SD={SD}>
      {children}
    </BaseSonamuProvider>
  );
}

2. Apply to App

Configure SonamuProvider in __root.tsx.
// src/routes/__root.tsx
import { QueryClientProvider } from "@tanstack/react-query";
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { SonamuProvider } from "@/contexts/sonamu-provider";

export const Route = createRootRouteWithContext()({
  component: RootComponent,
});

function RootComponent() {
  const { queryClient } = Route.useRouteContext();

  return (
    <QueryClientProvider client={queryClient}>
      <SonamuProvider>
        <Outlet />
      </SonamuProvider>
    </QueryClientProvider>
  );
}
Why create a separate SonamuProvider file?SonamuProvider uses React hooks like FileService.useUploadMutation(), so it must be placed under QueryClientProvider. Separating it into its own file allows you to co-locate authOptions, useSonamuContext, and type definitions.

Auth Configuration

Sonamu uses better-auth for authentication. Pass authOptions to SonamuProvider and a better-auth client is created internally.

Defining authOptions

Define better-auth client options. You can extend user fields through plugins.
import { type BetterAuthClientOptions } from "better-auth/client";
import { inferAdditionalFields } from "better-auth/client/plugins";

const authOptions = {
  plugins: [
    inferAdditionalFields({
      user: {
        role: { type: "string" },
        // Add additional fields as needed
      },
    }),
  ],
} satisfies BetterAuthClientOptions;
What is inferAdditionalFields?It adds project-specific fields (like role) to the client-side User type beyond better-auth defaults (id, name, email, etc.). These should match the user schema defined on the server.

Login Flow

Use the better-auth client’s signIn method.
import { useSonamuContext } from "@/contexts/sonamu-provider";

export function LoginPage() {
  const { auth } = useSonamuContext();
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");

  const handleSubmit = async () => {
    const result = await auth.signIn.email({ email, password });

    if (result.error) {
      alert(result.error.message);
      return;
    }
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  );
}

Logout Flow

const handleLogout = () => {
  auth.signOut();
};

Using Session

Access the auth client via useSonamuContext in components, and use auth.useSession() to query the current session.
import { useSonamuContext } from "@/contexts/sonamu-provider";

export function Header() {
  const { auth } = useSonamuContext();
  const session = auth.useSession();
  const user = session.data?.user ?? null;

  if (session.isPending) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <a href="/login">Login</a>;
  }

  return (
    <div>
      <span>Welcome, {user.name}</span>
      <button onClick={() => auth.signOut()}>Logout</button>
    </div>
  );
}

File Upload Configuration

Uploader Interface

type Uploader = (files: File[]) => Promise<SonamuFile[]>;

type SonamuFile = {
  name: string;
  url: string;
  mime_type: string;
  size: number;
};

FileService Integration

const uploadMutation = FileService.useUploadMutation();

const uploader_config = async (files: File[]): Promise<SonamuFile[]> => {
  if (files.length === 0) {
    return [];
  }

  const result = await uploadMutation.mutateAsync({ files });
  return result.files;
};
How it works:
  1. Upload files to backend with mutateAsync
  2. Return uploaded file info (SonamuFile[])
  3. useTypeForm automatically uses this uploader

Auto-integration with useTypeForm

When uploader is configured, useTypeForm’s submit automatically uploads files.
import { useTypeForm } from "@sonamu-kit/react-components";
import { FileInput } from "@sonamu-kit/react-components/components";

export function UserForm() {
  const { form, setForm, register, submit } = useTypeForm(UserSaveParams, {
    username: "",
    avatar: null,  // SonamuFile | null
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        submit(async (formData) => {
          // formData.avatar is already uploaded as SonamuFile
          await UserService.save({ params: formData });
        });
      }}
    >
      <Input {...register("username")} />

      {/* FileInput: File → SonamuFile conversion handled by submit */}
      <FileInput {...register("avatar")} />

      <button type="submit">Save</button>
    </form>
  );
}
Auto-upload MechanismThe submit function internally calls traverseAndUploadFiles to find and upload all File objects. Files in nested objects or arrays are automatically handled.

Custom Upload Logic

You can implement your own if using a different upload service.
// AWS S3 direct upload example
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const s3Client = new S3Client({ region: "ap-northeast-2" });

const uploader_config = async (files: File[]): Promise<SonamuFile[]> => {
  const uploadPromises = files.map(async (file) => {
    const key = `uploads/${Date.now()}-${file.name}`;

    await s3Client.send(
      new PutObjectCommand({
        Bucket: "my-bucket",
        Key: key,
        Body: file,
        ContentType: file.type,
      }),
    );

    return {
      name: file.name,
      url: `https://my-bucket.s3.amazonaws.com/${key}`,
      mime_type: file.type,
      size: file.size,
    };
  });

  return Promise.all(uploadPromises);
};

Internationalization (SD)

SD Function

The SD (Sonamu Dictionary) function provides type-safe translations.
const sd_config = <K extends DictKey>(key: K): ReturnType<typeof SD<K>> => SD(key);

Usage Example

import { useSonamuContext } from "@sonamu-kit/react-components";

export function WelcomeMessage() {
  const { SD } = useSonamuContext();

  return (
    <div>
      <h1>{SD("common.welcome")}</h1>
      <p>{SD("user.login.prompt")}</p>
    </div>
  );
}
Why provide SD through Context?While you can import and use it directly, using Context makes it easy to add runtime locale switching or dynamic dictionary loading in the future.

Type Safety

Specify Generic Type

Specifying the Dictionary type in BaseSonamuProvider makes the SD function type-safe.
import type { MergedDictionary } from "@/i18n/sd.generated";

<BaseSonamuProvider<MergedDictionary> authOptions={authOptions} uploader={uploader} SD={SD}>
  {children}
</BaseSonamuProvider>

Auto-completion

// Specify Dictionary and authOptions types in useSonamuBaseContext.
const { SD } = useSonamuBaseContext<MergedDictionary, typeof authOptions>();

SD("

    common.welcome
    common.save
    common.cancel
    user.login.failed
    user.logout.failed
    entity.User.email
    ...

Advanced Configuration

Minimal Setup (Auth Only)

You can omit uploader if file uploads are not needed.
export function SonamuProvider({ children }: { children: ReactNode }) {
  return (
    <BaseSonamuProvider<MergedDictionary> authOptions={authOptions} SD={SD}>
      {children}
    </BaseSonamuProvider>
  );
  // uploader omitted (fallback function auto-configured)
}
Omitting uploader will cause errors when using FileInput. Always configure it if file upload functionality is needed.

Using Without Auth

If authentication isn’t needed, authOptions can be omitted.
export function SonamuProvider({ children }: { children: ReactNode }) {
  return (
    <BaseSonamuProvider<MergedDictionary> SD={SD}>
      {children}
    </BaseSonamuProvider>
  );
}

Display Loading State

import { useSonamuContext } from "@/contexts/sonamu-provider";

export function App({ children }: { children: React.ReactNode }) {
  const { auth } = useSonamuContext();
  const session = auth.useSession();
  const user = session.data?.user ?? null;

  // Initial loading
  if (session.isPending) {
    return (
      <div className="flex items-center justify-center h-screen">
        <Spinner />
      </div>
    );
  }

  // Authentication required
  if (!user) {
    return <LoginPage />;
  }

  // Authenticated
  return children;
}

Customize Redirect

You can dynamically determine the redirect path after successful login.
const handleLogin = async (email: string, password: string) => {
  const result = await auth.signIn.email({ email, password });

  if (result.error) {
    alert(result.error.message);
    return;
  }

  // Navigate to different pages based on role
  const session = await auth.getSession();
  if (session.data?.user.role === "admin") {
    navigate({ to: "/admin", replace: true });
  } else {
    navigate({ to: "/dashboard", replace: true });
  }
};

Troubleshooting

”uploader is not configured” Error

Error: [SonamuProvider] uploader is not configured.
Please provide uploader configuration to SonamuProvider.
Solution: Pass uploader to SonamuProvider.

”auth is not configured” Error

Error: [SonamuProvider] auth is not configured.
Please provide auth configuration to SonamuProvider.
Solution: Pass authOptions to SonamuProvider.

Cannot Find QueryClient

Error: useQueryClient must be used within a QueryClientProvider
Solution: Place SonamuProvider inside QueryClientProvider.
<QueryClientProvider client={queryClient}>
  <SonamuProvider {...config}>
    {children}
  </SonamuProvider>
</QueryClientProvider>

Next Steps