Healthy state management also means secure state. This episode covers avoiding the persistence of tokens and secrets, choosing the right storage, sanitizing API data before it enters a store, and best practices for state classification, single responsibility per store, and role-based access control.

Stores hold an app's important data — including sensitive data. Common mistakes: storing tokens, storing entire API responses without sanitization, or persisting information that shouldn't survive. This episode changes those habits.
Episode 15 covers Pinia security and best practices: avoiding the persistence of secrets, choosing secure storage, sanitizing data before it enters a store, and the principles of state classification, single responsibility, and role-based access control.
The first rule: never persist tokens and secrets to localStorage. localStorage can be read by any JavaScript running on the page, making it vulnerable to XSS:
// DON'T: token stored in localStorage
persist: {
key: 'auth',
storage: localStorage,
}
// SAFER: token lives in memory (store without persist)
export const useAuthStore = defineStore('auth', {
state: () => ({ token: null as string | null }),
})Keep token only in state (memory) and give it a short lifetime via a refresh token on the server side. If persistent storage is truly needed, consider a server-controlled mechanism like an httpOnly cookie.
Not all data is dangerous — classify it first before deciding where to store it:
persist.persist: {
key: 'app-preferences',
storage: localStorage,
}persist: { storage: localStorage } is indeed the most convenient, but whether it's appropriate depends on the state's contents. Start with the question: what happens if this data is read by someone else using the same device?
API responses aren't always clean. Before data enters a store, validate it and pick only the fields you need:
actions: {
async fetchProfile() {
const res = await fetch('/api/profile').then((r) => r.json())
this.profile = {
id: String(res.id ?? ''),
name: typeof res.name === 'string' ? res.name : '',
role: res.role === 'admin' ? 'admin' : 'user',
}
},
}this.profile = {...} only stores the expected fields, with type validation and allowed values. This pattern prevents unexpected API data from polluting state and breaking rendering.
The less data in the store, the smaller the surface area for problems:
this.user = res.These small sanitization functions are an investment that pays off when the API changes its structure.
Separate state by its nature so its security treatment is clear:
This classification also helps determine what may be persisted and what must stay in memory.
Architecture best practices that intersect with security:
actions: {
async deleteUser(id: string) {
const auth = useAuthStore()
if (auth.user?.role !== 'admin') {
throw new Error('Forbidden')
}
await fetch(`/api/users/${id}`, { method: 'DELETE' })
},
}auth.user?.role !== 'admin' stops sensitive operations at the logic level, not just by hiding a button in the UI. Remember: client-side prevention doesn't replace server-side authorization — this is the first line of defense, not the only one.
Warning
All data on the client can be read by the user. Never put secrets like passwords or API keys in a store, no matter what form of persistence you use.
Episode 15 equips you with production-ready security and best practices. You now know how to avoid persisting secrets, choose secure storage, filter data from APIs, and apply state classification, single responsibility, and role-based access control.
Key takeaways:
In the next episode, episode 16, we'll discuss a TypeScript deep dive — typing stores that are inferred automatically, using generics in defineStore, typing plugins with PiniaCustomProperties, and type inference for Setup stores. Pinia will become your best TypeScript friend.