Files
OwOrganizer/frontend/ARCHITECTURE.md
T
2026-07-16 19:24:39 -07:00

6.3 KiB

Frontend Architecture

Role of the frontend

The frontend owns all business logic. It defines the object types, builds the tree from parent/child references, renders the UI, and is responsible for keeping the local state in sync with the backend.

The backend is treated as a dumb encrypted sync target: it stores opaque JSON blobs and returns them on request. The frontend encrypts and decrypts data, parses blobs into typed models, and maintains all relationships.

Object model

All domain objects are stored as rows in the backend objects table. Each row contains an objectId, a userId, and an objectData string (eventually encrypted JSON).

The frontend recognizes four typed objects:

Root

{
  objectId: string;
  type: 'root';
  childIds: string[];
}

Every user has exactly one root object. It is the entry point for rendering the tree.

Project

{
  objectId: string;
  type: 'project';
  name: string;
  childIds: string[];
}

A project is a container for tasks, notes, and other projects.

Task

{
  objectId: string;
  type: 'task';
  text: string;
  status: 'not-started' | 'in-progress' | 'completed' | 'cancelled';
  childIds: string[];
}

A task is a checkbox-style item that may contain child tasks or notes.

Note

{
  objectId: string;
  type: 'note';
  text: string;
  childIds: string[];
}

A note is a free-text object that may contain child notes or tasks.

Data flow

  1. Load: objectsStore.loadObjects(userId) calls GET /users/:id, which returns the user profile (userId, name, rootObjectId) and all associated objects.
  2. Parse: objectParser.js parses each objectData JSON string into a typed object and stores them in a Map<objectId, TypedObject>.
  3. Render: Components read from the store map and render children by following childIds.
  4. Mutate: User actions update the local store immediately, then persist the changed object(s) to the backend with PUT /objects/:id.
  5. Sync: Eventually, the frontend will compare local state with server state using per-object versioning and resolve conflicts.

Key modules

src/lib/models/*.js

Pure factory functions for creating typed objects. They contain no side effects and no backend knowledge.

src/lib/services/objectParser.js

Converts backend ObjectRecord rows into typed domain objects and serializes typed objects back into the JSON payload stored in objectData.

  • parseObjectRecord(record) — parse a single row.
  • parseObjectRecords(records) — parse many rows into a Map and identify the root object.
  • serializeTypedObject(obj) — convert a typed object into the JSON string stored in objectData.

src/lib/services/objects.js

Thin wrapper around the object HTTP endpoints.

src/lib/services/users.js

Thin wrapper around the user HTTP endpoints. Routes use plural REST conventions (/users, /objects).

src/lib/stores/objects.js

The central Svelte store for the object graph. Responsibilities:

  • Load all objects for the current user.
  • Maintain the Map of parsed objects and the rootId.
  • Provide mutation helpers:
    • addObject(userId, type, parentId, nameOrText, status) — create a child and update the parent's childIds.
    • updateProjectName(objectId, name) — update a project name.
    • updateText(objectId, text) — update task/note text.
    • updateTaskStatus(objectId, status) — update a task status.
    • removeObject(objectId) — delete an object and remove it from its parent's childIds.

Rendering

Components render the tree recursively:

  • ObjectRenderer.svelte dispatches to the correct component based on object.type.
  • Root.svelte, Project.svelte, Task.svelte, and Note.svelte each render their own data and iterate over childIds to render children.

Components look up child objects from $objectsStore.objects by ID. If a child is missing, it is skipped.

Parent/child maintenance

Because the backend cannot decrypt object data, the frontend is responsible for keeping childIds arrays consistent.

  • Create: insert the new object's ID into the parent's childIds, then persist both parent and child.
  • Delete: remove the object's ID from its parent's childIds, persist the parent, then delete the object.
  • Move: remove the ID from the old parent's childIds, add it to the new parent's childIds, and persist both parents.

Orphan handling

Network interruptions can leave orphaned objects in the backend (objects that are no longer referenced by any parent). The planned strategy:

  1. The client-side delete method walks the tree and deletes all descendants explicitly.
  2. On startup, a future cleanupOrphans() method will compare all loaded objects against the reachable tree and delete any object that is not referenced.

Known gaps and roadmap

Priority Item Notes
High Use bulk GET /users/:id for loading Replace the current getUser + getObjectsByUser + populateTree flow with a single request that returns all objects.
High Remove N+1 tree walking populateTree fetches missing children one by one. Once the bulk endpoint is used, this can be deleted.
Medium Encryption/decryption objectData should be encrypted before sending and decrypted after receiving. Deferred until core flow is solid.
Medium Offline support / local-first Client-generated IDs are a first step. Eventually the store should work against a local cache and sync in the background.
Medium Optimistic mutation error handling If a persist fails, local state and server drift. Need pending/conflict states.
Medium Unknown type fallback Currently unknown types are silently dropped. Add a fallback renderer or safe ignore policy.
Low Parent index removeObject scans all objects to find the parent. Maintain a childId -> parentId index for O(1) lookup.
Low Subtree move / duplicate Not yet implemented.

Why the business logic lives in the frontend

Because the backend is intended to be an encrypted sync target, it cannot understand the data it stores. Moving tree logic, validation, and rendering decisions to the frontend keeps the server simple and compatible with E2EE. The trade-off is that the frontend must be defensive about missing objects, unknown types, and sync conflicts.