Building a Privacy-First AI Chat — localStorage-Only History with No Server Logs
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.
| Storage | Privacy safety | Server cost | Multi-device sync | Typical use |
|---|---|---|---|---|
| Server DB | Needs leak protection and encryption | Storage / DB costs | Possible (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.
-
Safe read on mount To avoid hydration errors in SSR environments, read from
window.localStorageonly after the client has mounted (useEffectorDOMContentLoaded). -
Append and slice Append each user message and AI reply to the array, then
sliceit to the latest N messages (e.g. 50) before writing back. -
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
localStorageexists only on the client, causing errors or a screen flash. - Fix: Render with an empty array initially, then restore and re-render from
localStorageafter the browser has mounted (useEffectorDOMContentLoaded).
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 thecatchblock 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.