| name | data-layer |
| user-invocable | false |
| description | Local-first data layer - Dexie (IndexedDB), Yjs (CRDT sync), y-webrtc (peer transport), storage quotas, draft persistence. Use for local persistence, peer sync, offline features, and storage design. |
| version | 1.0.0 |
| status | active |
| packages | ["shared","extension"] |
| dependencies | [] |
| last_updated | 2026-03-12 |
| last_verified | 2026-03-12 |
Data Layer Skill
Unified local-first data layer guide: Dexie for structured storage, Yjs for CRDT sync, y-webrtc for peer transport, storage quotas, and media management.
Activation
When invoked:
- Coop is local-first: all data stays local until explicit publish/sync.
- Use Dexie for structured data (coops, tabs, drafts, preferences).
- Use Yjs for real-time collaborative editing (shared documents, flow boards).
- Use y-webrtc for peer-to-peer transport.
Part 1: Dexie (Structured Storage)
Core Concept
Coop uses Dexie as the IndexedDB abstraction layer. All structured data lives in Dexie tables, queried reactively via useLiveQuery.
import Dexie from "dexie";
class CoopDatabase extends Dexie {
coops!: Dexie.Table<Coop, string>;
tabs!: Dexie.Table<Tab, string>;
drafts!: Dexie.Table<Draft, string>;
preferences!: Dexie.Table<Preference, string>;
constructor() {
super("coopDB");
this.version(1).stores({
coops: "id, safeAddress, createdAt",
tabs: "id, coopId, url, createdAt",
drafts: "id, coopId, updatedAt",
preferences: "key",
});
}
}
export const db = new CoopDatabase();
Reactive Queries
import { useLiveQuery } from "dexie-react-hooks";
function CoopList() {
const coops = useLiveQuery(() => db.coops.toArray());
if (!coops) return <Skeleton />;
return coops.map((c) => <CoopCard key={c.id} coop={c} />);
}
function TabList({ coopId }: { coopId: string }) {
const tabs = useLiveQuery(
() => db.tabs.where("coopId").equals(coopId).toArray(),
[coopId]
);
return tabs?.map((t) => <TabCard key={t.id} tab={t} />);
}
Write Operations
await db.tabs.add({
id: crypto.randomUUID(),
coopId,
url: tab.url,
title: tab.title,
createdAt: Date.now(),
});
await db.drafts.put({
id: draftId,
coopId,
content: editorContent,
updatedAt: Date.now(),
});
await db.tabs.delete(tabId);
await db.tabs.bulkAdd(tabsArray);
Schema Versioning
class CoopDatabase extends Dexie {
constructor() {
super("coopDB");
this.version(1).stores({
coops: "id, safeAddress, createdAt",
tabs: "id, coopId, url, createdAt",
});
this.version(2).stores({
coops: "id, safeAddress, createdAt",
tabs: "id, coopId, url, createdAt",
drafts: "id, coopId, updatedAt",
preferences: "key",
});
this.version(3).stores({
coops: "id, safeAddress, createdAt",
tabs: "id, coopId, url, status, createdAt",
drafts: "id, coopId, updatedAt",
preferences: "key",
});
}
}
Part 2: Yjs (CRDT Sync)
Core Concept
Yjs provides conflict-free replicated data types (CRDTs) for real-time collaboration between peers. Each coop's shared state is a Yjs document.
import * as Y from "yjs";
const ydoc = new Y.Doc();
const yTabs = ydoc.getArray<Tab>("tabs");
const yMeta = ydoc.getMap("metadata");
const yContent = ydoc.getText("content");
Observing Changes
yTabs.observeDeep((events) => {
for (const event of events) {
console.log("Tabs changed:", event.changes);
}
});
yMeta.observe((event) => {
for (const [key, change] of event.changes.keys) {
console.log(`${key}: ${change.action}`);
}
});
Modifying Shared State
ydoc.transact(() => {
yTabs.push([newTab]);
yMeta.set("lastUpdated", Date.now());
});
yContent.insert(0, "Hello ");
yContent.delete(6, 5);
React Integration
import { useYjs } from "@coop/shared";
function SharedEditor({ coopId }: { coopId: string }) {
const { ydoc, yText, connected, peers } = useYjs(coopId);
return (
<div>
<p>{connected ? `${peers} peers connected` : "Offline"}</p>
<Editor yText={yText} />
</div>
);
}
Part 3: y-webrtc (Peer Transport)
Connection Setup
import { WebrtcProvider } from "y-webrtc";
const provider = new WebrtcProvider(
`coop-${coopId}`,
ydoc,
{
signaling: [signalingServerUrl],
password: coopSecret,
}
);
provider.on("status", ({ connected }: { connected: boolean }) => {
console.log("WebRTC connected:", connected);
});
provider.destroy();
API Server
bun dev:api
The API server facilitates WebRTC peer discovery via signaling. Once peers discover each other, data flows directly peer-to-peer.
Persistence Bridge (Dexie <-> Yjs)
Yjs state needs to be persisted to Dexie for offline access:
import * as Y from "yjs";
async function persistYjsState(coopId: string, ydoc: Y.Doc) {
const state = Y.encodeStateAsUpdate(ydoc);
await db.yjsStates.put({
coopId,
state: state,
updatedAt: Date.now(),
});
}
async function restoreYjsState(coopId: string, ydoc: Y.Doc) {
const saved = await db.yjsStates.get(coopId);
if (saved) {
Y.applyUpdate(ydoc, saved.state);
}
}
Part 4: Storage Quota Management
Quota Detection
async function getStorageQuota() {
if (!navigator.storage?.estimate) return null;
const { usage, quota } = await navigator.storage.estimate();
const used = (usage ?? 0) / (1024 * 1024);
const total = (quota ?? 0) / (1024 * 1024);
const percentUsed = total > 0 ? (used / total) * 100 : 0;
return {
used,
quota: total,
percentUsed,
isLow: percentUsed > 75,
isCritical: percentUsed > 90,
};
}
Tiered Cleanup Strategy
async function tieredCleanup(): Promise<CleanupResult> {
const quota = await getStorageQuota();
if (!quota) return { freedMB: 0, actions: [] };
const result: CleanupResult = { freedMB: 0, actions: [] };
if (quota.percentUsed > 75) {
await db.drafts.where("updatedAt").below(Date.now() - 30 * 86400000).delete();
result.actions.push("Cleaned old drafts");
}
if (quota.percentUsed > 85) {
await db.tabs.where("status").equals("archived").delete();
result.actions.push("Cleaned archived tabs");
}
if (quota.percentUsed > 90) {
result.actions.push("User intervention required");
}
return result;
}
Reference Files
For detailed patterns beyond core Dexie/Yjs usage:
-
storage-lifecycle.md -- Schema versioning details, storage quota thresholds, draft persistence patterns, data lifecycle (TTL patterns), and testing patterns.
-
service-worker.md -- Service worker registration, cache strategies, background sync, SW update flow, and connectivity detection.
Anti-Patterns
Dexie/Storage
- Never use localStorage for structured data -- use Dexie (localStorage is sync, 5MB limit, no indexes)
- Never store large media in localStorage -- use Dexie/IndexedDB
- Never assume storage is available -- always handle
QuotaExceededError
- Never skip schema versioning -- always increment version for schema changes
Yjs/Sync
- Never modify Yjs state outside transactions -- use
ydoc.transact()
- Never forget to destroy WebrtcProvider -- clean up on unmount
- Never persist Yjs state without debouncing -- batch writes to Dexie
- Never assume peers are connected -- always handle offline state
Quick Reference Checklists
Before Adding Local-First Features
Before Modifying Dexie Schema
Decision Tree
What data layer work?
|
+-- Structured data (coops, tabs)? --> Dexie tables + useLiveQuery
|
+-- Collaborative editing? ----------> Yjs + y-webrtc
| -> Y.Text for text, Y.Array for lists
| -> Persist to Dexie for offline
|
+-- Schema change needed? -----------> Increment Dexie version
| -> Add migration in constructor
|
+-- Storage running low? ------------> Tiered cleanup strategy
| -> Clean old drafts first
|
+-- Peer sync issue? ----------------> Check WebrtcProvider status
| -> Verify API server
|
+-- Form needs auto-save? -----------> Dexie draft persistence
|
+-- Testing storage? ----------------> Use fake-indexeddb
-> Test migration paths
Related Skills
web3 -- Safe operations that produce onchain state
error-handling-patterns -- Categorizing sync failures
react -- State management for offline indicators
testing -- Mock patterns for Dexie/Yjs in Vitest
performance -- Storage performance and memory management