# Backend Architecture ## Role of the backend The backend is intentionally a **dumb encrypted sync target**. It does not understand the domain model, object types, or the tree structure. All business logic lives in the frontend. The backend only stores and retrieves opaque encrypted JSON blobs on behalf of an authenticated user. This design is chosen to support end-to-end encryption (E2EE) and local-first sync in the future. The server never needs to decrypt object data. ## Data model ### `users` | Column | Type | Notes | | ------------ | ------------ | ---------------------------------------- | | `userId` | VARCHAR(255) | Primary key. UUID or opaque identifier for the user. | | `name` | VARCHAR(255) | Display name for the user. | | `rootObjectId` | VARCHAR(255) | Points to the user's root object row. | ### `objects` | Column | Type | Notes | | ------------ | ------------ | -------------------------------------------------- | | `objectId` | VARCHAR(255) | Primary key. UUID identifying the object. | | `userId` | VARCHAR(255) | Owner of the object. | | `objectData` | TEXT | Encrypted JSON blob (currently stored as plaintext during PoC). | ## API contract ### Users #### `POST /users` Create a new user and an empty root object. **Request body:** ```json { "userId": "string", "name": "string" } ``` Routes use plural REST conventions (`/users`, `/objects`). **Response:** ```json { "status": "created", "rootObjectId": "string" } ``` #### `GET /users/:id` Return the user profile **and all associated objects**. **Response:** ```json { "userId": "string", "name": "string", "rootObjectId": "string", "objects": [ { "objectId": "string", "objectData": "string" } ] } ``` `objects` includes the root object and every object owned by the user. The client uses this to rebuild its local state in one request. #### `PUT /users/:id` Update the user's display name. **Request body:** ```json { "name": "string" } ``` **Response:** ```json { "status": "updated" } ``` > Note: `userId` is immutable. This endpoint only updates the display `name`. #### `DELETE /users/:id` Delete the user and all associated objects. **Response:** ```json { "status": "deleted" } ``` ### Objects #### `POST /objects` Create a new object. **Request body:** ```json { "objectId": "string", "userId": "string", "objectData": "string" } ``` `objectId` is currently supplied by the client to support offline creation. In the future this may be server-generated as part of a more robust user creation flow. **Response:** ```json { "status": "created", "objectId": "string" } ``` #### `GET /objects/:id` Fetch a single object by ID. **Response:** ```json { "objectId": "string", "userId": "string", "objectData": "string" } ``` #### `PUT /objects/:id` Update an object's encrypted data. **Request body:** ```json { "objectData": "string" } ``` **Response:** ```json { "status": "updated" } ``` The client is responsible for updating parent `childIds` arrays when objects are moved or re-parented. #### `DELETE /objects/:id` Delete a single object. **Response:** ```json { "status": "deleted" } ``` The server does not cascade deletions because it cannot decrypt `objectData` to know the object's children. The client must walk the tree and delete children explicitly. ## Design decisions - **Opaque object data**: The server stores `objectData` as an opaque string so it can remain encrypted. It never parses or validates the JSON shape. - **Client-owned tree logic**: Parent/child relationships are maintained inside the encrypted blobs by the frontend. The backend has no concept of roots, projects, tasks, or notes. - **Bulk user fetch**: `GET /users/:id` returns the entire object graph so the frontend can reconstruct state in one request instead of walking the tree with N+1 fetches. - **Plaintext PoC**: Encryption is planned but not implemented. `objectData` is currently plaintext JSON during early development. ## Known gaps and roadmap | Priority | Item | Notes | | -------- | ---- | ----- | | High | Authentication & authorization | Every endpoint must verify the caller owns the requested user/object. Currently absent for PoC simplicity. | | High | Ownership checks | `GET /objects/:id`, `PUT /objects/:id`, and `DELETE /objects/:id` must reject access to objects owned by another user. | | Medium | Object versioning / `updatedAt` | Needed for safe multi-device sync and conflict detection. | | Medium | Multi-device conflict resolution | Last-write-wins is acceptable for PoC; eventually needs a real strategy (server wins, CRDTs, manual merge, etc.). | | Medium | Server-generated object IDs | Currently client-generated to support offline creation; may move to server-generated with auth flow. | | Low | Orphan garbage collection | Server cannot safely clean up orphans because it cannot decrypt blobs. Client-side `cleanupOrphans` on startup is planned. | | Low | Display name updates | `PUT /users/:id` updates `name`; `userId` remains immutable. | Routes use plural REST conventions (`/users`, `/objects`) consistent with the implementation. ## Why not a single giant blob? An alternative considered was storing the user's entire state as one encrypted JSON blob. Rejected because: - Every small edit would require re-uploading the entire state. - Avoiding that requires a diff-chain format (operation logs, CRDTs, Merkle trees), which is significantly more complex than per-object blobs. Per-object blobs strike a balance: the server stays dumb, edits are small, and the sync protocol remains simple.