Ownware
Home›Lendra›Manual
Lendra · Manual

Lendra Manual, as shipped in the download

Lendra — User Manual

A lending desk for physical assets, run on your own server. Version 3.1.4 [src: app/controllers/api.php:18].

About this manual

Every statement here was written by reading Lendra's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug — the markers exist so you can check rather than take our word for it.

Nothing is described that the code does not do. Where something is deliberately limited, that is said plainly rather than left out.

What Lendra is

Assets go out to people and come back. The product is one small state machine over an asset's status and whether its loan is still open [src: app/src/Checkout.php:5], and the source calls that engine the moat, because correctness there is the whole value [src: app/src/Checkout.php:3].

available ──check-out──▶ checked_out ──check-in──▶ available
    │  ▲                                              │  ▲
    │  └──────────────── set-status ─────────────────┘  │
    └──────────────▶ maintenance / retired ─────────────┘   (not checkoutable)

A loan is open while its return timestamp is null [src: app/src/Checkout.php:5].

The four guarantees

The source states them as guarantees and covers each with tests [src: app/src/Checkout.php:13].

Only an available asset can go out. Checking out something already lent, in maintenance, or retired is refused with a reason that says which [src: app/src/Checkout.php:14], and the refusal names the actual state rather than a generic failure [src: app/src/Checkout.php:214].

Check-out is atomic. The flip from available to checked out is a conditional update inside a transaction, so two racing requests can never both hand out the same asset [src: app/src/Checkout.php:16].

Check-in closes the loan — recording the return time and the condition it came back in — and returns the asset to available. Checking the same loan in twice is refused [src: app/src/Checkout.php:19].

Overdue means an open loan whose due time has passed [src: app/src/Checkout.php:22]. Nothing is stored as "overdue"; it is a comparison.

How the atomic guard actually works

Only a row that is still available flips. If the update matches no rows, another request or a status change got there first, and the answer is a refusal rather than a second loan [src: app/src/Checkout.php:241].

That is worth knowing when two people work the same desk: "that asset was just checked out by someone else" is the system being right, not the system failing.

The check-out desk, and scanning

The desk screen is built around a scan. It opens with an autofocused field labelled "Scan or type an asset tag" [src: app/views/checkout.php:30] and the page itself says "scan a tag to begin" [src: app/views/checkout.php:8].

Behind it sits a lookup that matches an asset tag exactly first — because, as the source puts it, a scanner types the tag and nothing else — and only then falls back to a name or category search [src: app/controllers/v3.php:1321]. The match ignores letter case on both SQLite and MySQL [src: app/controllers/v3.php:1337] (before 3.1.4 it used a SQLite-only collation, and every scan failed on a MySQL install).

That is what makes an ordinary handheld barcode scanner work here without any driver or integration: the common kind emulates a keyboard, so it simply types the tag into that field and the exact match fires. Partial text works the same way for the times you are typing by hand [src: app/controllers/v3.php:1339].

The desk shows you the verdict before you commit, and the screen is explicit that the verdict is the real one: it refuses exactly what the check-out itself would refuse [src: app/views/checkout.php:31].

The lookup requires a signed-in user with permission to view assets [src: app/controllers/v3.php:1328].

What is not included is camera-based scanning or any hardware driver. Asset tags reach the field as text, however they get there.

Due dates

A due date is a local calendar date, converted to the end of that day in your configured timezone [src: app/src/Checkout.php:59]. Something due today is not overdue until that day is over.

A loan cannot start overdue. A due date in the past is refused, and the reasoning is specific: a back-dated loan silently poisons the overdue report, which is the most-looked-at screen in the product [src: app/src/Checkout.php:222]. Due today is fine [src: app/src/Checkout.php:227].

The check compares against the same clock the rest of the operation uses rather than the wall clock, so the rule holds on any timeline [src: app/src/Checkout.php:224].

Returns, and damaged items

A return records the condition the item came back in. Every grade puts it back on the shelf except damaged, which sends it to maintenance [src: app/src/Checkout.php:297]: a broken item is not issued to the next person and not offered to the reservation queue. When it is mended, set it back to available and the head of the queue is offered it, exactly as a return would [src: app/src/Checkout.php:328].

Reminders to borrowers

Two emails can go to a borrower, both through your own SMTP server and both off until you switch them on under Settings → Email → Write to the borrower:

  • Due back soon — one message per borrower listing every item of theirs falling due within the number of days you choose (1 by default) [src: app/controllers/v3.php:1154]. Each loan is reminded once per due date; the slot is claimed before the mail server is contacted, and released if the send fails. Items already overdue are left to the overdue notice.
  • Overdue — one message per borrower listing everything they hold late, and not again until the cadence you set has passed.

Lendra runs no background job, so something has to run them: cron/overdue.php on a schedule sends both (--dry-run names who would be written to), and so does the token-guarded /overdue/notify-cron [src: app/controllers/v3.php:1265]. The Email these borrowers button on the Overdue page sends the overdue notices only. The script answers only on the command line.

Reservations — the waiting list

An asset already out can have a queue [src: app/src/Reservations.php:3], and the rule is stated in the source in one paragraph "so it can be argued with" [src: app/src/Reservations.php:5].

Anyone may join. The queue is strictly first-come, first-served by reservation id, with no priorities [src: app/src/Reservations.php:6]. The reasoning is a good one: a lending desk that can be jumped is a lending desk people stop trusting [src: app/src/Reservations.php:7].

When the asset comes back, the person at the head is offered it — their reservation becomes claimable and a claim window opens, 48 hours by default [src: app/src/Reservations.php:9]. During that window the asset can be issued only to them.

If they do not collect it in time, the reservation expires and the offer passes on [src: app/src/Reservations.php:11], so the queue never stalls on somebody who has stopped caring [src: app/src/Reservations.php:12]. Once the queue empties, the asset is simply available again.

Why an offer rather than an automatic loan

Because issuing an asset to somebody who is not standing there creates a loan nobody can return [src: app/src/Reservations.php:16]. The reservation reserves; a human still checks it out [src: app/src/Reservations.php:16].

Reservations do not weaken the double-issue guard

This is the design point worth understanding. The queue is asked before the atomic flip and never in place of it — it adds a condition on issuing rather than a second route around the guard [src: app/src/Checkout.php:231].

The source says the same thing from the other side: the atomic update stays exactly as it was, and reservations only add the question "is this asset promised to somebody else right now?" [src: app/src/Reservations.php:19].

When the borrower being issued to is the one holding the claim, that reservation is settled inside the same transaction as the loan, so the two can never disagree [src: app/src/Checkout.php:258].

Two refusals in the queue

A second place in line is not a stronger claim — joining twice is refused [src: app/src/Reservations.php:72].

A retired asset cannot be reserved, because it is not coming back [src: app/src/Reservations.php:78].

An inactive borrower can neither borrow [src: app/src/Checkout.php:210] nor join a queue [src: app/src/Reservations.php:82].

Users and roles

Three roles [src: app/src/Perms.php:21]. The source names the line it draws, and it is not "who can type":

who can make an asset or a person disappear — issuing and returning is the daily job of whoever is at the desk; retiring an asset, editing a borrower's details, anonymising them, or restoring the database are things you want a name attached to and fewer hands on [src: app/src/Perms.php:5].

Permissionviewermemberadmin
dashboard.viewyesyesyes
assets.viewyesyesyes
borrowers.viewyesyesyes
loans.viewyesyesyes
overdue.viewyesyesyes
loans.issue / loans.return—yesyes
assets.create / assets.update / assets.photo—yesyes
borrowers.create / borrowers.update—yesyes
reservations.manage—yesyes
everything else——yes

A viewer sees the catalogue, who has what, and the overdue list, and nothing else [src: app/src/Perms.php:10]. A member is the desk [src: app/src/Perms.php:11]. An admin adds retiring assets, borrower deletion and anonymisation, settings, the team and restore [src: app/src/Perms.php:12].

Grants below admin are enumerated rather than wildcarded, deliberately: a tidy *.view quietly hands a viewer the audit trail and the team roster, and that should be a decision rather than a pattern match [src: app/src/Perms.php:14].

Backups are admin-only

Both database downloads, /backup.json [src: app/controllers/api.php:297] and /backup.sqlite [src: app/controllers/api.php:323], require settings.manage, which only an admin holds, and the Security page shows the buttons to admins only. A backup contains every table, with only credential columns redacted [src: app/controllers/api.php:306] — the borrower list, the team roster and the audit trail — so it sits with the same role as restore. Before 3.1.4 both downloads checked sign-in alone, and a viewer login could take the whole database.

The API and agent access

Five tools are exposed to an agent, of which two write [src: app/controllers/v3.php:24]: listing assets, asset detail and the overdue report on the read side; issuing and returning on the write side.

An API key carries its holder's role, and a key belonging to a viewer is refused the write tools with an explanation naming the role [src: app/src/Perms.php:58].

The instructions given to an agent are worth quoting, because they teach it something a naive client would get wrong: one asset can have at most one open loan at a time, the server enforces that atomically, and an issue can legitimately fail with "already checked out" even though a list fetched a moment ago said otherwise [src: app/controllers/v3.php:20].

Dates sent in are local calendar dates; times returned are UTC [src: app/controllers/v3.php:23].

What Lendra does not do

It does not take payment, deposits or fines. It records what went out, to whom, when it is due and what condition it came back in.

It does not issue an asset to somebody who is not there [src: app/src/Reservations.php:16]. A reservation becomes an offer with a deadline; a person still checks it out.

It does not prioritise a queue [src: app/src/Reservations.php:6]. If your desk needs somebody to jump the line, cancel the reservation ahead of them — deliberately, with the cancellation on the record — rather than expecting the software to arrange it.

It does not decide that a loan is late for you. Overdue is a comparison between a due time and now [src: app/src/Checkout.php:22]; what you do about it is yours.

← Back to Lendra · Quickstart · API · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →