Files
2026-07-16 19:24:39 -07:00

5.7 KiB

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:

{
  "userId": "string",
  "name": "string"
}

Routes use plural REST conventions (/users, /objects).

Response:

{
  "status": "created",
  "rootObjectId": "string"
}

GET /users/:id

Return the user profile and all associated objects.

Response:

{
  "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:

{
  "name": "string"
}

Response:

{
  "status": "updated"
}

Note: userId is immutable. This endpoint only updates the display name.

DELETE /users/:id

Delete the user and all associated objects.

Response:

{
  "status": "deleted"
}

Objects

POST /objects

Create a new object.

Request body:

{
  "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:

{
  "status": "created",
  "objectId": "string"
}

GET /objects/:id

Fetch a single object by ID.

Response:

{
  "objectId": "string",
  "userId": "string",
  "objectData": "string"
}

PUT /objects/:id

Update an object's encrypted data.

Request body:

{
  "objectData": "string"
}

Response:

{
  "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:

{
  "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.