# PocketBase Settings Save/Load with JSON Fields ## Problem PocketBase collections with a fixed schema (e.g., `id`, `userId`, `name` text fields) can't store arbitrary key-value settings like `{darkMode: true, taxRate: 9.25, ...}`. Every field written to a PocketBase record MUST be defined in the collection schema, or it's **silently dropped** (HTTP 200, data lost). Additionally, **PocketBase v0.23+** enforces a minimum 15-character ID with format validation (lowercase alphanumeric only). Trying to `setDoc` with an ID like `appSettings` (11 chars) fails silently — the adapter's `update('appSettings', data)` → 404, fallback `create({...data, id:'appSettings'})` → 400 `validation_invalid_format`. ## The Fix: Three Layers ### 1. Add a `json` field to the collection ```python # PATCH the collection to add a JSON field api("PATCH", f"/api/collections/settings", { "fields": [ ...existing fields..., {"id": "json_data", "name": "data", "type": "json", "required": False, "system": False} ], ...keep existing rules... }, token) ``` The `json` field type accepts arbitrary objects — PocketBase stores and retrieves them natively. ### 2. Fix `setDoc` in the adapter — query-based upsert Don't try to set PocketBase's system `id` field to custom values. Instead, query by `name + userId` to find the record, then update or create: ```javascript function setDoc(docRef, data) { const collName = docRef._collection; const docId = docRef._id; const pbData = convertToPB(data); const userId = docRef._userId || ''; // Find existing record by name + userId return pb.collection(collName).getFullList({ filter: `name = "${docId}"${userId ? ` && userId = "${userId}"` : ''}`, requestKey: null }).then(records => { if (records.length > 0) { return pb.collection(collName).update(records[0].id, pbData); } else { return pb.collection(collName).create(pbData); } }).then(rec => ({ id: rec.id })); } ``` ### 3. Fix `getDoc` — fallback to name-based lookup ```javascript async function getDoc(docRef) { const collName = docRef._collection; const docId = docRef._id; const userId = docRef._userId || ''; try { // Try direct ID lookup first (for backward compat) let record = await pb.collection(collName).getOne(docId); return { id: record.id, data: () => convertFromPB(record), exists: () => true, ref: { id: record.id } }; } catch (err) { // Fallback: query by name + userId try { const filter = `name = "${docId}"${userId ? ` && userId = "${userId}"` : ''}`; const records = await pb.collection(collName).getFullList({ filter: filter, requestKey: null }); if (records.length > 0) { const record = records[0]; return { id: record.id, data: () => convertFromPB(record), exists: () => true, ref: { id: record.id } }; } } catch (e2) {} return { exists: () => false, data: () => null, ref: { id: docId } }; } } ``` ### 4. Fix save/load callers — wrap data in `data` json field **Save:** ```javascript // BEFORE (fails — PocketBase drops unknown fields) await setDoc(userSettingsRef, { ...appSettings, lastUpdated: new Date().toISOString() }); // AFTER (works — settings go into the 'data' json field) await setDoc(userSettingsRef, { data: { ...appSettings, lastUpdated: new Date().toISOString() }, userId: currentUser.uid, name: 'appSettings' }); ``` **Load:** ```javascript // BEFORE (reads top-level fields — gets nothing useful) const firebaseSettings = settingsDoc.data(); appSettings = { ...appSettings, ...firebaseSettings }; // AFTER (reads from 'data' json field) const record = settingsDoc.data(); if (record && record.data) { const { lastUpdated, ...cleanSettings } = record.data; appSettings = { ...appSettings, ...cleanSettings }; } ``` ## Full Pattern Checklist When settings aren't persisting: - [ ] Does the collection have a `json` field for arbitrary data? - [ ] Is `setDoc` using query-based upsert (not trying to set system IDs)? - [ ] Are all save callers wrapping data in `{data: {...}, userId, name}`? - [ ] Are all load callers reading from `record.data` instead of top-level fields? - [ ] Does every save path include `userId` and `name` for collection rule compliance?