The persistence layer is the most carefully designed part of MC Modeler’s architecture. It is built around a single principle: no store, hook, or component ever knows which storage backend is active. All storage goes through one interface, and swapping implementations requires changing exactly one line of code.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Aston2710/mc-modeler/llms.txt
Use this file to discover all available pages before exploring further.
The repository pattern was chosen from the start specifically to make the local→cloud migration zero-cost for the rest of the codebase. Stores, hooks, and components call
diagramRepository methods — they have no knowledge of IndexedDB, Supabase, or any other backend.The IDiagramRepository Interface
This interface is the immutable contract for all persistence. It is defined in/src/persistence/IDiagramRepository.ts and must never change in ways that break existing implementations.
The Single Binding Point
/src/persistence/index.ts is the only file that knows which implementation is active. It is the only place you need to change when switching backends.
isSupabaseConfigured is a boolean computed at module load time — it checks whether VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY environment variables are present. If they are not set, the app falls back to fully offline local mode with no authentication required.
LocalRepository
LocalRepository implements IDiagramRepository using localforage, which uses IndexedDB with an automatic localStorage fallback. It works with zero configuration, no authentication, and no network access.
Storage Keys
Two separate localforage instances are created under theflujo database name:
| Instance | Store name | Keys |
|---|---|---|
store | main | flujo:diagrams → Diagram[] |
store | main | flujo:folders → Folder[] |
store | main | flujo:projects → Project[] |
store | main | flujo:preferences → UserPreferences |
thumbStore | thumbnails | <diagramId> → data URL string |
thumbStore | thumbnails | subproc:<parentId>:<elementId> → data URL string |
flujo:diagrams does not load thumbnail bytes.
Concurrency model
LocalRepository is single-device and single-user, so optimistic CAS is a no-op — the expectedUpdatedAt parameter is accepted but ignored. There is no concurrency to protect against locally.
SupabaseRepository
SupabaseRepository implements IDiagramRepository against a Supabase project (PostgreSQL + Storage). Preferences are an exception — they are always stored locally via a delegated LocalRepository instance, as preferences are per-device by design.
Database Tables
| Table | Description |
|---|---|
profiles | User profile data synced from auth.users via trigger |
folders | Diagram folders, scoped to owner_id |
diagrams | Primary diagram table with current_xml, thumbnail_path, element_count |
diagram_collaborators | Per-diagram role grants (owner, editor, viewer) |
diagram_invites | Invite tokens for sharing diagrams |
projects | Project groups, scoped to owner_id |
project_collaborators | Per-project role grants |
project_invites | Invite tokens for sharing projects |
Row Level Security (RLS)
All tables have RLS enabled. Access is enforced by helper functions defined in the database:diagramsSELECT:can_access_diagram(id)— collaborators can readdiagramsUPDATE:can_edit_diagram(id)— only editors/owners can writediagramsDELETE:owner_id = auth.uid()— only the owner can permanently deletefoldersALL:owner_id = auth.uid()— private to owner
Storage Buckets
| Bucket | Contents | Path pattern |
|---|---|---|
thumbnails | Diagram thumbnail images | <diagramId>/thumb |
thumbnails | Sub-process thumbnails | <parentId>/subproc/<elementId> |
diagram-images | Library images | <scopeId>/imglib/<uuid>.<ext> |
Performance Optimisation: List Without XML
A critical performance decision:getAll() never fetches current_xml. The XML column can be hundreds of kilobytes, and fetching it for a list of 100+ diagrams would transfer tens of megabytes on every app load.
xml field in returned Diagram objects is an empty string for list responses. It is hydrated on demand when a diagram is opened via diagramStore.ensureXml(id), which calls getById(id) and caches the result.
Optimistic CAS: Save Conflict Handling
When multiple users (or multiple browser tabs) edit the same diagram, save operations can conflict.SupabaseRepository.save() implements optimistic concurrency control using the updated_at timestamp as a version token.
- How CAS works
- Conflict resolution in diagramStore
- Rename safety
Every save includes the
updated_at value the client loaded. The database trigger diagrams_set_updated_at automatically sets updated_at = now() on every UPDATE. If the updated_at in the database no longer matches what the client sent, zero rows are affected and DiagramConflictError is thrown.Thumbnail CAS
Saving a thumbnail can also bumpupdated_at (the first time thumbnail_path is set for a diagram, the row is updated and the trigger fires). saveThumbnail() returns the new updated_at if this happened:
Auto-Save: useAutoSave Hook
TheuseAutoSave hook (/src/hooks/useAutoSave.ts) drives automatic persistence. It listens for the unsavedChanges flag in uiStore and schedules a save.
Save timing
| Trigger | Delay |
|---|---|
| Continuous editing (typing labels, moving elements) | intervalSeconds + 0–5 s jitter |
| Tab becomes active with unsaved changes | Same debounce applies |
| Import, create element, delete element | Immediate (external callers set unsavedChanges = true then the timer fires) |
Save guards
The hook performs three checks before saving:Save serialization
diagramStore maintains a saveChain promise that serializes all save operations from the same client:
Status bar states
TheStatusBar component reads uiStore.unsavedChanges and diagramStore.lastSavedAt:
| State | Display |
|---|---|
unsavedChanges = false | ● Saved |
unsavedChanges = true (save pending) | ● Unsaved changes |
| Save in progress | ● Saving… |
Soft Delete and Trash
All destructive operations are two-phase. The first phase is recoverable (soft delete); the second is permanent (purge).delete(id) — move to trash
delete(id) — move to trash
Sets
deleted_at to the current timestamp. The diagram is excluded from getAll() results but is still in the database. Thumbnails and linked images are preserved so they can be shown in the trash UI and restored.restore(id) — remove from trash
restore(id) — remove from trash
Clears
deleted_at (sets to null). The diagram reappears in getAll() results.purge(id) — permanent delete
purge(id) — permanent delete
Performs a real
DELETE. Thumbnails are also removed from Storage (or thumbStore in local mode). This operation is irreversible.In SupabaseRepository, the parent_diagram_id foreign key has ON DELETE CASCADE, so purging a parent diagram automatically purges all its sub-process descendants.getTrash() — read trash contents
getTrash() — read trash contents
Returns all diagrams and projects where
deleted_at IS NOT NULL, ordered by deletion time (most recent first).purgeAll() — empty trash
purgeAll() — empty trash
Permanently deletes every item with
deleted_at set, across both diagrams and projects. In SupabaseRepository, thumbnail objects are batch-deleted from Storage.deleteWithChildren(id) — recursive soft delete
deleteWithChildren(id) — recursive soft delete
Soft-deletes a diagram and all its sub-process diagrams (diagrams where
parentDiagramId equals this ID, recursively). Used when deleting a top-level diagram that has expanded sub-processes.In LocalRepository, the recursion is done client-side by traversing the in-memory diagram array. In SupabaseRepository, the parent_diagram_id ON DELETE CASCADE foreign key handles cascaded purges automatically, but soft deletes are done with a single UPDATE scoped to the direct parent.Project-level trash
Deleting a project soft-deletes both the project record and all its diagrams in a single operation. Restoring a project restores all its diagrams. Purging a project purges all its diagrams.Adding a New Backend
The repository pattern makes new storage backends straightforward:Create the implementation
Create a new class in
/src/persistence/ that implements IDiagramRepository (and IImageRepository if needed):