Skip to content

Neolith, for Django · pre-alpha

Deploy Django apps, not Django projects.

A specification and Django library for running a Django app in a shared process or as its own service, switched by configuration, not code.

status
pre-alpha
installable
not yet
specification
draft 0.1
latest release
none yet
license
BSD-3-Clause

One project, one deployment

Django's app is a good unit of modularity, but its unit of deployment is the project.

A Django codebase therefore ships, scales and fails as one piece. Splitting it usually means several Django projects with duplicated settings and hand-written network calls, or a rewrite in a lighter framework that gives up the ORM, migrations and admin.

The fork in the road: three ways to split a Django monolith At the top, one Django project holds three apps, orders, inventory and billing, with one settings.py, urls.py and manage.py: every app ships, scales and fails together. The road forks three ways, and each path shows the same three apps side by side. Left, gray: several projects. Each app becomes its own project, so inventory carries its own copies of settings.py, urls.py, manage.py and the admin, and the projects call each other through tangled network calls. Costs: duplicated settings, auth and admin, and tangled network calls. Right, gray: a lighter framework. Inventory is rewritten, and its ORM, migrations, admin and auth are struck out. Costs: batteries left behind and app code rewritten. Middle, green, straight on: Neolith. The three apps stay Django apps joined by contracts, and inventory is meant to keep its ORM, migrations, admin and auth. Aims: no second settings.py, and the batteries travel with the app. Caption: Deploy Django apps, not Django projects.
Fig. 1 Splitting a monolith usually means many projects or a lighter framework; Neolith is designed as a third path that keeps Django.

Make the app the unit of deployment

This is the design in the specification. The code has contracts and typed errors so far.

Each app declares the contracts it provides and requires, and Neolith's app host, the runtime that boots apps from their declarations, runs it attached, in a shared process, or detached, as its own service. The mode is deployment configuration: neither the app nor its callers change.

Same app, two modes: attached and detached Two panels side by side. Left, attached: orders and inventory run in one app host, and orders reaches inventory over the in-process transport. Right, detached: orders runs in one app host and inventory in its own app host, reached over a network transport. Under each panel is the calling code in orders, from the planned API and identical in both: inventory = connect("inventory.v1"), then reply = inventory.reserve(ReserveRequest(sku="A1", qty=2)). Under that is a settings block, NEOLITH = {"providers": {...}}, planned and illustrative, not current syntax, where one highlighted line is the only difference: "inventory.v1": "in-process" when attached, and "inventory.v1": "http://inventory" when detached. In the detached panel, inventory slides out of the shared app host into its own as the figure scrolls into view, unless reduced motion is requested.
Fig. 2 The same app and the same calling code, attached or detached; only the deployment configuration changes.Illustration. The settings format for 0.1 is not final.

How it works

Every call between apps goes through a contract, a connector and a transport.

contract
An app's only public surface: named, versioned actions with typed requests, replies and errors, such as inventory.v1.
connector
What a caller gets from connect(). It picks a provider of the contract and hands the call to a transport.
transport
What carries the call: in-process when attached, HTTP when detached, or another conformant transport.
Contract, connector, transport: how one call travels The caller, orders, calls inventory.reserve. The call goes through the contract, inventory.v1, which names the action reserve with a typed request, reply and errors, and then through the connector, which picks a provider by its routing policy. This is the design in specification draft 0.1. From the connector two tracks lead to the provider, inventory. The first is the in-process transport, to inventory in the same app host; values still cross as JSON text, in-process too. The second is a network transport, drawn as a socket for network transports that takes plugs: HTTP is plugged in, and gRPC and a plug marked yours wait beside it, because any conformant transport plugs in. It leads to inventory in its own app host. A dashed error branch taps both tracks and returns to the caller as a ContractError of kind unavailable or timeout, the same kinds in both modes.
Fig. 3 One contract over any conformant transport, and a failed call raises the same typed error in every mode.Only in-process and HTTP are specified, and neither is built yet; gRPC and others are possible additions.

What you can check today

Neolith is pre-alpha, so the evidence is the written record and the tests, not production use.

EvidenceWhat it showsStatusWhere
SpecificationHow apps declare contracts, how calls travel and how an app boots alone, with no Django or Python in it.Fact: draft 0.1. Five of its seven documents are still outlines.Specification (opens another site)
Design decisionsEach choice that shapes the architecture, with the options weighed and the consequences.Fact: 15 accepted and 8 proposed.Decision records (opens another site)
Requirement traceEach normative statement has an ID, and CI reports which ones a passing test covers.Fact: enforced for the contracts document; others join as their roadmap steps land.Test rules (opens another site)
CI matrixThe tests on Django 5.2 with Python 3.12 and Django 6.1 with Python 3.15, on x86-64 and arm64.Fact: runs on every pull request, with deprecation warnings as errors.CI runs (opens another site)
Contracts in code@contract, @action, @implements and ContractError; a contract and its implementation are checked when their module is imported.Fact: on main, not released.Changelog (opens another site)
Conformance suiteEvery contract test run over the in-process and the HTTP transport, with identical results.Plan: roadmap step 6.Conformance (opens another site)
App boots aloneneolith run <app> starting one app with only what it declares, shown in CI.Plan: roadmap steps 4 and 8.Roadmap (opens another site)

Not solved yet

  • How the admin and Django's other contrib apps run across hosts.
  • Authentication between apps in production. The only credential the specification requires so far is a shared-secret token meant for development.
  • Consistency across apps without shared transactions. It relies on events, which are deferred past 0.1.
  • Moving an app's data when it detaches, also deferred past 0.1.
  • Changing an app's mode at runtime, without a redeploy.

What Neolith asks of an app, such as owning its data, is in the FAQ.

Get started

Nothing is installable yet. The first release, 0.1, is still being built.

  1. Read the overview and terms (opens another site), the first document of the specification.
  2. Read the vision (opens another site) and the guiding principles (opens another site) to see the trade-offs.
  3. Follow the roadmap (opens another site) and watch the repository (opens another site) for the first release.

Planned public surface, abridged from the roadmap. Not installable yet.

from typing import Protocol

from neolith import NeolithAppConfig, contract, action, implements, connect, ContractError

@contract("inventory.v1")
class Inventory(Protocol):
    @action(idempotent=True)
    def reserve(self, request: ReserveRequest) -> ReserveReply: ...

@implements(Inventory)
class InventoryImpl:
    def reserve(self, request: ReserveRequest, ctx: CallContext) -> ReserveReply: ...

class InventoryConfig(NeolithAppConfig):
    name = "inventory"
    provides = ["inventory.v1"]
    requires = []
    resources = {"database": "inventory"}

# in a caller
inventory = connect("inventory.v1")
try:
    reply = inventory.reserve(ReserveRequest(sku="A1", qty=2))
except ContractError as e:
    e.kind  # "unavailable", "conflict", ...

contract, action, implements and ContractError are in the code on main; connect and NeolithAppConfig are not yet. In 0.1, neolith run <app> is planned to boot one app on SQLite, with support planned from Django 5.2 LTS on Python 3.12 up to Django 6.1 on Python 3.15.

Contribute

The spec and the code are written together, so design discussion is as useful as code right now.

Neolith is licensed under BSD-3-Clause, the same license as Django. Read the FAQ.

Neolith is not affiliated with or endorsed by the Django Software Foundation.