662 lines
18 KiB
Markdown
662 lines
18 KiB
Markdown
# padhle — Implementation Plan
|
||
## Rate Limiting + Supabase Integration + Session Bug Fix
|
||
|
||
**Date:** 2026-08-19
|
||
**Total Estimated Time:** ~3 hours
|
||
**Status:** Ready to implement
|
||
|
||
---
|
||
|
||
## Issues to Address
|
||
|
||
### 🔴 Critical: Session Data Leak (User A ↔ User B)
|
||
**Symptoms:**
|
||
- User A logs in → chats with AI → logs out
|
||
- User B logs in → sidebar empty (correct)
|
||
- BUT User B sees User A's conversation on main screen ❌
|
||
|
||
**Root Cause:**
|
||
- Frontend `activeChat` state not cleared on sign-out
|
||
- In-memory backend store allows session ID guessing
|
||
- Stale `messages` state renders after User A logs out
|
||
|
||
---
|
||
|
||
### 🟡 Security: No Rate Limiting
|
||
- Auth endpoints (`/signup`, `/signin`) vulnerable to brute-force
|
||
- No per-IP throttling
|
||
|
||
---
|
||
|
||
### 🟠 Architecture: In-Memory Sessions
|
||
- Sessions lost on server restart
|
||
- Not tied to database — only scoped via cookies
|
||
- Need Supabase Postgres for persistence
|
||
|
||
---
|
||
|
||
## Implementation Phases
|
||
|
||
### **PHASE 1: Rate Limiting by IP (30 min)**
|
||
|
||
**Goal:** Prevent brute-force attacks on auth endpoints
|
||
|
||
#### 1.1 Install dependency
|
||
```bash
|
||
cd backend
|
||
npm install express-rate-limit
|
||
```
|
||
|
||
#### 1.2 Create middleware file
|
||
**File:** `backend/src/middleware/rateLimiter.js` (NEW)
|
||
|
||
```js
|
||
import rateLimit from 'express-rate-limit';
|
||
|
||
export const authLimiter = rateLimit({
|
||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||
max: 5, // 5 requests per window per IP
|
||
message: { error: 'Too many auth attempts. Try again later.' },
|
||
standardHeaders: false,
|
||
skip: (req) => process.env.NODE_ENV !== 'production' && req.ip === '::1',
|
||
});
|
||
|
||
export const chatLimiter = rateLimit({
|
||
windowMs: 60 * 1000, // 1 minute
|
||
max: 30, // 30 requests per minute
|
||
message: { error: 'Rate limit exceeded. Try again later.' },
|
||
standardHeaders: false,
|
||
});
|
||
|
||
export const sessionsLimiter = rateLimit({
|
||
windowMs: 60 * 1000, // 1 minute
|
||
max: 20, // 20 requests per minute
|
||
message: { error: 'Rate limit exceeded. Try again later.' },
|
||
standardHeaders: false,
|
||
});
|
||
```
|
||
|
||
#### 1.3 Apply to auth routes
|
||
**File:** `backend/src/routes/auth.js` (MODIFY top of file)
|
||
|
||
Add import:
|
||
```js
|
||
import { authLimiter } from '../middleware/rateLimiter.js';
|
||
```
|
||
|
||
Then apply to each route:
|
||
```js
|
||
router.post('/signup', authLimiter, async (req, res) => { ... });
|
||
router.post('/signin', authLimiter, async (req, res) => { ... });
|
||
router.post('/signout', authLimiter, async (req, res) => { ... });
|
||
```
|
||
|
||
#### 1.4 Apply to other routes
|
||
**File:** `backend/src/index.js` (MODIFY)
|
||
|
||
Add imports:
|
||
```js
|
||
import { chatLimiter, sessionsLimiter } from './middleware/rateLimiter.js';
|
||
```
|
||
|
||
Update middleware order:
|
||
```js
|
||
app.use('/api/chat', chatLimiter, supabaseAuth, chatRoutes);
|
||
app.use('/api/sessions', sessionsLimiter, supabaseAuth, sessionRoutes);
|
||
```
|
||
|
||
#### 1.5 Test
|
||
```bash
|
||
cd backend && npm start
|
||
|
||
# Test rate limiting (6th request should fail)
|
||
for i in {1..7}; do
|
||
curl -X POST http://localhost:3001/api/auth/signup \
|
||
-H 'Content-Type: application/json' \
|
||
-d "{\"email\": \"test$i@example.com\", \"password\": \"test\"}"
|
||
echo ""
|
||
done
|
||
# Expect 429 on 6th attempt
|
||
```
|
||
|
||
---
|
||
|
||
### **PHASE 2: Supabase Database Integration (90 min)**
|
||
|
||
**Goal:** Move sessions & messages from in-memory to Supabase Postgres with RLS
|
||
|
||
#### 2.1 Create database migration
|
||
**File:** `backend/supabase/migrations/{timestamp}_create_sessions_messages.sql` (NEW)
|
||
|
||
```sql
|
||
-- sessions table
|
||
CREATE TABLE public.sessions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||
grade TEXT,
|
||
subject TEXT,
|
||
chapter TEXT,
|
||
preview TEXT,
|
||
created_at TIMESTAMPTZ DEFAULT now(),
|
||
updated_at TIMESTAMPTZ DEFAULT now()
|
||
);
|
||
|
||
-- messages table
|
||
CREATE TABLE public.messages (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
session_id UUID NOT NULL REFERENCES public.sessions(id) ON DELETE CASCADE,
|
||
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
||
text TEXT NOT NULL,
|
||
created_at TIMESTAMPTZ DEFAULT now()
|
||
);
|
||
|
||
-- profiles table (for future use)
|
||
CREATE TABLE public.profiles (
|
||
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||
email TEXT NOT NULL,
|
||
display_name TEXT,
|
||
avatar_url TEXT,
|
||
created_at TIMESTAMPTZ DEFAULT now(),
|
||
updated_at TIMESTAMPTZ DEFAULT now()
|
||
);
|
||
|
||
-- Indexes
|
||
CREATE INDEX idx_sessions_user_id ON public.sessions(user_id);
|
||
CREATE INDEX idx_messages_session_id ON public.messages(session_id);
|
||
|
||
-- Enable RLS
|
||
ALTER TABLE public.sessions ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
||
|
||
-- RLS: sessions
|
||
CREATE POLICY "Users view own sessions"
|
||
ON public.sessions FOR SELECT
|
||
USING (auth.uid() = user_id);
|
||
|
||
CREATE POLICY "Users create sessions"
|
||
ON public.sessions FOR INSERT
|
||
WITH CHECK (auth.uid() = user_id);
|
||
|
||
CREATE POLICY "Users update own sessions"
|
||
ON public.sessions FOR UPDATE
|
||
USING (auth.uid() = user_id)
|
||
WITH CHECK (auth.uid() = user_id);
|
||
|
||
CREATE POLICY "Users delete own sessions"
|
||
ON public.sessions FOR DELETE
|
||
USING (auth.uid() = user_id);
|
||
|
||
-- RLS: messages
|
||
CREATE POLICY "Users view own messages"
|
||
ON public.messages FOR SELECT
|
||
USING (session_id IN (SELECT id FROM public.sessions WHERE user_id = auth.uid()));
|
||
|
||
CREATE POLICY "Users insert own messages"
|
||
ON public.messages FOR INSERT
|
||
WITH CHECK (session_id IN (SELECT id FROM public.sessions WHERE user_id = auth.uid()));
|
||
|
||
CREATE POLICY "Users delete own messages"
|
||
ON public.messages FOR DELETE
|
||
USING (session_id IN (SELECT id FROM public.sessions WHERE user_id = auth.uid()));
|
||
|
||
-- RLS: profiles
|
||
CREATE POLICY "Users view own profile"
|
||
ON public.profiles FOR SELECT
|
||
USING (auth.uid() = id);
|
||
|
||
CREATE POLICY "Users update own profile"
|
||
ON public.profiles FOR UPDATE
|
||
USING (auth.uid() = id)
|
||
WITH CHECK (auth.uid() = id);
|
||
```
|
||
|
||
#### 2.2 Apply migration
|
||
```bash
|
||
cd /path/to/padhle
|
||
npx supabase db push
|
||
```
|
||
|
||
Verify in Supabase Studio (http://localhost:54323):
|
||
- Tables exist: `sessions`, `messages`, `profiles`
|
||
- RLS enabled on all three
|
||
- Policies listed under each table
|
||
|
||
#### 2.3 Create database service
|
||
**File:** `backend/src/services/db.js` (NEW)
|
||
|
||
```js
|
||
import { createClient } from '@supabase/supabase-js';
|
||
import dotenv from 'dotenv';
|
||
|
||
dotenv.config();
|
||
|
||
const SUPABASE_URL = process.env.SUPABASE_URL;
|
||
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||
|
||
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
|
||
|
||
export async function createSession(userId, grade, subject, chapter) {
|
||
const { data, error } = await supabase
|
||
.from('sessions')
|
||
.insert({
|
||
user_id: userId,
|
||
grade: grade || 'General',
|
||
subject: subject || 'General',
|
||
chapter: chapter || 'General',
|
||
preview: 'New Chat',
|
||
})
|
||
.select()
|
||
.single();
|
||
|
||
if (error) throw new Error(`Failed to create session: ${error.message}`);
|
||
return data;
|
||
}
|
||
|
||
export async function getSession(sessionId) {
|
||
const { data, error } = await supabase
|
||
.from('sessions')
|
||
.select('*')
|
||
.eq('id', sessionId)
|
||
.single();
|
||
|
||
if (error && error.code === 'PGRST116') return null;
|
||
if (error) throw new Error(`Failed to get session: ${error.message}`);
|
||
return data;
|
||
}
|
||
|
||
export async function listSessions(userId) {
|
||
const { data, error } = await supabase
|
||
.from('sessions')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.order('updated_at', { ascending: false });
|
||
|
||
if (error) throw new Error(`Failed to list sessions: ${error.message}`);
|
||
return data || [];
|
||
}
|
||
|
||
export async function getMessages(sessionId) {
|
||
const { data, error } = await supabase
|
||
.from('messages')
|
||
.select('*')
|
||
.eq('session_id', sessionId)
|
||
.order('created_at', { ascending: true });
|
||
|
||
if (error) throw new Error(`Failed to get messages: ${error.message}`);
|
||
return data || [];
|
||
}
|
||
|
||
export async function addMessage(sessionId, role, text) {
|
||
const { data: messageData, error: msgError } = await supabase
|
||
.from('messages')
|
||
.insert({ session_id: sessionId, role, text })
|
||
.select()
|
||
.single();
|
||
|
||
if (msgError) throw new Error(`Failed to add message: ${msgError.message}`);
|
||
|
||
const { error: updateError } = await supabase
|
||
.from('sessions')
|
||
.update({
|
||
updated_at: new Date().toISOString(),
|
||
preview: role === 'user' ? text.substring(0, 60) + (text.length > 60 ? '...' : '') : undefined,
|
||
})
|
||
.eq('id', sessionId);
|
||
|
||
if (updateError) console.error('Failed to update session:', updateError);
|
||
return messageData;
|
||
}
|
||
|
||
export async function deleteSession(sessionId) {
|
||
const { error } = await supabase
|
||
.from('sessions')
|
||
.delete()
|
||
.eq('id', sessionId);
|
||
|
||
if (error) throw new Error(`Failed to delete session: ${error.message}`);
|
||
return true;
|
||
}
|
||
|
||
export async function clearMessages(sessionId) {
|
||
const { error } = await supabase
|
||
.from('messages')
|
||
.delete()
|
||
.eq('session_id', sessionId);
|
||
|
||
if (error) throw new Error(`Failed to clear messages: ${error.message}`);
|
||
return true;
|
||
}
|
||
```
|
||
|
||
#### 2.4 Install SDK
|
||
```bash
|
||
cd backend
|
||
npm install @supabase/supabase-js
|
||
```
|
||
|
||
#### 2.5 Update routes to use DB service
|
||
**File:** `backend/src/routes/chat.js` (MODIFY)
|
||
|
||
Replace top imports:
|
||
```js
|
||
// OLD
|
||
import { createSession, addMessage, getMessages, getSession } from "../stores/sessionStore.js";
|
||
|
||
// NEW
|
||
import { createSession, addMessage, getMessages, getSession } from "../services/db.js";
|
||
```
|
||
|
||
Update function calls (add `await`):
|
||
```js
|
||
// OLD: session = createSession(userId, grade, subject, chapter);
|
||
// NEW: session = await createSession(userId, grade, subject, chapter);
|
||
|
||
// OLD: addMessage(currentChatId, userMsg);
|
||
// NEW: await addMessage(currentChatId, userMsg.role, userMsg.text);
|
||
|
||
// OLD: getMessages(currentChatId)
|
||
// NEW: const messages = await getMessages(currentChatId);
|
||
|
||
// OLD: addMessage(currentChatId, assistantMsg);
|
||
// NEW: await addMessage(currentChatId, assistantMsg.role, assistantMsg.text);
|
||
```
|
||
|
||
Wrap in try-catch:
|
||
```js
|
||
try {
|
||
session = await createSession(...);
|
||
// ... rest of logic
|
||
} catch (err) {
|
||
return res.status(500).json({ error: 'Database error' });
|
||
}
|
||
```
|
||
|
||
**File:** `backend/src/routes/sessions.js` (MODIFY)
|
||
|
||
Same replacements as chat.js:
|
||
```js
|
||
import { listSessions, getSession, deleteSession, clearMessages, createSession } from "../services/db.js";
|
||
|
||
router.get('/', async (req, res) => {
|
||
try {
|
||
const sessions = await listSessions(req.user.uid);
|
||
res.json(sessions);
|
||
} catch (err) {
|
||
res.status(500).json({ error: 'Failed to load sessions' });
|
||
}
|
||
});
|
||
|
||
// ... same for POST, GET/:id, DELETE/:id, PATCH/:id/clear
|
||
```
|
||
|
||
#### 2.6 Update .env
|
||
**File:** `backend/.env` (ADD)
|
||
|
||
```
|
||
SUPABASE_SERVICE_ROLE_KEY=<rotated-local-dev-service-role-key>
|
||
```
|
||
|
||
(Get key from `npx supabase status` — never commit the real value; see `docs/security-hardening-plan.md` Phase 1)
|
||
|
||
#### 2.7 Test integration
|
||
```bash
|
||
cd backend && npm start
|
||
|
||
# Create a session
|
||
curl -X POST http://localhost:3001/api/sessions \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Cookie: padhle.token=<valid_jwt>' \
|
||
-d '{"grade": "Grade 10", "subject": "math", "chapter": "Chapter 1"}'
|
||
|
||
# Check Supabase Studio: http://localhost:54323
|
||
# → Tables → sessions → should see new row
|
||
```
|
||
|
||
---
|
||
|
||
### **PHASE 3: Fix Session Leak Bug (45 min)**
|
||
|
||
**Goal:** Ensure User A's data not visible to User B
|
||
|
||
#### 3.1 Fix frontend state cleanup
|
||
**File:** `frontend/src/App.jsx` (MODIFY)
|
||
|
||
Find `useEffect([isAuthenticated])` around line 85. Replace entire block:
|
||
|
||
```js
|
||
useEffect(() => {
|
||
if (!hasMountedRef.current) {
|
||
hasMountedRef.current = true;
|
||
} else if (isAuthenticated) {
|
||
// User just signed in after sign-out
|
||
setMessages([]);
|
||
setActiveChat(null); // CRITICAL: clear before loading
|
||
setActiveGrade(DEFAULTS.grade);
|
||
setActiveSubject(DEFAULTS.subject);
|
||
setActiveChapter(DEFAULTS.chapter);
|
||
setIsTyping(false);
|
||
setSelectorStep('grade');
|
||
loadSessions(); // Load fresh sessions for current user
|
||
} else {
|
||
// User just signed out
|
||
setMessages([]);
|
||
setActiveChat(null);
|
||
setActiveGrade(DEFAULTS.grade);
|
||
setActiveSubject(DEFAULTS.subject);
|
||
setActiveChapter(DEFAULTS.chapter);
|
||
setIsTyping(false);
|
||
setSelectorStep('grade');
|
||
setSessions([]);
|
||
}
|
||
}, [isAuthenticated]);
|
||
```
|
||
|
||
#### 3.2 Add safety check in ChatHistory
|
||
**File:** `frontend/src/components/chat-history/ChatHistory.jsx` (MODIFY at top of component)
|
||
|
||
Add safety check before rendering:
|
||
|
||
```js
|
||
function ChatHistory({ messages, sessions, activeChat, selectorStep, setMessages, setActiveChat, ...props }) {
|
||
// Safety: verify activeChat session still exists
|
||
const sessionExists = activeChat && sessions.some(s => s.id === activeChat);
|
||
|
||
if (selectorStep !== 'chat') {
|
||
return <SelectorFlow {...props} />;
|
||
}
|
||
|
||
if (!sessionExists && messages.length > 0) {
|
||
// Stale state detected — clear
|
||
setMessages([]);
|
||
setActiveChat(null);
|
||
return <WelcomeScreen />;
|
||
}
|
||
|
||
if (messages.length === 0) {
|
||
return <WelcomeScreen />;
|
||
}
|
||
|
||
return <MessageList messages={messages} />;
|
||
}
|
||
```
|
||
|
||
#### 3.3 Verify RLS on backend
|
||
**File:** `backend/src/routes/sessions.js` (VERIFY ownership check)
|
||
|
||
```js
|
||
router.get('/:id', async (req, res) => {
|
||
try {
|
||
const userId = req.user.uid;
|
||
const session = await getSession(req.params.id);
|
||
|
||
// Defense-in-depth: explicit ownership check
|
||
if (!session || session.user_id !== userId) {
|
||
return res.status(404).json({ error: 'Session not found' });
|
||
}
|
||
|
||
res.json(session);
|
||
} catch (err) {
|
||
res.status(500).json({ error: 'Failed to load session' });
|
||
}
|
||
});
|
||
```
|
||
|
||
#### 3.4 E2E Test
|
||
```
|
||
1. Open Window 1 (Firefox)
|
||
2. Sign in as User A
|
||
3. Send message → create session
|
||
4. Verify session in sidebar ✓
|
||
5. Sign out
|
||
6. Open Window 2 (Chrome) or incognito tab
|
||
7. Sign in as User B
|
||
8. Verify:
|
||
- Sidebar empty ✓
|
||
- Main screen shows welcome (NOT User A's chat) ✓
|
||
9. Send a message → create new session
|
||
10. Sign out User B
|
||
11. In Window 1: Sign back in as User A
|
||
12. Verify User A sees original session ✓
|
||
```
|
||
|
||
---
|
||
|
||
### **PHASE 4: Error Handling & Validation (30 min)**
|
||
|
||
#### 4.1 Sanitize AI provider errors
|
||
**File:** `backend/src/services/ai.js` (MODIFY error handlers)
|
||
|
||
In `streamAnthropic`:
|
||
```js
|
||
if (!response.ok) {
|
||
const errText = await response.text();
|
||
console.error('Anthropic error:', errText); // Log server-side only
|
||
onError('AI service encountered an error. Please try again.');
|
||
return '';
|
||
}
|
||
```
|
||
|
||
In `streamGoogle`:
|
||
```js
|
||
if (!response.ok) {
|
||
const errText = await response.text();
|
||
console.error('Google error:', errText); // Log server-side only
|
||
onError('AI service encountered an error. Please try again.');
|
||
return '';
|
||
}
|
||
```
|
||
|
||
#### 4.2 Fix Anthropic non-streaming endpoint
|
||
**File:** `backend/src/services/ai.js` (MODIFY line ~138)
|
||
|
||
```js
|
||
// OLD
|
||
const res = await fetch(ANTHROPIC_BASE_URL, {
|
||
|
||
// NEW
|
||
const res = await fetch(`${ANTHROPIC_BASE_URL}/messages`, {
|
||
```
|
||
|
||
#### 4.3 Validate metadata
|
||
**File:** `backend/src/routes/chat.js` (ADD at top)
|
||
|
||
```js
|
||
const VALID_GRADES = ['Grade 6', 'Grade 7', 'Grade 8', 'Grade 9', 'Grade 10', 'Grade 11', 'Grade 12', 'General', 'Choose standard'];
|
||
const VALID_SUBJECTS = ['math', 'science', 'history', 'language', 'english', 'geography', 'General', 'choose-subject'];
|
||
```
|
||
|
||
Then in POST handler:
|
||
```js
|
||
if (grade && !VALID_GRADES.includes(grade)) {
|
||
return res.status(400).json({ error: 'Invalid grade' });
|
||
}
|
||
if (subject && !VALID_SUBJECTS.includes(subject)) {
|
||
return res.status(400).json({ error: 'Invalid subject' });
|
||
}
|
||
```
|
||
|
||
#### 4.4 Test error paths
|
||
```bash
|
||
# Invalid grade
|
||
curl -X POST http://localhost:3001/api/chat \
|
||
-H 'Cookie: padhle.token=<jwt>' \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"text": "hello", "grade": "invalid"}'
|
||
# Expect 400
|
||
|
||
# Message too long
|
||
curl -X POST http://localhost:3001/api/chat \
|
||
-H 'Cookie: padhle.token=<jwt>' \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"text": "'$(printf 'x%.0s' {1..10001})'}'
|
||
# Expect 400
|
||
```
|
||
|
||
---
|
||
|
||
## Summary Table
|
||
|
||
| Phase | Task | Time | Dependencies |
|
||
|-------|------|------|--------------|
|
||
| 1.1 | Install `express-rate-limit` | 2 min | — |
|
||
| 1.2 | Create `rateLimiter.js` | 5 min | — |
|
||
| 1.3 | Apply to auth.js | 5 min | 1.2 |
|
||
| 1.4 | Apply to index.js | 5 min | 1.3 |
|
||
| 1.5 | Test rate limiting | 13 min | 1.1–1.4 |
|
||
| **Phase 1 Total** | **Rate Limiting** | **30 min** | — |
|
||
| 2.1 | Create SQL migration | 10 min | — |
|
||
| 2.2 | Apply migration | 5 min | 2.1 |
|
||
| 2.3 | Create `db.js` | 20 min | 2.2 |
|
||
| 2.4 | Install SDK | 3 min | — |
|
||
| 2.5 | Update chat.js & sessions.js | 20 min | 2.3, 2.4 |
|
||
| 2.6 | Update .env | 2 min | — |
|
||
| 2.7 | Test integration | 15 min | 2.1–2.6 |
|
||
| **Phase 2 Total** | **DB Integration** | **75 min** | Phase 1 (optional) |
|
||
| 3.1 | Fix App.jsx state cleanup | 10 min | — |
|
||
| 3.2 | Add ChatHistory safety check | 10 min | 3.1 |
|
||
| 3.3 | Verify RLS in sessions.js | 5 min | 2.7 |
|
||
| 3.4 | E2E test | 20 min | 3.1–3.3 |
|
||
| **Phase 3 Total** | **Fix Session Leak** | **45 min** | Phase 2 |
|
||
| 4.1 | Sanitize AI errors | 5 min | — |
|
||
| 4.2 | Fix Anthropic endpoint | 2 min | — |
|
||
| 4.3 | Validate metadata | 10 min | — |
|
||
| 4.4 | Test error paths | 13 min | 4.1–4.3 |
|
||
| **Phase 4 Total** | **Error Handling** | **30 min** | — |
|
||
| | | | |
|
||
| **GRAND TOTAL** | **All Phases** | **~180 min (3 hrs)** | |
|
||
|
||
---
|
||
|
||
## Verification Checklist
|
||
|
||
Before marking complete, verify:
|
||
|
||
- [ ] Phase 1: Rate limiting blocks 6th auth attempt
|
||
- [ ] Phase 2: Sessions stored in Supabase (check Studio)
|
||
- [ ] Phase 2: User A's session NOT visible to User B via API
|
||
- [ ] Phase 3: Frontend clears state on sign-out
|
||
- [ ] Phase 3: User B sees welcome screen (not User A's chat) after sign-in
|
||
- [ ] Phase 3: E2E test passes (User A → chat → signout → User B → fresh state)
|
||
- [ ] Phase 4: Invalid grade returns 400
|
||
- [ ] Phase 4: AI errors return generic message to client
|
||
- [ ] Phase 4: Anthropic non-streaming calls work
|
||
- [ ] All tests pass locally
|
||
- [ ] No console errors in browser
|
||
- [ ] No console errors in backend
|
||
|
||
---
|
||
|
||
## Next Steps After Implementation
|
||
|
||
1. **Staging deployment** — Test on staging server
|
||
2. **Production safety** — Ensure `.env` secrets not exposed
|
||
3. **Monitoring** — Log rate limit hits, DB errors
|
||
4. **Performance** — Monitor Supabase query times
|
||
5. **Backup** — Set up Supabase backup schedule
|
||
6. **Documentation** — Update API docs with RLS details
|
||
|
||
---
|
||
|
||
Ready to start? Let me know which phase first, or proceed sequentially.
|