Skip to main content

django-joist

Latest version on PyPI Downloads Tests Python versions Django 5.2+ License: MIT AND OFL-1.1

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 llm format — from the dashboard or from CI with manage.py joist_export. Deterministic: the same schema always produces the same bytes, so joist_export --check fails the build when a committed schema file drifts.
  • Feed your real structure to a coding agent as grounding context: annotations for business meaning, --compact to trim tokens, --focus to 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_doctor reviews the structure for problems (missing primary keys, unindexed foreign keys, duplicate indexes, risky types) — deterministic, structure-only, safe in CI, with --fail-on exit 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 migrate and 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 AuthenticationMiddleware must 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.toml and .cursor/mcp.json launch joist_mcp with the absolute path of the current interpreter and of manage.py. A virtualenv's interpreter is kept as it is, so the server starts with the project's packages.
  • The bundled joist-schema and joist-model-review skills go under .claude/skills and .agents/skills (Cursor reads .agents/skills too).
  • A short pointer is added to AGENTS.md.

Options:

  • --agent claude, --agent codex or --agent cursor selects the clients; repeat the flag for more than one. The default is all three.
  • --python /path/to/python selects another interpreter. A bare name such as --python python3 is looked up on PATH.
  • --project-root DIR selects 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)

Source distribution for django-joist 0.3.1
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)

Table of built distributions (wheels) for django-joist 0.3.1
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page