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.
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).
No value-level validation
MEDIUMThe 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.
Optional per-field constraints (regex / min / max / enum) checked centrally at write time.
No atomic add/remove on a to-many relation
HIGHA 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).
A {add:[guid], remove:[guid]} delta op on a relation, making membership edits atomic and concurrency-safe.
No atomic increment / counter
HIGHA 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.
A PATCH {likes:{$inc:1}}-style atomic numeric op.
No atomic reorder / multi-object transaction
HIGHOrdering 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.
A small multi-object transaction / atomic reorder endpoint (and fractional ordering helps the renumber problem).
No realtime / subscriptions
BLOCKERThe 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.
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.
No full-text / substring search
HIGHList 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.
A tokenized full-text index (relevance + prefix), or an external search engine fed by a change-feed.
No server-side aggregation / group-by
HIGHList 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.
A count-by / group-by on an indexed field returning grouped totals.
App keys are namespaces, not identity/auth
BLOCKERX-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.
Identity + per-object authorization — see the companion MorphRules permissions spec, which designs exactly this.
No client-supplied IDs, no soft-delete + restore
MEDIUMThe 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.
A client-supplied id (idempotent create) or a soft-delete/restore (trash + undelete) primitive, making undo/redo and offline replay correct instead of approximate.
No cascade / bulk-delete-by-filter
MEDIUMDeleting 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".)
A relation-level cascade and a bulk-delete-by-filter endpoint.
No uniqueness constraint, no sequence
HIGHENG-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.
A unique-field constraint + a server-side sequence / auto-increment.
No collation control on sort
MINORsort=<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.
A per-sort collation flag (case-insensitive / locale-aware) so the server returns the right order.
No null-ordering control, no OR / multi-field filter
MEDIUMTwo 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.
A nulls-ordering flag and a basic OR / multi-field filter.
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.
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-Keyis 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.
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).
Search & discovery
- Full-text + typeahead + fuzzy + faceted search BLOCKER — only
containson 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
jsonfield 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
includedepth ≤4 hydration is not graph algorithms.
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
totalper 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.
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.
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).
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).
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.
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.
(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.
Companion design: MorphRules — per-user authorization · all specs