Self-hosted equipment lending with real reservations. Register your kit, book it out for a date window, and it refuses to double-book anything — telling you when the next one frees up instead of just saying no.
A lending desk in one Python file. Built for a school AV cage; it fits anywhere a clipboard is currently doing the job — tool libraries, makerspaces, studios, church sound cupboards, a lab that lends out meters.
(The name: two loans are compossible if they can share a world without contradicting each other. Deciding which ones can is the whole program.)
python app.py # native desktop window (falls back to server mode)
python app.py --server # Flask only, browse to http://127.0.0.1:5050
python app.py --demo # a populated counter to poke at, throwaway database
python app.py --demo --server # ...the same, in your browser
Start with --demo. It builds a separate database — your real one is never
touched — with six item types, loans in every state, and one booking that is
deliberately impossible, so the thing this app does differently is visible in
about ten seconds rather than after you have invented test data.
No accounts, no server to administer, no cloud. One SQLite file next to the script holds everything.
Most small asset trackers answer "is this item out right now?" — a boolean against the present moment. That is not the question a loans counter actually faces. The real question is "can I promise this to someone for Thursday through Monday?", and answering it means reasoning about overlapping intervals in the future.
Leibniz used compossible for things that can hold together in one world without contradiction. Two bookings are compossible when they can share a window. Deciding which ones can is the whole program.
type_availability() does that. For a requested window it counts what is
committed against a type:
- units physically bound to an open loan whose window overlaps yours, and
- the unfilled quantity on reservations that overlap yours — a reservation for two cameras with only one allocated so far still holds back two.
Two consequences that are easy to get wrong and are pinned by tests:
Overlap is half-open. A loan ending exactly when the next begins does not conflict. Without this you cannot re-lend a camera the same afternoon it comes back, which is most of what a busy counter does.
A partly-allocated reservation is not counted twice. qty=2 with one unit
already bound commits two, not three. Getting this wrong under-reports
availability, and the counter quietly starts refusing loans it could fulfil.
The check and the write are one serialized transaction, so a double-click or a second tab cannot slip between them and promise the same unit twice.
When it does have to refuse, it doesn't stop at "unavailable". It replays the deadlines of everything blocking you, accumulating units as each falls due, and reports the first moment enough are free:
Camera: only 0 free for this window (need 1). Next time 1 are free: Mon Aug 10, 17:00.
| Open Loans | what is out right now, overdue flagged |
| New Loan | customer lookup, add typed lines or free-text "loose" items, availability checked before it will save |
| Returns | scan a barcode to return that unit, from any loan |
| Reservations | future bookings, staged when fully allocated |
| Inventory | types and their units, photos, per-type availability |
| History | closed loans |
The Reservations screen is the day's work list, in order: what still needs pulling off the shelf, what is bagged up waiting for collection, what has already gone out.
Whoever actually works the counter does not need any of this README. There is a one-page guide for them — docs/OPERATING.md — written by situation ("someone is bringing something back") rather than by feature, meant to be printed and left by the machine.
A loan moves reserved → staged → out → returned. Staging is automatic: bind
the last outstanding unit on a reservation and it becomes staged; remove one and
it drops back. Returns are per-unit, so a partial return is just a loan with
some units still open.
There is no import screen. There is something more useful: the schema is small enough to hand to an AI along with whatever you already keep.
schema.sql is 69 lines and six tables. Give an assistant that file and your
spreadsheet — in any shape, from any system — and ask for SQL:
Here is the schema of a SQLite database, and my current inventory spreadsheet. Write me SQL INSERT statements that load my data into it. Wrap everything in
BEGIN;…COMMIT;. One row inunitsper physical item; create eachtypesrow once and reference it by a subquery on its name. Treat all codes as text — never drop a leading zero. Do not invent data: if something required is missing, leave that item out and list it for me instead.
Then, with the app closed:
# 1. Back up first, always. Close the app before you do.
cp compossible.db compossible-backup.db
# 2. Apply it. Python is already installed — the app needs it — whereas the
# sqlite3 command line tool is not standard on Windows.
python -c "import sqlite3;sqlite3.connect('compossible.db').executescript(open('load.sql',encoding='utf-8').read())"
# ...or, if you have it: sqlite3 compossible.db < load.sqlAnything wrong and nothing happens. The UNIQUE constraints on
units.barcode and customers.student_id reject duplicates, and a failure
inside the transaction rolls the whole thing back — so a bad file leaves the
database exactly as it was. That guarantee comes from the database, not from
code someone had to remember to write.
Not technical? DB Browser for SQLite is free, opens the file, and has an Execute SQL tab you can paste into.
This is worth stating plainly because it generalises: any application with a readable schema can be loaded this way, and an AI is a better translator between two arbitrary schemas than any import wizard, because it can read both.
Six tables, in schema.sql:
- types — a kind of thing you lend ("Camera"), with a photo and a list of accessories that should be in the bag
- units — the individual physical items, each with a barcode
- customers — borrowers
- loans — one entity for reservations and active loans; a reservation is a
loan with
status='reserved', which is why one availability check covers both - loan_lines — either a typed line (
type_id+ qty) or a loose item (free text, for one-off things not worth registering). ACHECKconstraint enforces exactly one of the two. - loan_line_units — which physical units filled a typed line, and when each came back
The customers.student_id column is a leftover of where this was built. It is
just a borrower reference — put a staff number, a phone number, or a name in it.
Python 3.12+ (3.10 should be fine; only tested on 3.12).
git clone https://github.com/Object-Orient/compossible.git
cd compossible
pip install -r requirements.txt
python app.pyFirst launch creates compossible.db and seeds three sample item types so the
screens have something in them. Delete the file to start over. There is no
migration story beyond two additive ALTER TABLEs at startup — this is a
single-counter app, and the schema is small enough to read in full.
The photos in static/photos/ are demo placeholders — ordinary objects on a
workbench, so the screens look like a real counter rather than a wireframe.
They are stored at 1024px, so clicking one on a type's page enlarges it enough
to actually inspect the item. Delete them as you add your own stock; the app
handles types with no photo.
setup.ps1 is a one-shot Windows bootstrap: it installs git and Python via
winget, clones, installs dependencies, and drops a desktop shortcut. It defaults
to %USERPROFILE%\compossible; pass -RepoDir to put it elsewhere.
To run it: Python, and nothing else.
| Python | 3.12+ (3.10 should be fine; only tested on 3.12) |
| Flask | the only required package — pip install -r requirements.txt |
| SQLite | standard library, nothing to install |
| pywebview | optional, and not in requirements.txt. Only for the native desktop window; pip install -r requirements-desktop.txt if you want it. Without it the app says so and serves in your browser |
No internet connection, no Node, no build step, no external services, no CDN. The stylesheet ships as a plain committed file. This matters more than it sounds: lending desks live on locked-down school and workplace networks where outbound requests are blocked.
To change the styling: Node, once.
static/app.css is generated from src/input.css by Tailwind CSS v3 and is
committed. You only need the toolchain if you are editing styles or adding
Tailwind classes to a template:
npm install # ~14 MB of build tooling, gitignored, never shipped
npm run build:css # rewrites static/app.cssOne trap if you do. Tailwind reads the templates as plain text and emits only the class names it literally finds there. A class assembled at render time —
border-+ a colour variable +-200— is never generated, and the element silently loses that style, usually only on a page with real data in it. Write class names whole. If one genuinely must vary, pass the complete class in as the variable; see theres_cardmacro intemplates/reservations.html.
python -m unittest test_availability -vFifteen tests covering the booking rules. They exercise type_availability
against an in-memory database directly — no Flask context, no HTTP, no fixture
files — because the function takes a connection rather than reaching for one.
If you change the availability logic, these are the contract.
- Single counter, no accounts. Anyone who can reach the port has full control, and nothing records who did what — with more than one person on the desk, "the system says it went out Tuesday" cannot tell you who released it. Meant to run on the desk machine, bound to localhost. Do not put it on a network you don't trust.
- No import screen, no bulk edit, no reporting. Stock is typed in one item at a time through the interface. For a large initial load, see Getting a lot of stock in at once — the schema is small enough to hand to an AI, which handles the one-off migration without this app needing an importer to maintain.
- Barcode scanning is just an input field that expects a scanner acting as a keyboard. There is no camera scanning.
- No email, no notifications, no reporting. Overdue items are shown, not chased.
Worth saying plainly, because the gap this fills is narrow:
- Snipe-IT is the mature self-hosted asset tracker. Far more capable — but it has no native reservation layer, which is the whole point here.
- Cheqroom and Reftab do booking properly and are commercial SaaS.
- Shelf is AGPL and self-hostable, and considerably more of a product than this.
- leihs, Koha, Evergreen are the serious open-source options for an institution with staff to run them.
compossible exists in the space none of those occupy: one file, one database, runs on the counter PC, does booking correctly.
Published as reference, and not actively maintained. It works and the booking logic is tested; issues and PRs may sit. Fork it freely.
MIT — see LICENSE.


