Rentara Manual
Rentara — User Manual
Properties, tenancies, rent and maintenance, on your own server. Version 4.1.3 [src: app/controllers/api.php:17].
About this manual
Every statement here was written by reading Rentara'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 Rentara is
Properties and units, tenants and leases, the rent they owe and the maintenance they report. The operations logic sits in one pure module that the screens and the test suite both call directly, with no database and no network involved [src: app/src/Rent.php:3].
Money is integer cents; dates are YYYY-MM-DD and months YYYY-MM [src: app/src/Rent.php:15]. Because ISO dates sort lexicographically the same way they sort chronologically, plain string comparison is used for every date boundary [src: app/src/Rent.php:16].
Rent charges
Charge generation is idempotent. A lease that already has a charge in a month is skipped, so re-running never double-creates [src: app/src/Rent.php:100]. Only active leases produce charges [src: app/src/Rent.php:120].
There is a second guard in the same function against duplicate leases appearing in the input [src: app/src/Rent.php:129], so a bad query upstream cannot produce two charges either.
Arrears means an unpaid charge past its due date — the boundary being strictly before today [src: app/src/Rent.php:57].
Late fees
Off by default: with both the flat amount and the percentage at zero, the automation is disabled entirely [src: app/src/Rent.php:367].
A fee is a flat amount plus a proportion of the overdue rent expressed in basis points, clamped to the integer range [src: app/src/Rent.php:343].
Four rules govern when one is charged, and they are worth knowing because they are what stops a late-fee system running away.
One fee per overdue period, ever [src: app/src/Rent.php:385].
Fees never compound. Only rent charges are considered; a late fee cannot itself attract a late fee [src: app/src/Rent.php:377].
Grace is inclusive of its last day. The source gives the worked example: a charge due 1 July with five grace days incurs no fee through 6 July, and a fee from 7 July [src: app/src/Rent.php:353].
The fee's own due date is deterministic — the original due date plus the grace period plus one day — so re-running the catch-up on any later day produces the identical row [src: app/src/Rent.php:354].
That last one matters more than it sounds. It means you can run the process late, or twice, or after restoring a backup, and the books do not change.
A fee per day, and a cap (3.2)
Two more settings shape the rule. Rentara applies the rule you set; it does not know the limits where your properties are, so set them to what applies there.
- Then per day. After the grace days, a fee for every day until the rent for that period is paid in full; a period paid inside its grace days earns nothing, and one paid three days late earns three days [src: app/src/Rent.php:332]. It is posted as its own line ("Late fee per day — Rent 2026-06"), which grows while it is unpaid; once a payment has reached it, the next days are a new line and the paid one never changes [src: app/src/Billing.php:97].
- A cap per rent period. A fixed amount, a percent of that period's rent, or both (the lower wins) [src: app/src/Rent.php:319]. The one-off fee and the daily fee together stay under it.
- Nothing posted is reduced. Lowering the rule changes what is posted from then on, not what is already on the ledger.
- Both kinds count as fees on the owner statement and in every total.
⭐ The owner portal (3.2)
Each owner named on a property can be given one link to their own statements, with no login: their properties (units, let, rent owed today), and for this month or any of the eleven before it, the rent and fees collected, what is due and unpaid, and the repairs paid for, with a CSV. It shows nothing of another owner and no tenant's email or phone.
- Only the link's SHA-256 is stored, so it is shown once; a new link stops the old, and Withdraw stops it outright [src: app/src/Owners.php:31] [src: app/src/Owners.php:47]. Making, emailing and withdrawing links is for the letting desk (
portal.manage). - The monthly email (off by default, administrators switch it on): on the first reminder run of each month, every owner with an email address on their properties and a live link is sent last month's statement link, once per owner per month, through your own mail server [src: app/src/Owners.php:110]. Each email carries a fresh link, which replaces the one before.
Leases and units
Assigning an active lease to a unit occupies it; ending the lease frees it [src: app/src/Rent.php:186].
Leases expiring within a window are listed soonest first, with the window inclusive at both ends and already-expired leases excluded [src: app/src/Rent.php:138].
Lease-ending emails
Off until you tick "a lease is 60 days, and again 14 days, from ending" under Settings → Email. The check lists every active lease that has newly reached 60 days, or 14 days, from its end date, in one message to your notification address, through your own SMTP server [src: app/controllers/app.php:95]; each lease is mentioned once at each stage, and a renewed lease (a new end date) starts over. Slots are claimed before the mail server is contacted and released if the send fails [src: app/controllers/app.php:122]. It runs when cron/lease-ends.php runs (once a day; --dry-run sends nothing) or when you press Check leases now [src: app/controllers/app.php:172]. Nothing is written to a tenant.
⭐ Part payments (3.2)
A tenant who pays 1,500 of 2,100 can now be recorded honestly. On the lease page, Record a payment takes any amount and pays the oldest unpaid charges first [src: app/src/Billing.php:219]; the charge it only partly covers is split: its row keeps the paid part (so "collected" counts it on the day it arrived) and a balance row for the same period and due date carries the rest [src: app/src/Billing.php:196]. Because of that, every collected, outstanding and arrears figure in the product stays right as it was written, the catch-up still posts one rent row per period, and a period with a paid part and an unpaid balance still gets at most one late fee. The REST API takes the same payment [src: app/controllers/api.php:82]. A payment larger than what is owed is refused, with the amount owed.
⭐ A bank statement, matched to rent (3.3)
From the rent roll, Bank statement opens it for anyone who can record rent [src: app/controllers/v32.php:320]. Upload the CSV your bank exports: it needs a date column and the money in, either as an amount column or as a "paid in" column, and a description helps. Say whether its dates are day-first or month-first; a date written 2026-09-25 is read as it is either way, and a date that does not exist is named as a line not read [src: app/src/BankImport.php:34]. Money out (a minus sign or brackets, or a line with nothing paid in) is left aside [src: app/src/BankImport.php:49]. A statement holds up to 2,000 payments in [src: app/src/BankImport.php:64].
For each payment in, Rentara proposes the tenancy it belongs to:
- By name: the tenant's surname, as a whole word in the line's description. If the amount is more than that tenancy owes, nothing is proposed and the line says so [src: app/src/BankImport.php:115].
- By amount: with no name in the line, the one tenancy whose oldest unpaid charge, or whole balance, is exactly that amount, marked for you to check. When two tenancies fit, or none does, nothing is proposed [src: app/src/BankImport.php:121].
You can change or clear any proposal. Nothing is recorded until you press Record the payments chosen; each chosen line is then recorded as a payment typed in would be: a transfer on the statement's date, oldest charges first, never more than is owed [src: app/src/BankImport.php:145], with the same webhook and receipt [src: app/controllers/v32.php:360]. Each line carries a reference made from its date, amount, description and place among identical lines in the file [src: app/src/BankImport.php:73], and a payment's reference is unique [src: app/src/Database.php:533], so the same statement uploaded again, or one that overlaps, records each payment once and shows the rest as already recorded [src: app/controllers/v32.php:361]. Two identical lines in one statement are two payments.
⭐ Rent reminders and your arrears digest (3.2)
Off until you switch them on in Settings → Rent reminders [src: app/controllers/v32.php:132], through your own SMTP server:
- Rent coming up: a note to the tenant a set number of days before a charge is due (default 3), once per charge.
- Outstanding: a note at the steps you set after the oldest unpaid due date (default 3 and 10 days) [src: app/src/RentRemind.php:107]. A step keeps trying for up to seven days and the next one takes over when its day comes, so a tenancy first seen long in arrears gets one note, never a run of them.
- At most one note per tenant per day, a daily cap, a pause on each lease, and every note claimed before the mail server is contacted, so two page loads never send it twice; a refused send gives its claim back and ends the run [src: app/src/RentRemind.php:207].
- The wording is informational [src: app/src/RentRemind.php:149]: what is owed and since when, the tenant's own ledger link, and "This is a reminder, not a formal notice." Notice periods and late-payment rules differ from place to place; Rentara does not serve notices.
- Your digest: one email a day to your own address listing every tenancy in arrears and every lease ending within 60 days [src: app/src/RentRemind.php:278]. Nothing in it goes to a tenant.
They go a few at a time when the dashboard opens; a daily cron can call /cron/reminders?t=<scheduled-backup token> [src: app/controllers/v32.php:168]. Before 4.1.2 the shipped .htaccess closed every address under /cron/, so that line answered 403; now only the files in the cron/ folder are closed and the address reaches the app [src: app/.htaccess:23]. Reminders (from Settings) shows exactly what goes out today, lease by lease, and sends nothing [src: app/controllers/v32.php:152].
⭐ Text reminders on your own SMS account
Rent texts are off until you connect an SMS gateway you already pay for and switch them on, in Settings → Text reminders [src: app/controllers/texts.php:10]. You pay for the texts, not Rentara and not your tenants: each one goes through your own account, your gateway bills you at its own rate, and Rentara sells no credits and adds nothing to the price.
- Any HTTPS gateway. Rentara sends one HTTPS POST per text, as JSON or form-encoded, with your three field names for the number, the sender and the message (
to,from,textby default; Twilio's areTo,From,Body), and your key as a Bearer token, as HTTP Basic (user:password, for TwilioAccountSid:AuthToken) or not at all. Any 2xx answer counts as sent [src: app/src/Sms.php:72]. It is the same sender Slotly uses. A tenant's number goes as digits, with a leading + kept; many gateways, Twilio among them, expect the international form, so type tenants' numbers that way (+1 555 201 4499). - Only tenants who agreed. A tenant gets texts only once their agreement is recorded, in one of two places: the tick Text this tenant about rent at this number (they agreed to texts) on the tenant form, recorded with the time and the name of the person who ticked it [src: app/controllers/texts.php:58], or Text me about rent on the tenant's own link, recorded as theirs [src: app/controllers/texts.php:69]. A phone number on file is not agreement: the queries that pick who gets a text only include tenants marked as agreed [src: app/src/Sms.php:314].
- Rent coming up. A text a set number of days before the due date,
3, 0by default (three days before, and on the day itself). Each step goes once per rent period, so next month's rent gets its own texts. Rent first seen inside a later step gets that step's text only, and paid rent gets none [src: app/src/Sms.php:259]. - When a late fee is added. When your late-fee rule (above) posts a fee, the tenant gets one text for that rent period, saying the fee and what is owed now, and "This is a reminder, not a formal notice." A fee posted more than three days ago is never texted, and when switching on a fee rule posts fees for several old months at once, the tenant gets one text, not one per month [src: app/src/Sms.php:334]. This text has its own switch.
- Every text names you and ends with a stop link. The first words are your organization name from Settings. When the lease has a tenant link, the text carries it: "Pay by card or see your account" with card payment on, "See your account" without. The last line is the tenant's stop link [src: app/src/Sms.php:290].
- How a tenant stops them. The stop link opens a page with one Stop rent texts button; opening the page changes nothing, so a phone's link preview cannot stop texts by itself [src: app/controllers/texts.php:93]. The tenant's own link has the same button. You can also untick the box on the tenant form. When a tenant has stopped texts, the tenant form says so, and texts start again only when someone records a new agreement.
- Your send window. Texts go between 09:00 and 20:00 in your time zone by default; you set both times, within one day (the start comes before the end, so no window runs through the night). A text that comes due outside the window waits until it opens. Saved hours that do not run from an earlier to a later time fall back to 09:00 to 20:00, and Settings says so [src: app/src/Sms.php:194].
- What keeps it from nagging. At most one text a day per tenancy (when rent is due on the same day a late fee is added, the due-day text goes first and the late-fee text the next day), a daily cap (the number set for email reminders, counted separately for texts), and a lease paused on its page gets neither emails nor texts.
- Each text goes once. Rentara claims a text before contacting your gateway: one row under a unique key made of the lease, the rent period and the step. Two page loads or cron runs at the same moment cannot both send it. Just before each text, Rentara reads the tenant's agreement and the charge again, so a stop or a payment a moment earlier stops the text.
- When your gateway refuses. The text is logged with exactly what your gateway answered, texts stop for the rest of that day, and the dashboard, Settings and Reminders say so. A failed text is tried again on a later day while it is still due, never the same day. Nothing on the page fails because a gateway did. Send test sends one text to a number you type, works before texts are switched on, and shows what your gateway answered; a test that goes through starts texts again the same day [src: app/src/Sms.php:497].
Texts go out with the rent reminders: a few at a time when the dashboard opens, and all of today's (up to the cap) when a daily cron calls /cron/reminders?t=<scheduled-backup token>. Reminders lists today's texts, word for word, beside the email reminders, plus the most recent texts and what happened to each; opening it sends nothing. The log keeps the words of each text, never the tenant's link or stop link. The gateway key is write-only in Settings and left out of backups. Anonymizing a tenant stops their texts and removes their number from the text log. On a demo install a text is recorded and no connection is opened.
⭐ Pay by card from the tenant's link (3.2)
Off until you switch it on in Settings → Get paid by card with your own Stripe secret key and webhook signing secret [src: app/controllers/v32.php:112]. When something is owed, the tenant's private link shows Pay by card for the balance; the card is typed into Stripe's page, never into Rentara [src: app/src/Pay.php:85]. Stripe reports the payment to /hooks/stripe with a signed event; only a verified, recent, paid checkout for that lease in your currency records anything, once per checkout, oldest charge first [src: app/src/Pay.php:144]. Reminders to that tenant then offer the same link.
Maintenance tickets
The transition table is small and explicit [src: app/src/Rent.php:161]:
| From | May move to |
|---|---|
open | in progress, resolved |
in_progress | open, resolved |
resolved | open |
A resolved ticket can be reopened, and the agent instructions give the reason plainly: reopening a job that was not actually fixed is a real thing that happens [src: app/controllers/v3.php:31].
Resolving stamps the time and preserves an existing stamp; moving to any other state — including reopening — clears it [src: app/src/Rent.php:174].
The tenant portal
A lease can be issued a self-service link, and revoked again [src: app/index.php:119].
The portal is public and token-gated [src: app/index.php:155], reachable at a token URL where a tenant can view their tenancy and submit a request [src: app/index.php:156]. The source describes it as carrying no personal information beyond the lease itself [src: app/index.php:155].
Issuing and revoking those links is a portal.manage permission, held by agents and administrators.
Users and roles
Three roles. The source names the asset as money movement and the tenancy record, and the reasoning is the sharpest justification for a role split in the catalog [src: app/src/Perms.php:5]:
Marking rent paid is not data entry — it is an assertion that money arrived, and it is what a tenant will point at months later when a deposit is withheld. Ending a lease, deleting a tenant or wiping a property destroys the record a deposit dispute rests on. Changing the late-fee rule silently changes what every future arrears figure means [src: app/src/Perms.php:7].
The split it draws: the letting desk runs the day, the owner's office holds the record [src: app/src/Perms.php:12].
This table was produced by executing the product's own permission function against every permission the routes ask for.
| Permission | viewer | member | admin |
|---|---|---|---|
dashboard.view | yes | yes | yes |
properties.view | yes | yes | yes |
tenants.view | yes | yes | yes |
leases.view | yes | yes | yes |
rent.view | yes | yes | yes |
tickets.view | yes | yes | yes |
statements.view | yes | yes | yes |
exports.run | yes | yes | yes |
rent.record | — | yes | yes |
properties.edit / tenants.edit / leases.edit | — | yes | yes |
tickets.edit | — | yes | yes |
portal.manage | — | yes | yes |
properties.delete / tenants.delete / leases.delete | — | — | yes |
settings.view / settings.manage | — | — | yes |
team.view / team.manage | — | — | yes |
audit.view | — | — | yes |
Recording rent is a member permission; deleting anything is not. That is the split in practice — an agent can assert that money arrived, but only the owner's office can remove the record of it.
The late-fee rule sits behind the settings permission, so it is an administrator's decision. Changing it changes what every future arrears figure means [src: app/src/Perms.php:9].
A viewer reads the rent roll, arrears, leases, tickets and statements, sees no tenant contact details it does not need, and can change nothing [src: app/src/Perms.php:14].
Backups follow the model here
Rentara's database backups are permission-gated, requiring the settings permission rather than a bare sign-in [src: app/controllers/api.php:349], and so are the restore page and the scheduled-backup token. A viewer or member cannot download the database.
The API and agent access
Seven tools are exposed to an agent, of which two write [src: app/controllers/v3.php:34]: properties, leases, lease detail, the rent roll and the arrears report on the read side; creating and advancing a maintenance ticket on the write side.
The MCP tool set contains no payment tool — an agent working through it cannot assert that money arrived.
The REST interface is a different matter, and the distinction is worth stating precisely rather than glossed. POST /api/leases/{id}/payments does record a payment against a charge [src: app/controllers/api.php:240], and it is gated by exactly the permission a person needs: rent.record, checked against the key's own role [src: app/controllers/api.php:82].
So a key issued to a viewer cannot record a payment, and a key issued to an agent or administrator can. That is the same rule the office works under, applied to the key — not a bypass of it.
Payment methods are recorded, not processed: cash, check, transfer, card or other [src: app/src/Billing.php:160]. "Card" means a card payment happened somewhere else and is being written down.
The instructions tell an agent that a ticket moves only along the product's own transition table [src: app/controllers/v3.php:30], and that arrears and rent-roll figures come from the same engine the screens render [src: app/controllers/v3.php:33] — so an agent's numbers and the office's numbers cannot diverge.
What Rentara does not do
It does not hold money or card details. Recording that rent arrived is an assertion by a person [src: app/src/Perms.php:7]; with card payment switched on (3.2), the tenant pays on your own Stripe account's page and Rentara records what Stripe reports [src: app/src/Pay.php:166]. The money moves from Stripe to your bank.
It does not connect to your bank. A statement is a CSV file you export from your bank and upload; Rentara proposes and you confirm (3.3) [src: app/src/BankImport.php:6].
It does not credit an overpayment by itself. A payment larger than what is owed is refused when you record it [src: app/src/Billing.php:219]; if a card payment turns out larger than what is still owed when Stripe reports it, the excess is written on the payment for you to refund or apply.
It does not compound late fees [src: app/src/Rent.php:377], and it charges at most one per overdue period [src: app/src/Rent.php:385].
It does not double-charge if you run it twice [src: app/src/Rent.php:100], and a late-fee catch-up run on any later day produces the same rows it would have produced on time [src: app/src/Rent.php:353].
It is not a deposit scheme, a credit reference or a tenancy-law adviser. It holds the record that a dispute would be argued from; it does not decide the dispute.
Tenant data requests: what an erase removes and what it keeps (4.1.1)
A tenant's page carries Data requests: download everything Rentara holds about them as JSON, or anonymize them once the tenancy is over (whoever may delete tenants). Anonymizing replaces their name, email, phone and notes and the address the tenant portal recorded when they reported a fault, stops their texts and removes their number from the text log and their address from the reminder log. It is refused while a tenancy is active or money is owed. The tenancy, ledger and maintenance history stay: they are the record of a tenancy, not of a person.
Kept: the documents filed on their lease, with their titles and files, such as the tenancy agreement and ID copies, because they are contract records with their own keeping periods.
Looking after your install (from 4.0)
Updates from inside Rentara
Updates in the menu opens the Updates page (/updates) [src: app/views/updates.php]. Paste your license key once and press Save key: either key works, the one in your Lemon Squeezy receipt or the one on your order page (it starts with OWX-). Check for updates asks the store for the newest release your license covers; Rentara never checks by itself.
When a new version is ready, Back up and update to that version backs up your data and the current version, installs the new one and checks it. If the update is interrupted, or the new version's database step or that check fails, the previous version is put back by itself. Rentara is unavailable for about a minute while this runs. The update replaces Rentara's files, including any you edited, except .htaccess, .user.ini and robots.txt: an edited copy of those is kept, with the new version's copy beside it as .new to compare and merge. The Health page lists any file that differs from the version installed, so you can see that before you update. A copy of the previous version (data/updates/code-<version>/) and a backup of your data from just before the update stay in data/updates/ [src: app/src/Updater.php]. If Rentara shows an error page after an update anyway, that is your way back: copy the files from code-<version>/ over the app's files, then restore the backup on the backup page. Updating from the page needs PHP's sodium and Phar extensions; if one is missing, the Updates and Health pages say so and updates stay by hand. Where Rentara is hosted for you, the host updates it and the page says so.
Reaching 4.0 from a 3.x version is the last update done by hand: replace the files as before (config.php and data/ stay, and the database migrates itself on the next page load). From 4.0 on, the Updates page does it.
Starting with sample data
The installer offers Start with sample data: two properties, eight units and six tenants with their rent history to try things on. You can remove it in one click. [src: app/install/index.php:119]. It is ticked by default; untick it to start empty. The sample is added when you open the first page after the installer.
When you are done with it, press the remove button in the sample banner. It asks "Remove the sample properties, units, tenants and leases? Your own records stay." Only the sample goes: your own records stay, and a sample record that one of your own records uses is kept and named in the report [src: app/src/SampleData.php].
Backups by themselves, and a copy somewhere else
On the backup page (/restore, linked from Settings), Back up once a day by itself is on by default; untick it to stop [src: app/views/restore.php:126]. The first visit at least 24 hours after the last backup starts the next one, after the page has loaded, so no cron job is needed. The newest 14 stay in data/backups/, with credentials left out of them (a restore keeps the live values for those). Back up now takes one straight away.
To keep a copy somewhere else as well, fill in an S3-compatible bucket (Backblaze B2, Wasabi, AWS S3 or your own MinIO): the bucket address (an https endpoint), the region, the bucket name, an optional folder, and the access key ID and secret key. Leave the address empty to keep backups on this server only. The copy goes only to the bucket you set; nothing is sent to Ownware. Once a week Rentara reads the newest backup back through the restore's own dry run and keeps the result for the Health page [src: app/src/BackupTargets.php].
On Nginx: keep data/ private
On Apache, the .htaccess that ships with Rentara keeps data/ (your backups included), src/, config.php and the other private files off the web. Nginx does not read .htaccess, so add these four blocks to your server block, above the block that passes .php files to PHP (Nginx uses the first one that matches):
location ~ ^/(src|data|bin|tests|controllers|deploy)/ { deny all; }
location ~ ^/cron/.+\.php$ { deny all; }
location ~ ^/views/.+\.php$ { deny all; }
location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }
Then open /config.php in a browser: it should answer 403. Only the template files under views/ are closed, not the folder: the app's own saved-view addresses start with /views/ too, and they must reach it.
A complete server block for a new site (PHP-FPM 8.3; change the name, the folder and the socket to yours):
server {
listen 80;
server_name example.com;
root /var/www/rentara/app;
index index.php;
location ~ ^/(src|data|bin|tests|controllers|deploy)/ { deny all; }
location ~ ^/cron/.+\.php$ { deny all; }
location ~ ^/views/.+\.php$ { deny all; }
location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }
location = /install/ { rewrite ^ /install/index.php last; }
location / { try_files $uri /index.php?$query_string; }
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
The Health page
Health in the menu (/health) shows, one line each, whether the install is looking after itself: the PHP version and extensions, disk space, whether data/ is writable, email, the age of the last backup, the offsite copy, the weekly check, whether an update is waiting, and any file that differs from the version installed. Each line says what to do when it is not fine. The page only reads: it never changes anything and never contacts anything [src: app/src/Health.php].
Connect it to other tools (Zapier, Make, n8n)
Rentara sends a webhook the moment something happens and accepts calls to its API, so Zapier, Make and n8n can connect to it without a plugin. The steps, the events and example recipes are in API.md, in the section "Connect it to Zapier, Make or n8n"; the API page of Rentara on the store shows the same file [src: app/API.md].
New in 4.1.2: sign-in limits
New in 4.1.2 (security update): Two-step codes are now limited (10 wrong per person or per address in 10 minutes), and wrong passwords count per account as well as per address. An installed app whose database does not answer keeps its web installer closed instead of showing the install form. On Ownware Cloud, opening the app from the console starts a fresh session.
Wrong passwords are counted per address and, since 4.1.2, per account too (the email typed, in any case, stored only as a hash). After 10 wrong passwords in 10 minutes the sign-in at /login answers "Too many attempts — wait a few minutes and try again." for that address, and for that account from any address. A right password clears both counts [src: app/src/LoginRate.php:40].
Wrong two-step codes (for people who turned two-step sign-in on under /security) are counted per person and per address. After 10 wrong codes in 10 minutes the code step answers "Too many wrong codes. Wait ten minutes, then sign in again. A recovery code works in place of the code." even for the right code, and the half-finished sign-in is dropped, so the password is asked again [src: app/src/LoginRate.php:91]. The right code clears that person's count; an address's count runs out ten minutes after its wrong codes. Before 4.1.2 the code step had no limit.
If you are locked out: wait ten minutes, then sign in again with your password and a current code, or one of your recovery codes (each works once). If you lost both the authenticator and the recovery codes, whoever runs the server can turn two-step sign-in off for you in Rentara's database (by default the SQLite file in data/, or your MySQL database): UPDATE users SET totp_secret = '' WHERE email = 'you@example.com'; Then sign in with your password and turn it on again under /security.
On Ownware Cloud, opening Rentara from the console starts a fresh session [src: app/index.php:63].
The web installer stays closed when the database does not answer: with config.php in place, /install now answers 503 "This app is already installed (config.php exists), but its database is not answering, so the installer stays closed." and writes nothing until the database answers again. Before 4.1.2 a database outage (or a missing SQLite file) showed the install form there, and whoever found it could set up a new administrator. To install again from scratch, remove config.php first [src: app/install/index.php:20].
New in 4.1.3: security update
New in 4.1.3 (security update): no other site can put Rentara's pages in a frame; the /mcp address refuses requests from other sites' pages; company sign-in is stricter; and the audit CSV treats one more first character as text. Nothing else changed: no feature, screen, setting or database table, and your data, config.php and data/ are not touched.
No other site can frame these pages. Every page now answers Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN, so a page on another site cannot show Rentara in a hidden frame and trick a signed-in person into pressing a button [src: app/index.php:4]. The installer answers frame-ancestors 'none' and X-Frame-Options: DENY [src: app/install/index.php:4]. If you had Rentara framed inside another site of your own, that frame now stays empty; link to it instead.
/mcp refuses other sites. A request whose Origin header names another site gets 403 before anything is read. A desktop or server client sends no Origin and is served as before, and so is a page at Rentara's own address [src: app/src/Mcp.php:272].
Single sign-on. The sign-in now uses PKCE (S256), so an intercepted sign-in code is of no use on its own; a sign-in that comes back to a different browser session is asked to start again [src: app/src/Oidc.php:228]. With Google as the provider, a company address needs a Google Workspace account of that same domain (the token's hd claim), and Google must report the address as verified, so a personal Google account made with a work address no longer gets in [src: app/src/Oidc.php:215]. Gmail addresses work as before, other providers are unchanged, and password sign-in is not affected.
The audit CSV. A cell that starts with a carriage return now opens in a spreadsheet as text, as cells starting with = + - @ or TAB already did [src: app/src/AuditLog.php:138].
Docker. The Dockerfile now installs PHP's zip extension, so a Docker install can make the backup with files [src: app/Dockerfile:7].
Also changed, with no effect on a running install: the corner card on a public demo offers only the purchase, and API.md no longer shows the config example of a product we retired.