MorphDB · Backend Gaps · Findings from the example clones

The Gaps

What the generic MorphDB backend can't do yet — found by building LinkedIn, Notion, Figma & Linear on it.

Every gap below is a primitive a real product needs that the generic object API doesn't express. Part 1 is what we hit empirically while building; Part 2 is what a production rebuild would categorically require. Comment on any section — contributors, this is the to-do list.

Status living document Source example-app audit loop Scope backend primitives, not app bugs
Part 1

Empirical limitations

Things the clones couldn't do cleanly because the generic backend lacks a primitive — each one reproduced while building. Not frontend bugs (those got fixed in the apps).

01

No value-level validation

MEDIUM
Figma (fill, w/h)

The store validates a field's type (it rejects w:"abc" with 400 Field w expects a number) and rejects unknown fields, but it cannot constrain a value: no format (hex color), no numeric range/min, no string enum/length, no allowed-values for a free-form kind. So fill:"banana" and w:-150 are accepted and persisted, collapsing a shape to 0px. All semantic validation must live client-side.

→ direction

Optional per-field constraints (regex / min / max / enum) checked centrally at write time.

02

No atomic add/remove on a to-many relation

HIGH
LinkedIn (connections), Notion/Figma (list membership)

A to-many relation is written by sending its whole new array. To add one connection you read the full connections list and PATCH it back — a read-modify-write that loses updates when two edits overlap (connect to two people in quick succession → one is dropped, server-confirmed).

→ direction

A {add:[guid], remove:[guid]} delta op on a relation, making membership edits atomic and concurrency-safe.

03

No atomic increment / counter

HIGH
LinkedIn (post likes)

A counter like likes is a scalar the client reads, +1s, and writes back. Concurrent likes race (last-write-wins), and there's no server-side increment op.

→ direction

A PATCH {likes:{$inc:1}}-style atomic numeric op.

04

No atomic reorder / multi-object transaction

HIGH
Figma (z-order), Notion (block order), Linear (ordered lists)

Ordering is emulated with an integer order/z field the client computes (max(z)+1) and PATCHes one object at a time. There's no atomic "move item to position N" and no way to renumber a whole list in one transaction, so concurrent reorders collide and a full renumber is N separate non-atomic writes.

→ direction

A small multi-object transaction / atomic reorder endpoint (and fractional ordering helps the renumber problem).

05

No realtime / subscriptions

BLOCKER
all (multi-tab); Figma header reads "synced" but isn't live

The client is request/response fetch only. Two tabs or two users never see each other's edits without a manual reload. The generic backend offers no change-feed / observe primitive (no push, websocket, or SSE), so a collaborative "live" experience — the whole point of Figma/Notion — can't be built on it.

→ direction

A change-feed primitive (server-sent events or websocket) over an op/change log. This is the keystone gap: one change-feed also unlocks webhooks and cache/search sync.

06

No full-text / substring search

HIGH
Figma (search layers); also LinkedIn/Notion search

List filtering is exact-equality on indexed fields (plus contains only where explicitly supported per field). There's no general full-text or fuzzy search across an app's objects, so a global search box — a core feature of every one of these products — isn't expressible against the backend.

→ direction

A tokenized full-text index (relevance + prefix), or an external search engine fed by a change-feed.

07

No server-side aggregation / group-by

HIGH
Figma ("Layers · N"), Linear (per-status counts), LinkedIn (counts)

List responses expose a flat total for the filter, but no grouped counts (shapes-per-kind, issues-per-status, connection-count without fetching them all). Every tally is computed client-side after fetching the objects, which doesn't scale.

→ direction

A count-by / group-by on an indexed field returning grouped totals.

08

App keys are namespaces, not identity/auth

BLOCKER
LinkedIn (per-user likes, "your" posts)

X-App-Key isolates apps but is not authentication and carries no per-user identity. There's no notion of "the current user", so per-user semantics (one like per user, ownership, permissions) must be faked client-side (here: localStorage), which is trivially bypassed. Real multi-user apps need an identity/auth primitive.

→ direction

Identity + per-object authorization — see the companion MorphRules permissions spec, which designs exactly this.

09

No client-supplied IDs, no soft-delete + restore

MEDIUM
Figma (undo of a delete)

The server assigns every _guid on create; a client can't create an object with a known id, and there's no soft-delete/restore. So "undo a delete" can't bring the same object back — re-creating yields a new _guid, and any references to the old guid must be remapped client-side.

→ direction

A client-supplied id (idempotent create) or a soft-delete/restore (trash + undelete) primitive, making undo/redo and offline replay correct instead of approximate.

10

No cascade / bulk-delete-by-filter

MEDIUM
Notion (delete a page + its subtree)

Deleting a type's objects is one-at-a-time. Deleting a Notion page must walk the subtree client-side and issue a DELETE per descendant page and per block. It's non-atomic — a mid-way failure orphans blocks/pages — and O(N) round-trips. (App-level delete cascades its own schema/objects, but there's no object-level ON DELETE CASCADE and no "delete all matching this filter".)

→ direction

A relation-level cascade and a bulk-delete-by-filter endpoint.

11

No uniqueness constraint, no sequence

HIGH
Linear (duplicate ENG-11 minted)

There's no unique index and no atomic sequence allocator. An issue tracker needs unique, monotonic identifiers (ENG-1, ENG-2, …); with no UNIQUE the store happily accepted two issues with identifier:"ENG-11", and with no next_val() the client computes max(N)+1 over the rows it has loaded — racy under concurrency and simply wrong when a filter is active.

→ direction

A unique-field constraint + a server-side sequence / auto-increment.

12

No collation control on sort

MINOR
Todo (sort by title)

sort=<field> orders by the raw stored value — for strings that's byte/ASCII order, so "Zebra" sorts before "apple", with no locale/case-insensitive option. The Todo app re-sorts the fetched page client-side with localeCompare — which can't stay correct across pagination.

→ direction

A per-sort collation flag (case-insensitive / locale-aware) so the server returns the right order.

13

No null-ordering control, no OR / multi-field filter

MEDIUM
Todo (sort by due date; search title-or-notes)

Two query-expressiveness gaps. (a) Null ordering: sort has no NULLS FIRST/LAST, and SQLite places NULLs first — so sorting tasks by due puts every undated task above the dated ones, the opposite of what a to-do app wants. (b) OR / multi-field: __contains matches one field at a time and filters are AND-combined; there's no OR across fields, so "search title or notes" must be two indexed queries merged client-side.

→ direction

A nulls-ordering flag and a basic OR / multi-field filter.

Part 2

Architectural gaps for a production rebuild

A forward-looking pass: if you rebuilt the real product (LinkedIn / Notion / Figma), what infrastructure would it need, and where does MorphDB fall short. Part 1 was hit empirically on toy clones; these are the categorical systems a real rebuild requires.

A

Identity & access — the first cliff

  • Authentication & identity BLOCKER — no login/sessions/OAuth/SSO/MFA/recovery; the server can't tell which user is calling (X-App-Key is a tenant namespace, not identity). Needs an identity provider + request→user binding. (deepens #8)
  • Authorization, ACLs & sharing BLOCKER — no per-object/row/field access control, no owner/viewer, no roles/teams/orgs, no share links or inherited+overridden page ACLs. Any key-holder reads/writes everything. Needs a policy engine the backend enforces — designed in the MorphRules spec.
B

Realtime & collaboration — kills Figma/Notion as-is

  • Realtime transport (WebSocket/SSE, pub/sub) BLOCKER — polling-only; two tabs never see each other live. (= #5)
  • Conflict-free concurrent editing (CRDT/OT) + a per-document authoritative server + durable op-log/WAL BLOCKER (Figma, Notion) — read-modify-write loses edits; no op ordering, merge, or authoritative in-memory document.
  • Presence & live cursors/selections MAJOR (Figma, Notion).
  • Offline / local-first sync with reconnect reconciliation MAJOR (Notion, Figma).
C

Search & discovery

  • Full-text + typeahead + fuzzy + faceted search BLOCKER — only contains on one indexed field; no relevance, tokenization, typo-tolerance, prefix autocomplete, or facets. (= #6)
  • Semantic / vector search & embeddings (ANN) MAJOR→BLOCKER (LinkedIn PYMK/jobs, Notion AI) — a json field can hold an embedding but it isn't queryable; no vector type or ANN index.
  • Graph traversal — 2nd/3rd-degree, shortest-path, mutual-connection, degree counts MAJOR (LinkedIn) — relations model edges, but include depth ≤4 hydration is not graph algorithms.
D

Feeds, aggregation & derived data

  • Feed generation — fan-out, candidate gen, dedup, ML ranking, per-user materialized timelines BLOCKER (LinkedIn).
  • Aggregation / group-by / count-by / distinct / analytics MAJOR (all) — only a flat total per filter. (= #7)
  • Rollups & formula / computed properties MAJOR (Notion) — no server-side derived fields.
  • Atomic increment counters MAJOR (all). (= #3)   Atomic relation deltas MAJOR (all). (= #2)
  • Multi-object transactions & cross-entity invariants BLOCKER (all) — accept-connection (both sides + notification), apply-to-job, payments. (= #4)
  • Idempotency & optimistic concurrency (compare-and-set / version token) MAJOR (all) — dedup double-submits, prevent lost updates.
E

Storage & media

  • Blob / file / media storage + processing + CDN BLOCKER (all) — objects are JSON; you can store a URL but not the bytes. No upload endpoint, transcoding, image resize, thumbnail/export rendering, or edge delivery. Needs object storage (S3/GCS) + media pipeline + CDN.
F

Async & integration

  • Background jobs / queues / cron / event pipelines BLOCKER (all) — fan-out, search indexing, embedding generation, email batches, exports, cleanup.
  • Webhooks / triggers / change-data-capture / change-feed BLOCKER (all) — keep search/feed/analytics in sync; third-party integration. The store emits no change events.
  • Notifications — generation, fan-out, rollup ("X and 12 others"), in-app/push/email, read state BLOCKER (all).
  • Transactional / outbound email MAJOR (LinkedIn, Notion).
G

History & governance

  • Version history / snapshots / restore / branching+merge BLOCKER (Figma), MAJOR (Notion) — no history; PATCH overwrites.
  • Audit log / activity / compliance trails (GDPR, SOC2, moderation) MAJOR (all).
  • Soft delete / trash / TTL purge MINOR (Notion).
H

Scale, performance & ops

  • Query at scale — cursor pagination (offset-only today), composite/partial indexes, geospatial, deep filters MAJOR (all).
  • Write scalability / sharding / horizontal throughput BLOCKER (all) — a single serialized writer (or N stateless nodes over one Postgres) won't take these products' write volume or hot partitions.
  • Caching / read replicas / CDN / denormalized read views MAJOR (all).
  • Rate limiting / quotas / anti-abuse / spam / moderation BLOCKER (LinkedIn), MAJOR (others).
  • Server-side compute / functions / validation rules MAJOR (all) — semantic validation, computed defaults, hooks. (deepens #1)
  • Observability / metrics / tracing MINOR.
I

Product / business plumbing

  • Billing / seats / plans / workspace admin MAJOR (Notion, LinkedIn).
  • i18n / localization MINOR.

The honest verdict

MorphDB is a genuinely good fit for the system-of-record, prototype layer — schema-fluid typed entities, bidirectional relations, indexed filtering, relation-include hydration, offset pagination over SQLite/Postgres. That covers maybe 10–20% of any of these products (modeling + CRUD of profiles/posts/jobs, the block tree, the scene-graph metadata). Every load-bearing system that makes them what they are sits outside MorphDB.

◇ Most reusable primitives to add, in rough priority

(1) identity + per-object authorization  ·  (2) a change-feed/subscription (unlocks realtime, webhooks, cache/search sync)  ·  (3) atomic ops (increment, relation delta, compare-and-set, small transactions)  ·  (4) aggregation/group-by  ·  (5) blob storage.

The clones work because a demo only needs the 10–20% MorphDB nails. The list above is what separates "a convincing single-file demo" from "the real product" — and almost none of it is expressible against the generic object API today.

MorphDB · living findings document  ·  generated from the example-app audit loop  ·  source: docs/morphdb-limitations.md
Companion design: MorphRules — per-user authorization  ·  all specs