morphdb 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {morphdb-0.1.2 → morphdb-0.1.4}/PKG-INFO +178 -75
- {morphdb-0.1.2 → morphdb-0.1.4}/README.md +177 -74
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/__init__.py +1 -1
- morphdb-0.1.4/morphdb/cli/__init__.py +31 -0
- morphdb-0.1.4/morphdb/cli/dashboard.py +879 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/cli/main.py +88 -15
- morphdb-0.1.4/morphdb/cli/mcp.py +519 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/cli/skill.py +10 -8
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/db.py +31 -0
- morphdb-0.1.4/morphdb/fieldindex.py +204 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/fieldtypes.py +13 -3
- morphdb-0.1.4/morphdb/objects.py +667 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/routes.py +13 -7
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/schema.py +19 -0
- morphdb-0.1.4/morphdb/skill/SKILL.md +311 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/PKG-INFO +178 -75
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/SOURCES.txt +5 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/pyproject.toml +1 -1
- {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_cli.py +42 -9
- {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_core.py +5 -3
- morphdb-0.1.4/tests/test_field_index.py +341 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_hardening.py +1 -1
- morphdb-0.1.4/tests/test_includes.py +99 -0
- morphdb-0.1.4/tests/test_mcp.py +447 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_relations.py +136 -1
- morphdb-0.1.2/morphdb/cli/__init__.py +0 -22
- morphdb-0.1.2/morphdb/cli/dashboard.py +0 -134
- morphdb-0.1.2/morphdb/objects.py +0 -395
- morphdb-0.1.2/morphdb/skill/SKILL.md +0 -258
- {morphdb-0.1.2 → morphdb-0.1.4}/LICENSE +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/__main__.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/apps.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/associations.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/cli/service.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/errors.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/router.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/server.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/skill/scripts/morphdb_schema.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/util.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/dependency_links.txt +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/entry_points.txt +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/requires.txt +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/top_level.txt +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/setup.cfg +0 -0
- {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_apps.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: morphdb
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A coding-agent-friendly, multi-tenant backend for vibe-coded websites. One process hosts many isolated apps; reshape each app's schema as fast as your agent iterates while the frontend keeps calling the same generic endpoints.
|
|
5
5
|
Author: morphdb contributors
|
|
6
6
|
License: MIT
|
|
@@ -30,21 +30,123 @@ Dynamic: license-file
|
|
|
30
30
|
**A coding-agent-friendly, multi-tenant backend for vibe-coded websites.**
|
|
31
31
|
|
|
32
32
|
Reshape the data model as fast as your coding agent iterates — the frontend
|
|
33
|
-
keeps calling the same small set of generic, deterministic endpoints.
|
|
33
|
+
keeps calling the same small set of generic, deterministic endpoints. One
|
|
34
|
+
process hosts many isolated apps (one per site), zero dependencies, backed by
|
|
35
|
+
SQLite.
|
|
34
36
|
|
|
37
|
+
📖 **[Visual explainer → morphdb.pages.dev](https://morphdb.pages.dev)** — the whole idea (schema-fluid, API-stable), the agent/frontend split, relations, and how Claude plugs in over MCP, on one page.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install morphdb
|
|
35
43
|
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
|
|
45
|
+
Manage the local server with the `morphdb` CLI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
morphdb start # run in the background (default 127.0.0.1:8787)
|
|
49
|
+
morphdb status # running? where? how many apps?
|
|
50
|
+
morphdb stop # stop it
|
|
51
|
+
morphdb run # run in the foreground instead (blocking)
|
|
52
|
+
morphdb dashboard # read-only web view of every app + its tables
|
|
53
|
+
morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
|
|
57
|
+
`--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
|
|
58
|
+
`--host`, `--port`, `--db`. From a source checkout with no install, the
|
|
59
|
+
foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
|
|
60
|
+
|
|
61
|
+
To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
|
|
62
|
+
to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
|
|
63
|
+
|
|
64
|
+
**Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
|
|
65
|
+
`https://db.example.com`) and the schema CLI — plus any frontend that reads
|
|
66
|
+
`window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
|
|
67
|
+
of localhost. It's a client-side setting that names a *backend*, not a database
|
|
68
|
+
connection string.
|
|
69
|
+
|
|
70
|
+
## Use it
|
|
71
|
+
|
|
72
|
+
With the server running (`morphdb start`):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
BASE=http://127.0.0.1:8787
|
|
76
|
+
|
|
77
|
+
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
78
|
+
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
79
|
+
H="X-App-Key: my-site"
|
|
80
|
+
|
|
81
|
+
# 1. define types + a relation
|
|
82
|
+
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
83
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
84
|
+
"fields": {"title":"string",
|
|
85
|
+
"done":{"type":"boolean","index":true},
|
|
86
|
+
"priority":{"type":"number","index":true}},
|
|
87
|
+
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
88
|
+
|
|
89
|
+
# 2. create + read + query
|
|
90
|
+
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
91
|
+
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
92
|
+
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
93
|
+
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
94
|
+
|
|
95
|
+
# 3. morph the schema later — existing rows just gain the new field as null
|
|
96
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
100
|
+
|
|
101
|
+
## Command-line interface
|
|
102
|
+
|
|
103
|
+
`morphdb` runs the server as a **background service** — `start` launches it
|
|
104
|
+
detached and hands your terminal straight back; `status` / `stop` find it again
|
|
105
|
+
via a pid file under the state dir.
|
|
106
|
+
|
|
107
|
+
| Command | What it does |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
|
|
110
|
+
| `morphdb status` | Is it running? URL, pid, health, and app count. |
|
|
111
|
+
| `morphdb stop` | Stop the background server. |
|
|
112
|
+
| `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
|
|
113
|
+
| `morphdb run` | Run in the **foreground** (blocking) instead. |
|
|
114
|
+
| `morphdb dashboard` | Open a read-only web view of every app and its tables. |
|
|
115
|
+
| `morphdb install-skill` | Install the bundled Claude Code skill (below). |
|
|
116
|
+
| `morphdb --version` | Print the version. |
|
|
117
|
+
|
|
118
|
+
`start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
|
|
119
|
+
and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
|
|
120
|
+
`dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
|
|
121
|
+
state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
|
|
122
|
+
`$MORPHDB_HOME`.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
morphdb start # background, default 127.0.0.1:8787
|
|
126
|
+
morphdb start --port 9000 --db ./my.sqlite3
|
|
127
|
+
morphdb status # -> running (pid …) at http://… [healthy]
|
|
128
|
+
morphdb dashboard # opens http://127.0.0.1:8788
|
|
129
|
+
morphdb stop
|
|
130
|
+
morphdb run # foreground instead (Ctrl-C to quit)
|
|
46
131
|
```
|
|
47
132
|
|
|
133
|
+
### Install the Claude Code skill
|
|
134
|
+
|
|
135
|
+
`install-skill` writes the bundled MorphDB skill into a Claude skills directory,
|
|
136
|
+
so a coding agent automatically reaches for MorphDB when building a data-backed
|
|
137
|
+
site:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
|
|
141
|
+
morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
|
|
142
|
+
morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
It installs the skill **bundled in the installed package** (not live from
|
|
146
|
+
GitHub) and is **idempotent** — re-running overwrites with the current version.
|
|
147
|
+
To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
|
|
148
|
+
Claude Code afterward to pick it up.
|
|
149
|
+
|
|
48
150
|
## Why
|
|
49
151
|
|
|
50
152
|
AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
|
|
@@ -58,6 +160,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
|
|
|
58
160
|
**no migration, no row rewrite, no downtime** — regardless of how much data
|
|
59
161
|
exists. Meanwhile the frontend talks to generic endpoints that never change.
|
|
60
162
|
|
|
163
|
+
```
|
|
164
|
+
you (the coding agent) the frontend you build
|
|
165
|
+
────────────────────── ──────────────────────
|
|
166
|
+
reshape the schema freely │ calls fixed generic endpoints
|
|
167
|
+
PUT /schema/{type} │ POST /objects/{type}
|
|
168
|
+
GET /schema │ GET /objects/{type}?field=…
|
|
169
|
+
DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
|
|
170
|
+
│ │
|
|
171
|
+
└────────────── MorphDB ───────────┘
|
|
172
|
+
(one process · many apps · SQLite)
|
|
173
|
+
every call: X-App-Key: <app>
|
|
174
|
+
```
|
|
175
|
+
|
|
61
176
|
## The shape of it
|
|
62
177
|
|
|
63
178
|
One MorphDB process hosts **many apps** (one per website), fully isolated from
|
|
@@ -126,63 +241,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
|
|
|
126
241
|
> Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
|
|
127
242
|
> horizontal scale, or production durability guarantees.
|
|
128
243
|
|
|
129
|
-
## Install / run
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
pip install morphdb
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Manage the local server with the `morphdb` CLI:
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
morphdb start # run in the background (default 127.0.0.1:8787)
|
|
139
|
-
morphdb status # running? where? how many apps?
|
|
140
|
-
morphdb stop # stop it
|
|
141
|
-
morphdb run # run in the foreground instead (blocking)
|
|
142
|
-
morphdb dashboard # read-only web view of every app + its tables
|
|
143
|
-
morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
|
|
147
|
-
`--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
|
|
148
|
-
`--host`, `--port`, `--db`. From a source checkout with no install, the
|
|
149
|
-
foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
|
|
150
|
-
|
|
151
|
-
Then: `curl http://127.0.0.1:8787/help` for a live reference.
|
|
152
|
-
|
|
153
|
-
**Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
|
|
154
|
-
`https://db.example.com`) and the schema CLI — plus any frontend that reads
|
|
155
|
-
`window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
|
|
156
|
-
of localhost. It's a client-side setting that names a *backend*, not a database
|
|
157
|
-
connection string.
|
|
158
|
-
|
|
159
|
-
## Quickstart
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
BASE=http://127.0.0.1:8787
|
|
163
|
-
|
|
164
|
-
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
165
|
-
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
166
|
-
H="X-App-Key: my-site"
|
|
167
|
-
|
|
168
|
-
# 1. define types + a relation
|
|
169
|
-
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
170
|
-
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
171
|
-
"fields": {"title":"string","done":"boolean","priority":"number"},
|
|
172
|
-
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
173
|
-
|
|
174
|
-
# 2. create + read + query
|
|
175
|
-
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
176
|
-
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
177
|
-
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
178
|
-
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
179
|
-
|
|
180
|
-
# 3. morph the schema later — existing rows just gain the new field as null
|
|
181
|
-
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
185
|
-
|
|
186
244
|
## Data model
|
|
187
245
|
|
|
188
246
|
| Concept | What it is |
|
|
@@ -267,15 +325,58 @@ Every schema and object request must send the app key as the `X-App-Key` header
|
|
|
267
325
|
|
|
268
326
|
### Query operators
|
|
269
327
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
328
|
+
A field is filterable/sortable only if its schema marks it **`"index": true`**
|
|
329
|
+
(opt-in, default off). Filtering or sorting an un-indexed field returns a 400
|
|
330
|
+
telling you to index it; turning the flag on backfills existing objects
|
|
331
|
+
automatically, turning it off is instant. `json` fields can't be indexed.
|
|
332
|
+
|
|
333
|
+
Append `__op` to an **indexed field** name: `eq` (default), `ne`, `gt`, `gte`,
|
|
334
|
+
`lt`, `lte`, `contains` (substring), `in` (comma-separated), `exists`
|
|
335
|
+
(`true`/`false`).
|
|
273
336
|
|
|
274
337
|
```
|
|
338
|
+
# priority, title, done, status all declared with "index": true
|
|
275
339
|
GET /objects/task?priority__gte=3&title__contains=buy&done=false
|
|
276
|
-
GET /objects/task?status__in=open,blocked&sort=
|
|
340
|
+
GET /objects/task?status__in=open,blocked&sort=priority&order=desc&limit=50
|
|
277
341
|
```
|
|
278
342
|
|
|
343
|
+
You can also filter by a **relation** — treat it like an ORM foreign key, not a
|
|
344
|
+
manual join. Filtering by a relation matches objects linked to a given neighbor,
|
|
345
|
+
and resolves through the indexed edge table (so it is index-backed):
|
|
346
|
+
|
|
347
|
+
| Query | Meaning |
|
|
348
|
+
| --- | --- |
|
|
349
|
+
| `?assignee=<guid>` | objects whose `assignee` is / includes that neighbor |
|
|
350
|
+
| `?assignee__in=<g1>,<g2>` | linked to any of those neighbors |
|
|
351
|
+
| `?assignee__ne=<guid>` | not linked to that neighbor (includes unlinked) |
|
|
352
|
+
| `?assignee__exists=true` | has any `assignee` (`false` → has none) |
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
# "stages of business X that are still in build" — relation + field, one query
|
|
356
|
+
GET /objects/stage?business=<bizguid>&status=build&sort=_created_at
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Relation filters compose with field filters, `sort`, and pagination. Scalar
|
|
360
|
+
comparisons (`gt`/`lt`/`contains`) are field-only; relations support
|
|
361
|
+
`eq`/`ne`/`in`/`exists`. So **model a foreign key as a relation, not a string
|
|
362
|
+
field** — you keep one-read traversal *and* get filtering, indexed, for free.
|
|
363
|
+
|
|
364
|
+
### Including related objects
|
|
365
|
+
|
|
366
|
+
By default a relation reads back as a guid (to-one) or list of guids (to-many).
|
|
367
|
+
Add `?include=` with comma-separated relation paths (dots nest) to hydrate them
|
|
368
|
+
into the full neighbor objects, nested Prisma-style:
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
GET /objects/post?include=author,comments,comments.author
|
|
372
|
+
# each post.author becomes a full user; post.comments a list of full comments,
|
|
373
|
+
# and each comment.author a full user too.
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Works on the list endpoint and both single-object reads. Read-only, depth ≤ 4,
|
|
377
|
+
and batched (one query per relation per level — no N+1). Writes stay flat: create
|
|
378
|
+
and update with guids, never nested objects.
|
|
379
|
+
|
|
279
380
|
## Errors
|
|
280
381
|
|
|
281
382
|
JSON shape: `{"error": {"code": "...", "message": "...", ...extra}}`.
|
|
@@ -310,8 +411,10 @@ allowed, `413` body too large, `500` internal.
|
|
|
310
411
|
old type simply reads as unset (the field's default, or null) until it's
|
|
311
412
|
written again; reads and queries apply this rule identically, so they always
|
|
312
413
|
agree. Re-adding a dropped field at the same type recovers its values.
|
|
313
|
-
- **Filtering is field
|
|
314
|
-
|
|
414
|
+
- **Filtering/sorting is opt-in per field.** Only a field marked `"index": true`
|
|
415
|
+
can be filtered or sorted (it gets a row in the indexed `field_index` table);
|
|
416
|
+
an un-indexed field is storage-only and a filter/sort on it is a 400. Relations
|
|
417
|
+
are always filterable via the indexed edge table — no flag needed.
|
|
315
418
|
- **Integer magnitude.** Numbers are stored and read back exactly at any size.
|
|
316
419
|
Filtering/sorting on integers beyond ±2⁶³ uses floating-point comparison (a
|
|
317
420
|
SQLite limitation), so equality/range queries on such huge integers may be
|
|
@@ -3,21 +3,123 @@
|
|
|
3
3
|
**A coding-agent-friendly, multi-tenant backend for vibe-coded websites.**
|
|
4
4
|
|
|
5
5
|
Reshape the data model as fast as your coding agent iterates — the frontend
|
|
6
|
-
keeps calling the same small set of generic, deterministic endpoints.
|
|
6
|
+
keeps calling the same small set of generic, deterministic endpoints. One
|
|
7
|
+
process hosts many isolated apps (one per site), zero dependencies, backed by
|
|
8
|
+
SQLite.
|
|
7
9
|
|
|
10
|
+
📖 **[Visual explainer → morphdb.pages.dev](https://morphdb.pages.dev)** — the whole idea (schema-fluid, API-stable), the agent/frontend split, relations, and how Claude plugs in over MCP, on one page.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install morphdb
|
|
8
16
|
```
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
Manage the local server with the `morphdb` CLI:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
morphdb start # run in the background (default 127.0.0.1:8787)
|
|
22
|
+
morphdb status # running? where? how many apps?
|
|
23
|
+
morphdb stop # stop it
|
|
24
|
+
morphdb run # run in the foreground instead (blocking)
|
|
25
|
+
morphdb dashboard # read-only web view of every app + its tables
|
|
26
|
+
morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
|
|
30
|
+
`--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
|
|
31
|
+
`--host`, `--port`, `--db`. From a source checkout with no install, the
|
|
32
|
+
foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
|
|
33
|
+
|
|
34
|
+
To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
|
|
35
|
+
to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
|
|
36
|
+
|
|
37
|
+
**Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
|
|
38
|
+
`https://db.example.com`) and the schema CLI — plus any frontend that reads
|
|
39
|
+
`window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
|
|
40
|
+
of localhost. It's a client-side setting that names a *backend*, not a database
|
|
41
|
+
connection string.
|
|
42
|
+
|
|
43
|
+
## Use it
|
|
44
|
+
|
|
45
|
+
With the server running (`morphdb start`):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
BASE=http://127.0.0.1:8787
|
|
49
|
+
|
|
50
|
+
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
51
|
+
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
52
|
+
H="X-App-Key: my-site"
|
|
53
|
+
|
|
54
|
+
# 1. define types + a relation
|
|
55
|
+
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
56
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
57
|
+
"fields": {"title":"string",
|
|
58
|
+
"done":{"type":"boolean","index":true},
|
|
59
|
+
"priority":{"type":"number","index":true}},
|
|
60
|
+
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
61
|
+
|
|
62
|
+
# 2. create + read + query
|
|
63
|
+
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
64
|
+
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
65
|
+
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
66
|
+
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
67
|
+
|
|
68
|
+
# 3. morph the schema later — existing rows just gain the new field as null
|
|
69
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
73
|
+
|
|
74
|
+
## Command-line interface
|
|
75
|
+
|
|
76
|
+
`morphdb` runs the server as a **background service** — `start` launches it
|
|
77
|
+
detached and hands your terminal straight back; `status` / `stop` find it again
|
|
78
|
+
via a pid file under the state dir.
|
|
79
|
+
|
|
80
|
+
| Command | What it does |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
|
|
83
|
+
| `morphdb status` | Is it running? URL, pid, health, and app count. |
|
|
84
|
+
| `morphdb stop` | Stop the background server. |
|
|
85
|
+
| `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
|
|
86
|
+
| `morphdb run` | Run in the **foreground** (blocking) instead. |
|
|
87
|
+
| `morphdb dashboard` | Open a read-only web view of every app and its tables. |
|
|
88
|
+
| `morphdb install-skill` | Install the bundled Claude Code skill (below). |
|
|
89
|
+
| `morphdb --version` | Print the version. |
|
|
90
|
+
|
|
91
|
+
`start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
|
|
92
|
+
and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
|
|
93
|
+
`dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
|
|
94
|
+
state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
|
|
95
|
+
`$MORPHDB_HOME`.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
morphdb start # background, default 127.0.0.1:8787
|
|
99
|
+
morphdb start --port 9000 --db ./my.sqlite3
|
|
100
|
+
morphdb status # -> running (pid …) at http://… [healthy]
|
|
101
|
+
morphdb dashboard # opens http://127.0.0.1:8788
|
|
102
|
+
morphdb stop
|
|
103
|
+
morphdb run # foreground instead (Ctrl-C to quit)
|
|
19
104
|
```
|
|
20
105
|
|
|
106
|
+
### Install the Claude Code skill
|
|
107
|
+
|
|
108
|
+
`install-skill` writes the bundled MorphDB skill into a Claude skills directory,
|
|
109
|
+
so a coding agent automatically reaches for MorphDB when building a data-backed
|
|
110
|
+
site:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
|
|
114
|
+
morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
|
|
115
|
+
morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It installs the skill **bundled in the installed package** (not live from
|
|
119
|
+
GitHub) and is **idempotent** — re-running overwrites with the current version.
|
|
120
|
+
To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
|
|
121
|
+
Claude Code afterward to pick it up.
|
|
122
|
+
|
|
21
123
|
## Why
|
|
22
124
|
|
|
23
125
|
AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
|
|
@@ -31,6 +133,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
|
|
|
31
133
|
**no migration, no row rewrite, no downtime** — regardless of how much data
|
|
32
134
|
exists. Meanwhile the frontend talks to generic endpoints that never change.
|
|
33
135
|
|
|
136
|
+
```
|
|
137
|
+
you (the coding agent) the frontend you build
|
|
138
|
+
────────────────────── ──────────────────────
|
|
139
|
+
reshape the schema freely │ calls fixed generic endpoints
|
|
140
|
+
PUT /schema/{type} │ POST /objects/{type}
|
|
141
|
+
GET /schema │ GET /objects/{type}?field=…
|
|
142
|
+
DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
|
|
143
|
+
│ │
|
|
144
|
+
└────────────── MorphDB ───────────┘
|
|
145
|
+
(one process · many apps · SQLite)
|
|
146
|
+
every call: X-App-Key: <app>
|
|
147
|
+
```
|
|
148
|
+
|
|
34
149
|
## The shape of it
|
|
35
150
|
|
|
36
151
|
One MorphDB process hosts **many apps** (one per website), fully isolated from
|
|
@@ -99,63 +214,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
|
|
|
99
214
|
> Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
|
|
100
215
|
> horizontal scale, or production durability guarantees.
|
|
101
216
|
|
|
102
|
-
## Install / run
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
pip install morphdb
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Manage the local server with the `morphdb` CLI:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
morphdb start # run in the background (default 127.0.0.1:8787)
|
|
112
|
-
morphdb status # running? where? how many apps?
|
|
113
|
-
morphdb stop # stop it
|
|
114
|
-
morphdb run # run in the foreground instead (blocking)
|
|
115
|
-
morphdb dashboard # read-only web view of every app + its tables
|
|
116
|
-
morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
|
|
120
|
-
`--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
|
|
121
|
-
`--host`, `--port`, `--db`. From a source checkout with no install, the
|
|
122
|
-
foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
|
|
123
|
-
|
|
124
|
-
Then: `curl http://127.0.0.1:8787/help` for a live reference.
|
|
125
|
-
|
|
126
|
-
**Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
|
|
127
|
-
`https://db.example.com`) and the schema CLI — plus any frontend that reads
|
|
128
|
-
`window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
|
|
129
|
-
of localhost. It's a client-side setting that names a *backend*, not a database
|
|
130
|
-
connection string.
|
|
131
|
-
|
|
132
|
-
## Quickstart
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
BASE=http://127.0.0.1:8787
|
|
136
|
-
|
|
137
|
-
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
138
|
-
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
139
|
-
H="X-App-Key: my-site"
|
|
140
|
-
|
|
141
|
-
# 1. define types + a relation
|
|
142
|
-
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
143
|
-
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
144
|
-
"fields": {"title":"string","done":"boolean","priority":"number"},
|
|
145
|
-
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
146
|
-
|
|
147
|
-
# 2. create + read + query
|
|
148
|
-
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
149
|
-
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
150
|
-
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
151
|
-
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
152
|
-
|
|
153
|
-
# 3. morph the schema later — existing rows just gain the new field as null
|
|
154
|
-
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
158
|
-
|
|
159
217
|
## Data model
|
|
160
218
|
|
|
161
219
|
| Concept | What it is |
|
|
@@ -240,15 +298,58 @@ Every schema and object request must send the app key as the `X-App-Key` header
|
|
|
240
298
|
|
|
241
299
|
### Query operators
|
|
242
300
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
301
|
+
A field is filterable/sortable only if its schema marks it **`"index": true`**
|
|
302
|
+
(opt-in, default off). Filtering or sorting an un-indexed field returns a 400
|
|
303
|
+
telling you to index it; turning the flag on backfills existing objects
|
|
304
|
+
automatically, turning it off is instant. `json` fields can't be indexed.
|
|
305
|
+
|
|
306
|
+
Append `__op` to an **indexed field** name: `eq` (default), `ne`, `gt`, `gte`,
|
|
307
|
+
`lt`, `lte`, `contains` (substring), `in` (comma-separated), `exists`
|
|
308
|
+
(`true`/`false`).
|
|
246
309
|
|
|
247
310
|
```
|
|
311
|
+
# priority, title, done, status all declared with "index": true
|
|
248
312
|
GET /objects/task?priority__gte=3&title__contains=buy&done=false
|
|
249
|
-
GET /objects/task?status__in=open,blocked&sort=
|
|
313
|
+
GET /objects/task?status__in=open,blocked&sort=priority&order=desc&limit=50
|
|
250
314
|
```
|
|
251
315
|
|
|
316
|
+
You can also filter by a **relation** — treat it like an ORM foreign key, not a
|
|
317
|
+
manual join. Filtering by a relation matches objects linked to a given neighbor,
|
|
318
|
+
and resolves through the indexed edge table (so it is index-backed):
|
|
319
|
+
|
|
320
|
+
| Query | Meaning |
|
|
321
|
+
| --- | --- |
|
|
322
|
+
| `?assignee=<guid>` | objects whose `assignee` is / includes that neighbor |
|
|
323
|
+
| `?assignee__in=<g1>,<g2>` | linked to any of those neighbors |
|
|
324
|
+
| `?assignee__ne=<guid>` | not linked to that neighbor (includes unlinked) |
|
|
325
|
+
| `?assignee__exists=true` | has any `assignee` (`false` → has none) |
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
# "stages of business X that are still in build" — relation + field, one query
|
|
329
|
+
GET /objects/stage?business=<bizguid>&status=build&sort=_created_at
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Relation filters compose with field filters, `sort`, and pagination. Scalar
|
|
333
|
+
comparisons (`gt`/`lt`/`contains`) are field-only; relations support
|
|
334
|
+
`eq`/`ne`/`in`/`exists`. So **model a foreign key as a relation, not a string
|
|
335
|
+
field** — you keep one-read traversal *and* get filtering, indexed, for free.
|
|
336
|
+
|
|
337
|
+
### Including related objects
|
|
338
|
+
|
|
339
|
+
By default a relation reads back as a guid (to-one) or list of guids (to-many).
|
|
340
|
+
Add `?include=` with comma-separated relation paths (dots nest) to hydrate them
|
|
341
|
+
into the full neighbor objects, nested Prisma-style:
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
GET /objects/post?include=author,comments,comments.author
|
|
345
|
+
# each post.author becomes a full user; post.comments a list of full comments,
|
|
346
|
+
# and each comment.author a full user too.
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Works on the list endpoint and both single-object reads. Read-only, depth ≤ 4,
|
|
350
|
+
and batched (one query per relation per level — no N+1). Writes stay flat: create
|
|
351
|
+
and update with guids, never nested objects.
|
|
352
|
+
|
|
252
353
|
## Errors
|
|
253
354
|
|
|
254
355
|
JSON shape: `{"error": {"code": "...", "message": "...", ...extra}}`.
|
|
@@ -283,8 +384,10 @@ allowed, `413` body too large, `500` internal.
|
|
|
283
384
|
old type simply reads as unset (the field's default, or null) until it's
|
|
284
385
|
written again; reads and queries apply this rule identically, so they always
|
|
285
386
|
agree. Re-adding a dropped field at the same type recovers its values.
|
|
286
|
-
- **Filtering is field
|
|
287
|
-
|
|
387
|
+
- **Filtering/sorting is opt-in per field.** Only a field marked `"index": true`
|
|
388
|
+
can be filtered or sorted (it gets a row in the indexed `field_index` table);
|
|
389
|
+
an un-indexed field is storage-only and a filter/sort on it is a 400. Relations
|
|
390
|
+
are always filterable via the indexed edge table — no flag needed.
|
|
288
391
|
- **Integer magnitude.** Numbers are stored and read back exactly at any size.
|
|
289
392
|
Filtering/sorting on integers beyond ±2⁶³ uses floating-point comparison (a
|
|
290
393
|
SQLite limitation), so equality/range queries on such huge integers may be
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""MorphDB command-line interface — process management + admin dashboard.
|
|
2
|
+
|
|
3
|
+
Intentionally separate from the core engine (db / schema / objects / server):
|
|
4
|
+
this package only *orchestrates* a server process and offers a read-only admin
|
|
5
|
+
view. It never changes how the core stores or serves data.
|
|
6
|
+
|
|
7
|
+
Commands (see :mod:`morphdb.cli.main`):
|
|
8
|
+
|
|
9
|
+
morphdb start the server in the background (alias of `start`)
|
|
10
|
+
morphdb start same, explicit
|
|
11
|
+
morphdb status is it running? where? how many apps?
|
|
12
|
+
morphdb stop stop the background server
|
|
13
|
+
morphdb logs show the server log (-f to follow)
|
|
14
|
+
morphdb run run in the foreground (blocking; for dev)
|
|
15
|
+
morphdb dashboard open a read-only web view of every app + its tables
|
|
16
|
+
morphdb mcp run the MCP server (stdio; spawned by Claude Code, not you)
|
|
17
|
+
morphdb install-skill install/update the bundled Claude Code skill
|
|
18
|
+
|
|
19
|
+
The ``mcp`` command is a thin HTTP *client* of the running backend daemon (it
|
|
20
|
+
auto-starts the daemon if needed). It exposes schema + app operations to a coding
|
|
21
|
+
agent as MCP tools, so the agent calls real tools instead of shelling out to the
|
|
22
|
+
bundled schema script. It is pure stdlib — no MCP SDK — so the package stays
|
|
23
|
+
dependency-free.
|
|
24
|
+
|
|
25
|
+
Storage: the local server keeps data in a per-user SQLite file at
|
|
26
|
+
``~/.morphdb/data.sqlite3`` (override the file with ``--db``, or move the state
|
|
27
|
+
dir with ``$MORPHDB_HOME``). To talk to a MorphDB hosted somewhere else instead
|
|
28
|
+
of a local one, point *clients* at it with ``$MORPHDB_HOST`` (a full URL) — that
|
|
29
|
+
is a client-side setting, not a database connection string; the engine is always
|
|
30
|
+
SQLite.
|
|
31
|
+
"""
|