Most chatbots need history to survive reloads. The pattern: a chat store (your database), chat ids from generateId, and useChat seeded with loaded messages.

Shape:

// app/chat/page.tsx: create a chat and redirect to /chat/[id]
import { redirect } from 'next/navigation';
import { createChat } from '@/util/chat-store';

export default async function Page() {
  const id = await createChat();
  redirect(`/chat/${id}`);
}

// chat store interface (back it with your database)
import { generateId } from 'ai';
export async function createChat(): Promise[string] {
  const id = generateId();
  await db.chats.insert({ id, messages: [] });
  return id;
}
export async function loadChat(id: string) { /* validate id, return messages */ }
export async function saveChat({ id, messages }) { /* upsert */ }

Client:

const { messages, sendMessage } = useChat({
  id: chatId,
  messages: initialMessages, // loaded server-side
  transport: new DefaultChatTransport({ api: '/api/chat' }),
});

Rules:
1. Treat chat ids as opaque tokens. Validate the format before using them in file paths or queries; the docs use a strict alphanumeric-plus-dash regex and a startsWith containment check for file stores.
2. Save on completion: hook the stream's onEnd (server) to persist the finished messages. Saving only on send loses the assistant's half of the turn.
3. On load, repair incomplete tool rounds. A persisted history with a dangling tool call throws MissingToolResultsError on the next send. Filter or complete the round at load time.
4. In v7, system-role messages in persisted history are rejected unless allowSystemInMessages is true. Prefer instructions at the top level; migrate old histories.
5. Pass the chat id to useChat so the resume machinery and your store agree on identity.
6. This guide intentionally skips auth. In production, scope every store operation to the authenticated user or chats leak across accounts.