Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
277 lines
6.9 KiB
Markdown
277 lines
6.9 KiB
Markdown
---
|
|
title: Prevent Cross-Request State Pollution in SSR Applications
|
|
impact: CRITICAL
|
|
impactDescription: Singleton stores in SSR share state across all server requests, potentially leaking user data between requests
|
|
type: gotcha
|
|
tags: [vue3, ssr, state-management, pinia, vuex, security, server-side-rendering, nuxt]
|
|
---
|
|
|
|
# Prevent Cross-Request State Pollution in SSR Applications
|
|
|
|
**Impact: CRITICAL** - In Server-Side Rendering (SSR) applications, a singleton store pattern creates a single instance that is shared across all server requests. This means data from one user's request could leak into another user's response, causing serious security and data integrity issues.
|
|
|
|
This is one of the most critical gotchas in Vue state management that can have severe production consequences.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Never use a singleton store pattern in SSR applications
|
|
- [ ] Create a fresh store instance per request when using SSR
|
|
- [ ] Use Pinia which handles SSR state management correctly
|
|
- [ ] Test SSR state isolation with concurrent requests
|
|
- [ ] Review any global reactive state for SSR compatibility
|
|
|
|
## The Problem: Singleton State in SSR
|
|
|
|
```javascript
|
|
// store.js - DANGEROUS for SSR
|
|
import { reactive } from 'vue'
|
|
|
|
// This is a singleton - same instance for ALL requests
|
|
export const store = reactive({
|
|
user: null,
|
|
cart: [],
|
|
preferences: {}
|
|
})
|
|
```
|
|
|
|
**What happens in SSR:**
|
|
|
|
1. Request A comes in for User A
|
|
2. Server sets `store.user = userA`
|
|
3. Before response completes, Request B arrives for User B
|
|
4. Request B sees `store.user = userA` (User A's data leaked!)
|
|
5. Server sets `store.user = userB`
|
|
6. Request A's response might now contain User B's data
|
|
|
|
This creates unpredictable behavior and potential security vulnerabilities.
|
|
|
|
## Solution 1: Use Pinia (Recommended)
|
|
|
|
Pinia handles SSR correctly by creating fresh store instances per request:
|
|
|
|
```javascript
|
|
// stores/user.js
|
|
import { defineStore } from 'pinia'
|
|
|
|
export const useUserStore = defineStore('user', {
|
|
state: () => ({
|
|
user: null,
|
|
preferences: {}
|
|
}),
|
|
actions: {
|
|
setUser(user) {
|
|
this.user = user
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
```javascript
|
|
// main.js (or entry-server.js)
|
|
import { createPinia } from 'pinia'
|
|
import { createApp } from 'vue'
|
|
import App from './App.vue'
|
|
|
|
// For SSR: Create fresh instances per request
|
|
export function createAppInstance() {
|
|
const app = createApp(App)
|
|
const pinia = createPinia()
|
|
|
|
app.use(pinia)
|
|
|
|
return { app, pinia }
|
|
}
|
|
```
|
|
|
|
```javascript
|
|
// entry-server.js
|
|
import { createAppInstance } from './main'
|
|
import { renderToString } from 'vue/server-renderer'
|
|
|
|
export async function render(url, context) {
|
|
// Fresh app and store instance per request
|
|
const { app, pinia } = createAppInstance()
|
|
|
|
// ... setup router, fetch data, etc.
|
|
|
|
const html = await renderToString(app)
|
|
|
|
// Serialize state for client hydration
|
|
const state = pinia.state.value
|
|
|
|
return { html, state }
|
|
}
|
|
```
|
|
|
|
```javascript
|
|
// entry-client.js - Hydrate from serialized state
|
|
import { createAppInstance } from './main'
|
|
|
|
const { app, pinia } = createAppInstance()
|
|
|
|
// Restore server state before mounting
|
|
if (window.__PINIA_STATE__) {
|
|
pinia.state.value = window.__PINIA_STATE__
|
|
}
|
|
|
|
app.mount('#app')
|
|
```
|
|
|
|
## Solution 2: Factory Pattern for Hand-Rolled State
|
|
|
|
If not using Pinia, create a factory function:
|
|
|
|
```javascript
|
|
// store.js - SSR-safe with factory
|
|
import { reactive, readonly } from 'vue'
|
|
|
|
// Factory function creates fresh state per call
|
|
export function createStore() {
|
|
const state = reactive({
|
|
user: null,
|
|
cart: [],
|
|
preferences: {}
|
|
})
|
|
|
|
return {
|
|
state: readonly(state),
|
|
setUser(user) {
|
|
state.user = user
|
|
},
|
|
addToCart(item) {
|
|
state.cart.push(item)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
```javascript
|
|
// entry-server.js
|
|
import { createStore } from './store'
|
|
import { provide } from 'vue'
|
|
|
|
export async function render(url) {
|
|
const app = createApp(App)
|
|
|
|
// Fresh store instance for this request only
|
|
const store = createStore()
|
|
app.provide('store', store)
|
|
|
|
// ... render
|
|
}
|
|
```
|
|
|
|
## Solution 3: Context-Based State (Advanced)
|
|
|
|
For frameworks like Nuxt, use request context:
|
|
|
|
```javascript
|
|
// composables/useRequestState.js
|
|
import { useSSRContext } from 'vue'
|
|
|
|
export function useRequestState(key, initialValue) {
|
|
if (import.meta.env.SSR) {
|
|
const ctx = useSSRContext()
|
|
ctx.state = ctx.state || {}
|
|
|
|
if (!(key in ctx.state)) {
|
|
ctx.state[key] = initialValue()
|
|
}
|
|
|
|
return ctx.state[key]
|
|
}
|
|
|
|
// Client-side: use regular reactive state
|
|
return reactive(initialValue())
|
|
}
|
|
```
|
|
|
|
## Nuxt.js Handles This Automatically
|
|
|
|
In Nuxt 3, state isolation is handled automatically:
|
|
|
|
```javascript
|
|
// Nuxt automatically creates fresh Pinia instance per request
|
|
// You can use stores normally
|
|
|
|
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
const userStore = useUserStore()
|
|
await userStore.fetchUser()
|
|
})
|
|
```
|
|
|
|
## Testing for State Pollution
|
|
|
|
```javascript
|
|
// test/ssr-state-isolation.test.js
|
|
import { describe, it, expect } from 'vitest'
|
|
import { render } from './entry-server'
|
|
|
|
describe('SSR State Isolation', () => {
|
|
it('should not leak state between concurrent requests', async () => {
|
|
// Simulate concurrent requests
|
|
const [result1, result2] = await Promise.all([
|
|
render('/user/1', { userId: '1' }),
|
|
render('/user/2', { userId: '2' })
|
|
])
|
|
|
|
// Each should have their own user data
|
|
expect(result1.html).toContain('User 1')
|
|
expect(result2.html).toContain('User 2')
|
|
|
|
// State should not be mixed
|
|
expect(result1.html).not.toContain('User 2')
|
|
expect(result2.html).not.toContain('User 1')
|
|
})
|
|
})
|
|
```
|
|
|
|
```javascript
|
|
// Alternative: Test store isolation directly
|
|
import { createApp } from './app.js'
|
|
|
|
test('requests do not share state', async () => {
|
|
// Simulate two concurrent requests
|
|
const { app: app1, store: store1 } = createApp()
|
|
const { app: app2, store: store2 } = createApp()
|
|
|
|
store1.user = { id: 1, name: 'Alice' }
|
|
store2.user = { id: 2, name: 'Bob' }
|
|
|
|
// Each should have its own state
|
|
expect(store1.user.name).toBe('Alice')
|
|
expect(store2.user.name).toBe('Bob')
|
|
})
|
|
```
|
|
|
|
## Red Flags to Watch For
|
|
|
|
```javascript
|
|
// ANY module-level reactive state is dangerous in SSR
|
|
|
|
// BAD: Module-level reactive
|
|
export const globalUser = ref(null)
|
|
|
|
// BAD: Module-level reactive object
|
|
export const appState = reactive({})
|
|
|
|
// BAD: Shared Map/Set
|
|
export const cache = new Map()
|
|
|
|
// BAD: Even plain objects can be problematic
|
|
let requestCount = 0 // Shared across requests
|
|
```
|
|
|
|
## Why Pinia is Recommended for SSR
|
|
|
|
1. **Automatic request isolation** - Fresh store instances per request
|
|
2. **Built-in state serialization** - Easy hydration on client
|
|
3. **DevTools support** - Debug state on both server and client
|
|
4. **TypeScript support** - Type-safe state management
|
|
5. **Tested patterns** - Battle-tested SSR handling
|
|
|
|
## Reference
|
|
- [Vue.js State Management - SSR Considerations](https://vuejs.org/guide/scaling-up/state-management.html#ssr-considerations)
|
|
- [Pinia SSR Guide](https://pinia.vuejs.org/ssr/)
|
|
- [Vue SSR Guide](https://vuejs.org/guide/scaling-up/ssr.html)
|