Building a Privacy-First AI Chat — localStorage-Only History with No Server Logs

AI privacy web-development JavaScript
The short version

By keeping conversation logs out of the server database and storing them only in the browser’s localStorage, you get a privacy-protecting AI chat with zero operational storage cost.

When you add an AI chat to a web service, how you handle user input text and conversation history is a serious privacy question.

Persisting all conversation logs in a server-side database creates leak risk, GDPR-style obligations, and growing DB storage costs. This article explains a design that keeps everything client-side with localStorage.

Server DB vs client (localStorage) storage

How the two storage strategies compare.

StoragePrivacy safetyServer costMulti-device syncTypical use
Server DBNeeds leak protection and encryptionStorage / DB costsPossible (requires login)Enterprise SaaS, paid AI tools
localStorage (client)Very high (no logs on the server)Zero (no DB)Not possible (device-bound)Indie dev, privacy-first tools

With this architecture, the developer physically cannot view or retain users’ private conversations — a real technical assurance that builds trust.

Three client-side implementation steps

How to safely manage conversation history in React / Astro / plain JavaScript.

  1. Safe read on mount To avoid hydration errors in SSR environments, read from window.localStorage only after the client has mounted (useEffect or DOMContentLoaded).

  2. Append and slice Append each user message and AI reply to the array, then slice it to the latest N messages (e.g. 50) before writing back.

  3. Explicit clear-history UI Add a clear button that runs localStorage.removeItem() in one tap so users can voluntarily start over.

Minimal code example (history hook/helpers)

const STORAGE_KEY = 'ai_chat_history_v1';
const MAX_HISTORY_COUNT = 30; // cap to prevent quota overflow

// Load history
export function loadChatHistory() {
  if (typeof window === 'undefined') return [];
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    return raw ? JSON.parse(raw) : [];
  } catch (e) {
    console.error('Failed to load history:', e);
    return [];
  }
}

// Save history
export function saveChatHistory(messages: Array<{ role: string; content: string }>) {
  if (typeof window === 'undefined') return;
  try {
    // Keep only the latest 30 to avoid QuotaExceededError
    const trimmed = messages.slice(-MAX_HISTORY_COUNT);
    localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed));
  } catch (e) {
    console.error('localStorage save error:', e);
  }
}

// Clear history
export function clearChatHistory() {
  if (typeof window === 'undefined') return;
  localStorage.removeItem(STORAGE_KEY);
}

Failure boundary — real incidents and fixes

Common problems when running on localStorage, and how to solve them.

1. SSR hydration mismatch

  • Symptom: In SSR frameworks (Astro, Next.js), the server-rendered HTML differs from the browser’s first paint because localStorage exists only on the client, causing errors or a screen flash.
  • Fix: Render with an empty array initially, then restore and re-render from localStorage after the browser has mounted (useEffect or DOMContentLoaded).

2. QuotaExceededError

  • Symptom: Long conversations saved over months exceed the browser’s localStorage limit (~5MB) and throw DOMException: QuotaExceededError.
  • Fix: Always apply a count cap (e.g. .slice(-30)) on save, and in the catch block delete old messages and retry the save as automatic recovery.

The Kaga chat on this site follows exactly this design: all conversation history lives only in the user’s browser localStorage, with no database or conversation logs stored server-side.