django-joist
A live database structure viewer for Django. Joist introspects your live schema and renders it as a scrollable, zoomable ER diagram right inside your app, so you can see how tables actually connect without opening a DB client. It reads structure only by default — tables, columns, keys, indexes, foreign keys; row data is never queried or exposed in this mode.
django-joist is a Django port of the concept implemented by Laravel Truss (MIT, Alberto Arena), which inspired it. The name, branding and Django wiring are original; selected front-end code is adapted under MIT with attribution.
Features
- Live ER diagram of your database, rendered with Mermaid — self-hosted, no CDN, no build step.
- Focus mode: a table and its foreign-key neighbours, with a searchable picker that stays usable on schemas with hundreds of tables.
- Filter by table name; toggle native DB types against Django-style labels.
- Map-style pan and zoom, auto-fit with a legible floor, plus a Fit button.
- Exports: PNG/SVG from the browser; DBML, JSON, CSV, Markdown data
dictionary, Mermaid, or a token-trimmed
llmformat — from the dashboard or from CI withmanage.py joist_export. Deterministic: the same schema always produces the same bytes, sojoist_export --checkfails the build when a committed schema file drifts. - Feed your real structure to a coding agent as grounding context:
annotations for business meaning,
--compactto trim tokens,--focusto narrow to one neighbourhood. Only the schema is read — row data is never touched. - Schema diff: what changed since the last migration, in the dashboard's
Changes panel and via
manage.py joist_diff. - Schema doctor:
manage.py joist_doctorreviews the structure for problems (missing primary keys, unindexed foreign keys, duplicate indexes, risky types) — deterministic, structure-only, safe in CI, with--fail-onexit codes; the same findings power the dashboard's Health panel. - Multiple databases: list aliases under
JOIST["connections"]and switch diagrams from the toolbar, each scoped to its own database. - Light/dark drafting-grid theme, or bring your own: a handful of semantic colour/font knobs re-skin the whole dashboard, served as a same-origin stylesheet (CSP-safe, no inline styles).
- Rebuilds itself: the cached snapshot refreshes after every
migrateand degrades gracefully when the cache store is unreachable. - Production-safe by design: routes answer 404 unless enabled, denials never confirm the dashboard exists, and excluded tables are filtered server-side so their structure never reaches the browser.
Installation
uv add django-joist
Add the app and mount the dashboard:
# settings.py
INSTALLED_APPS = [
...,
"django_joist",
]
# urls.py
from django.urls import include, path
urlpatterns = [
...,
path("joist/", include("django_joist.urls")),
]
Requires Python 3.10+ and Django 5.2+. No migrations, no models, no
collectstatic needed (assets are served by the package, gated with the
rest of the dashboard). The dashboard HTML is a template shipped inside the
app, so your TEMPLATES must keep "APP_DIRS": True for django_joist
(the default in django-admin startproject); if you disabled it, add the
package's template directory to DIRS.
Development-only install
To keep Joist out of production builds entirely, declare it as a development
dependency of your project, the way composer require --dev would:
uv add --dev django-joist # deploy with: uv sync --no-dev
poetry add --group dev django-joist # deploy with: poetry install --without dev
Unlike Laravel's auto-discovery, Django imports everything named in
INSTALLED_APPS and urls.py, so a production build without the package
fails at startup unless both entries are conditional. Test for the package
itself rather than for DEBUG, so a deploy that turns DEBUG on by mistake
still boots:
# settings.py
from importlib.util import find_spec
if find_spec("django_joist"):
INSTALLED_APPS += ["django_joist"]
# urls.py
from importlib.util import find_spec
if find_spec("django_joist"):
urlpatterns += [path("joist/", include("django_joist.urls"))]
Removing Joist leaves nothing behind: it has no models and no migrations. The
management commands go with it, so a CI job that runs joist_doctor or
joist_export --check must install the development dependencies.
To run Joist gated on staging or production instead, install it as a regular dependency (above) and see the next section.
Quick start
By default Joist is enabled when DEBUG = True. Start your dev server and
visit /joist/.
To run Joist gated on staging or production, set both switches:
JOIST = {
"enabled": True,
"authorization": {
"allowed_emails": ["ada@example.com", "grace@example.com"],
},
}
Outside DEBUG the shipped default authorizer admits only allow-listed
emails and fails closed on an empty list. To authorize by role instead,
point authorization.callable at your own request -> bool (a callable or a
dotted path); it fully replaces the default. Denials always return 404.
Your project's
AuthenticationMiddlewaremust run for the mounted URLs (the normal case). Joist's own guard is part of the views and cannot be configured away.
Command line
python manage.py joist_show # the structure as a terminal table
python manage.py joist_open # print/open the dashboard URL
python manage.py joist_rebuild # rebuild the cached snapshot (CI)
python manage.py joist_diff # what changed since the last migration
python manage.py joist_doctor # structure review; exits non-zero on findings
python manage.py joist_export # DBML to stdout by default
python manage.py joist_export --format=json --output=docs/schema.json
python manage.py joist_export --format=dbml --output=docs/schema.dbml --check # CI drift check
python manage.py joist_export --tables=orders,order_lines --focus=orders --depth=1 --compact
python manage.py joist_mcp # serve the MCP server over stdio (for coding agents)
python manage.py joist_install_agent # install agent skills and MCP project configuration
joist_doctor --format=json and --fail-on=warning make it a CI gate;
joist_export --check regenerates, compares, writes nothing and exits 1 on
drift (2 on usage errors), so a migration that changes the schema without
refreshing the committed file fails the build.
As AI context
The same export doubles as grounding context for a coding agent, so it stops inventing columns:
python manage.py joist_export --format=llm # dense, token-trimmed plaintext
python manage.py joist_export --compact # drop defaults and non-unique indexes
python manage.py joist_export --focus=orders --depth=1
Annotations add the business meaning a type cannot — declare them in
settings under JOIST["annotations"] (per-table, per-column, global notes)
or let Joist read native database comments (Postgres/MySQL) by keeping
"database" in annotations["source"]. The llm format and Markdown/DBML
all render annotations; --no-annotations strips them.
Programmatically, the builder is immutable and mirrors the CLI byte-for-byte:
from django_joist import snapshot
text = (
snapshot()
.only(["orders", "order_lines"])
.focus("orders", depth=1)
.compact()
.to_dbml()
)
MCP server
Joist can also serve the same structure over the Model Context Protocol, so an
agent queries the live schema on demand instead of being handed a paste. The
server is the joist_mcp management command, spoken to over stdio.
It exposes five schema tools - list_tables, describe_table,
get_schema, focus_table and get_structural_review - plus the
joist://schema resource. Every one rides the same cached,
exclusion-filtered snapshot as the dashboard, so their format, compact,
focus, connection and annotation behaviour is identical to the CLI. It is
structure only and adds no dependency: the newline-delimited JSON-RPC stdio
transport lives in the package itself.
Automatic setup
Run the installer from the Django project root, with the interpreter that runs your project:
python manage.py joist_install_agent
It configures Claude Code, Codex and Cursor for this project:
.mcp.json,.codex/config.tomland.cursor/mcp.jsonlaunchjoist_mcpwith the absolute path of the current interpreter and ofmanage.py. A virtualenv's interpreter is kept as it is, so the server starts with the project's packages.- The bundled
joist-schemaandjoist-model-reviewskills go under.claude/skillsand.agents/skills(Cursor reads.agents/skillstoo). - A short pointer is added to
AGENTS.md.
Options:
--agent claude,--agent codexor--agent cursorselects the clients; repeat the flag for more than one. The default is all three.--python /path/to/pythonselects another interpreter. A bare name such as--python python3is looked up onPATH.--project-root DIRselects another project directory.
If the agent cannot run the project's Python directly - the app runs in a
container, a VM, or behind a wrapper - write the full server command after
--. It is stored as given, in place of the interpreter and manage.py path:
python manage.py joist_install_agent -- docker exec -i web python manage.py joist_mcp
The installer writes to the project directory it runs in. Inside a container, this is useful only when the project directory is mounted from the host, so the files reach the checkout your agent opens.
Re-running the installer is safe. Unrelated configuration is kept, and a run
after an install changes nothing. An entry that claude mcp add wrote for the
same command is accepted as installed. MCP entries and skills written by an
earlier Joist release are updated to the current version. If a Joist entry or
skill has been customized, the installer stops without writing anything and asks
you to reconcile it.
Manual setup
Register the server with your agent from the Django project root:
| Agent | Command |
|---|---|
| Claude Code | claude mcp add --scope project joist -- python manage.py joist_mcp |
| Codex (OpenAI) | codex mcp add joist -- python manage.py joist_mcp |
| Cursor | the JSON below in the project's .cursor/mcp.json |
{
"mcpServers": {
"joist": {"command": "python", "args": ["manage.py", "joist_mcp"]}
}
}
The server must start in the Django project root, with an interpreter that can
import your settings. python must therefore be the project's interpreter on
the agent's PATH; otherwise use its absolute path. For a containerised app,
replace python manage.py joist_mcp with a command that runs it in the
container, for example docker exec -i <container> python manage.py joist_mcp
(the container's working directory must be the project root).
Schema changes
For model changes, Django's makemigrations creates migrations from model and
migration-file state. Joist can inspect the live schema and, after migrate,
joist_diff can show the resulting structural change. Joist captures the live
structure before migrate and records a baseline only when the structure
actually changes; an empty cache, a no-op migrate, or flush does not erase
the previous diff.
Configuration
Everything lives in one JOIST dict; unset keys keep their defaults (nested
dicts merge key-by-key). The common knobs:
| Key | Purpose |
|---|---|
enabled |
Master switch; defaults to DEBUG. Off = every route 404s |
connections |
Visualizable database aliases + per-alias overrides, e.g. {"analytics": {"excluded_tables": [...]}}; empty = just default; an omitted alias selects the first managed alias |
fallback.enabled |
Opt in to replaying project migrations on in-memory SQLite when a database is unreachable; off by default, so connection errors surface normally |
excluded_tables |
Hidden server-side (never sent to the browser). Django bookkeeping tables are pre-listed |
authorization.callable / .allowed_emails |
Who may view outside DEBUG; a custom callable replaces the email allow-list |
cache.ttl / cache.alias |
Snapshot cache (seconds, <= 0 = forever) and which CACHES entry to use |
diagram.type_labels |
native (default) or django short labels |
diagram.mermaid_url |
Point at your own copy/CDN; default self-hosts from the gated asset route |
focus.default_depth |
FK-neighbour hops shown on focus |
large_schema.warn_above |
Table count that triggers the "use focus/filter" hint |
diff.enabled / diff.dir |
Schema-diff switch and where the structure-only baseline lives (default BASE_DIR/var/joist) |
doctor.* |
Preset, per-rule severity/enable, ignore patterns, fail_on, dashboard panel switches |
annotations.* |
Global notes, per-table/per-column meaning, comment sources |
theme.* |
Semantic colour/font knobs for both light and dark |
base_url |
Used by joist_open to build a browsable URL |
During normal schema reads, the baseline file under diff.dir is Joist's only
disk output; it is derived and safe to delete — worth gitignoring alongside
var/. The optional joist_install_agent command writes agent configuration
to the project.
The no-data promise
Structure is the CREATE TABLE definition: tables, columns (name, native
type, nullability, defaults), primary keys, indexes, foreign keys with
referential actions, and native comments. With the default settings, row
contents are never queried by the dashboard, the API, the MCP server, the CLI,
or the doctor. Enabling fallback.enabled executes project migrations when a
connection fails; RunPython and RunSQL migrations can perform arbitrary
queries and side effects, including against another database alias. Config
excluded_tables are stripped server-side, so an excluded table never
reaches the client, the diff, or an export.
Development
uv sync --extra dev # creates .venv with the package editable and the test tools
uv run pytest
Static checks are part of the same extra and run in CI - mypy on the shipped
package against django-stubs, and ruff for lint (it is not used as a
formatter). Install the hook to run both before every commit:
uv run mypy
uv run ruff check .
uv run pre-commit install
Supported versions are declared once, in pyproject.toml, and the CI matrix is
derived from them rather than the other way round: requires-python gives the
Python floor (3.10), the Framework :: Django :: * classifiers give the Django
list, and every combination of the two that Django itself supports runs — all
five Pythons on 5.2, only 3.12+ on 6.0 and 6.1. Because Django>=5.2 carries no
ceiling, the newest Django release is additionally tested in its own job, so a
new Django is caught here instead of by users — a version added to the metadata
belongs in .github/workflows/tests.yml (and in release.yml, which repeats
the matrix to gate a tag) in the same commit.
The suite runs on SQLite by default. The PostgreSQL and MySQL lanes are the
same suite against a real server: install the driver (psycopg[binary] for
PostgreSQL, pymysql[rsa] for MySQL — tests/settings.py registers it as
MySQLdb) and point the run at one:
JOIST_TEST_ENGINE=postgres JOIST_TEST_USER=joist JOIST_TEST_PASSWORD=password uv run pytest
JOIST_TEST_HOST, JOIST_TEST_PORT, JOIST_TEST_DB and
JOIST_TEST_DB_SECONDARY override the rest. tests/test_backends.py skips the
server-only assertions when the lane is SQLite, and its canary fails the run if
the snapshot fell back to SQLite instead of reading the server.
The client-side logic has its own suite on Node's built-in test runner (no build step: the assets ship as native ES modules), and the browser lane drives the real app in Chromium - plus Firefox for the label-geometry spec, which is the one thing only a second engine can check (see its comment for why).
npm install
npm test # the client-side logic and the stylesheet invariants
npx playwright install chromium firefox # once
JOIST_PYTHON=.venv/bin/python npm run test:e2e
npm run test:e2e starts the test project itself (tests/browser/serve.py) on a
throwaway SQLite file, so it needs the interpreter that has Django and this
package installed: JOIST_PYTHON points at it, defaulting to python.
License
MIT. See LICENSE (includes the attribution to Laravel Truss).
Vendored assets carry their own licenses (Mermaid MIT, IBM Plex Mono SIL OFL)
under src/django_joist/static/joist/.
Metadata
Release files for django-joist 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_joist-0.3.1.tar.gz | 1.2 MB | With provenance | Details for django_joist-0.3.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_joist-0.3.1-py3-none-any.whl | Python 3 | none | any | Details for django_joist-0.3.1-py3-none-any.whl |
Total release size: 2.4 MB
Release files / django_joist-0.3.1.tar.gz
| Download URL | django_joist-0.3.1.tar.gz |
|---|---|
| Size | 1.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4598e251b672de7a40130b76dca8289b4921c2669f71a81fa20b96a09cd45081
|
|
BLAKE2b-256 checksum How to use checksums |
655f3e25f53415fe129eb115d613bc868a68679ccf55c7ca6b8d1ccf5a7a134e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logRelease files / django_joist-0.3.1-py3-none-any.whl
| Download URL | django_joist-0.3.1-py3-none-any.whl |
|---|---|
| Size | 1.2 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6535a55a50286de72104fdeb8194d5abce5d4388df81a66f3d927d44b052362d
|
|
BLAKE2b-256 checksum How to use checksums |
bb9911540c348e00f0786cdfa5c42515f13a8ec986e22efa22f5dd5391deadfa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logSecurity for django-joist 0.3.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for django_joist-0.3.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.