Home›Vendra›Manual
Vendra · Manual

Vendra Manual

Vendra — User Manual

A self-hosted point-of-sale and inventory system. Version 4.1.3 [src: app/controllers/v3.php:78].

About this manual

Every statement here was written by reading Vendra'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 Vendra is, and what runs it

Vendra is a till and a stock ledger in one application, for a single shop. Money is held as integer cents throughout, so no total is ever the result of floating-point arithmetic [src: app/controllers/v3.php:80].

It runs on PHP and stores its data in either SQLite or MySQL; the driver is chosen in configuration and defaults to SQLite [src: app/src/Database.php:19]. There is no build step and no separate migration command.

Signing in and where you land

Sign in at /login [src: app/controllers/pos.php:43]. After a successful password sign-in you are sent to the application root [src: app/controllers/pos.php:34]. If two-step verification is enrolled you are sent to /login/2fa first [src: app/controllers/pos.php:32].

Single sign-on is available as an alternative route in, and it does one thing the local form does not: it sends a cashier straight to /pos and everyone else to the dashboard [src: app/controllers/v3.php:769].

Users, roles and what each may do

Vendra has four roles: cashier, viewer, manager and admin [src: app/src/Auth.php:67].

Two of those are older than the other two, and the distinction matters when you upgrade. Vendra originally had only admin and cashier [src: app/src/Auth.php:2]. The permission ladder added viewer and manager as an extension rather than a replacement, and existing installations are untouched by it — every stored role is still admin or cashier, both keep exactly the rights they had, and the two new roles exist only once somebody assigns them [src: app/src/Auth.php:53].

The source sets the four out in a comment, and they divide like this: the cashier is the shop floor, with the till, holds and returns but no catalog, no money reports and no administration; the viewer is a read-only back-office seat, the accountant who sees reports and lists and changes nothing; the manager holds catalog, purchases, stock, customers and reports, but not users, settings, API keys or restore; and the admin holds everything, including the things that can lock people out or leak credentials [src: app/src/Auth.php:45].

What each role actually holds

The table below was produced by evaluating Vendra's own permission map rather than by reading it, against the seventeen permissions the controllers really enforce [src: app/src/Auth.php:67].

Permissioncashierviewermanageradmin
pos.useyes—yesyes
sale.createyes—yesyes
product.viewyesyesyesyes
catalog.view—yesyesyes
product.edit——yesyes
customer.viewyesyesyesyes
customer.edityes—yesyes
purchase.view—yesyesyes
purchase.edit——yesyes
stock.view—yesyesyes
stock.verify——yesyes
stock.adjust——yesyes
report.view—yesyesyes
audit.view—yesyesyes
view.save——yesyes
user.edit———yes
settings.edit———yes

Two rows in that table are deliberate and worth understanding, because getting either wrong would loosen access quietly.

A cashier holds product.view but not catalog.view. The till has to be able to look a product up, and so does the reporting tool described later — but the back-office catalog screen is gated on catalog.view instead, precisely so that the right to look up a price does not hand the shop floor the back office [src: app/src/Auth.php:58].

A cashier also holds customer.view and customer.edit. That is not a new grant: the till has always been able to add a customer mid-sale, and the permission map records the behavior that already existed rather than introducing it [src: app/src/Auth.php:63]. The customer list is gated on being signed in at all, for the same reason [src: app/controllers/admin.php:369].

Unknown or legacy roles fall through the same map rather than being special-cased [src: app/src/Auth.php:80].

The till

Opening a register

A shift begins by opening a register session with the cash you are starting with [src: app/src/Registers.php:22]. Money paid in or out during the shift — a float top-up, a supplier paid from the drawer — is recorded against the session with a type, an amount and a reason [src: app/src/Registers.php:42].

Ringing a sale

A sale is quoted first and then checked out [src: app/src/Sales.php:22]. Checkout takes the lines, a sale-level discount, the payments, the session, the user, an optional customer and a note [src: app/src/Sales.php:101].

Payments are an array, not a single value, which is the important detail: one sale can be settled across several tenders, so a customer may pay part in cash and the rest by card. The methods carried in the source are cash, card and other [src: app/src/Sales.php:101].

Holding a sale

A sale in progress can be parked and picked up again later, which is what you want when somebody has forgotten their wallet and there is a queue. Holds are created, listed, resumed and deleted from the till, and all four actions require the till permission [src: app/controllers/pos.php:141].

Returns

Items are returned against the original sale, with a method and a reason recorded [src: app/src/Sales.php:198].

Receipts

A receipt is rendered as text from the sale, its items, its payments and your store settings [src: app/src/Sales.php:266].

Closing the register

Closing takes the counted cash and computes over or short as counted minus expected, storing the counted figure, the expected figure and the difference between them [src: app/src/Registers.php:82]. The session summary is available before you close [src: app/src/Registers.php:53].

Closing produces a Z-report [src: app/controllers/pos.php:224]. Access to it follows a rule worth knowing: you may read a Z-report if you hold the reporting permission or if the session was your own — so a cashier can always read their own shift and nobody else's [src: app/controllers/pos.php:224].

Products and stock

Stock cannot be oversold

Every movement goes through one function, and a movement that would take stock below zero is refused rather than clamped [src: app/src/Inventory.php:111].

The refusal is worth describing exactly, because it is stronger than it looks. The decrement is written as a single conditional update — the row is reduced only where the quantity on hand is already at least the amount being taken — and a zero row count is treated as "this would oversell" [src: app/src/Inventory.php:111]. It is one atomic statement rather than a read followed by a write, so two tills selling the last item at the same moment cannot both succeed.

The stock ledger

Every movement is a row: type, quantity change, unit cost, what it refers to, a note and the user who caused it [src: app/src/Inventory.php:111]. Stock is never simply overwritten, so the history of a quantity is recoverable.

Stock can be adjusted by hand for a count correction, damage or shrinkage [src: app/src/Inventory.php:227], verified against a physical count [src: app/src/Inventory.php:247], and valued [src: app/src/Inventory.php:262].

Purchases and suppliers

A purchase is entered as lines of product, quantity and unit cost, and requires the purchase-editing permission [src: app/controllers/admin.php:317].

Three things happen on save, inside one locked transaction so that a failure leaves nothing behind [src: app/controllers/admin.php:317]. A purchase record is created with a generated reference of the form PO- followed by six characters. Each line is written. And each line is received into stock immediately.

That last point is the one to plan around: a purchase is received at the moment you save it. The status is written as received rather than ordered, so there is no pending-order state to reconcile later [src: app/controllers/admin.php:317]. Vendra records what has arrived, not what has been ordered.

How receiving changes cost

Receiving stock updates the product's average cost, weighted by quantity: the existing quantity at the existing average, plus the incoming quantity at the incoming cost, divided by the total [src: app/src/Inventory.php:135].

One detail protects that figure. If on-hand stock is negative, it is treated as zero for the purposes of the average, so a negative quantity cannot poison the cost of everything received afterward [src: app/src/Inventory.php:135]. The product row is locked while this happens [src: app/src/Inventory.php:135].

Purchases can be listed and opened individually [src: app/controllers/admin.php:302]. Suppliers are managed separately [src: app/controllers/admin.php:347]; deleting one detaches it from its purchases rather than deleting them, so the purchase history survives the supplier [src: app/controllers/admin.php:347].

Reports and the audit log

Reports are gated on the reporting permission [src: app/controllers/admin.php:444]. Actions that change something are written to an audit log, which the viewer role can read [src: app/src/Auth.php:67].

Settings

Settings are administrator-only, both to view and to save [src: app/controllers/admin.php:535].

They fall into two groups. The first covers the business and the receipt: business name, e-mail, phone and address, timezone, currency and currency symbol, the default tax rate and what to call tax, whether negative stock is allowed, the default low-stock threshold, receipt header and footer, accent color and logo [src: app/controllers/admin.php:540].

The second covers mail and notifications: SMTP host, port, user, password, from-address and security mode, a notification address, and four toggles — low stock, a returned sale, the receipt and the Z-report [src: app/controllers/admin.php:540].

Several defaults are applied on save rather than assumed. An unrecognized timezone falls back to UTC, the SMTP port is clamped to a valid port number, and the security mode must be one of STARTTLS, TLS or none, defaulting to STARTTLS [src: app/controllers/admin.php:540].

Two of those toggles behave differently from the rest by design: the receipt and Z-report toggles are the only two that cause mail to be sent to a customer, so an absent checkbox means off, which is also the migration default. Emailing a customer is always a decision somebody made [src: app/controllers/admin.php:540].

One point of arithmetic worth knowing if you compare the form against the database: the tax rate you type is stored multiplied by one hundred, as basis points [src: app/controllers/admin.php:540].

The API and agent access

Keys

The API authenticates with a key rather than a session. Some endpoints take any valid key, and some require an administrator-scoped one [src: app/src/Api.php:31]. Purchase creation and the report endpoint both require the administrator-scoped key [src: app/controllers/api.php:222].

A machine-readable description of the interface is published [src: app/controllers/api.php:309].

Agent tools

Vendra exposes five tools to an agent [src: app/controllers/v3.php:74]:

ToolWrites?What it does
list_productsnoLists products with price and current stock
stock_levelsnoStock on hand for one product, by id or barcode, or for everything
low_stocknoProducts at or below their low-stock threshold
sales_reportnoSales totals for a date range, by product and by cashier
record_saleyesRecords a completed sale and decrements stock

Four are read-only and one writes, and the tool list says so in its own instructions rather than leaving an agent to infer it [src: app/controllers/v3.php:74]. A read-only key cannot call the one that writes.

The instructions given to an agent also state the oversell rule, because an agent needs to know a call can be refused: record_sale goes through the same checkout path the till uses, so a line exceeding available stock is refused and told how much is left [src: app/controllers/v3.php:74].

Webhooks

Two events can be sent to a URL you control: a recorded sale and a low-stock warning [src: app/src/Webhook.php:16].

Stock counts

Inventory → Stock counts (since 3.2.0, managers and administrators) counts the shelf — every active product, or one category — and corrects the ledger [src: app/src/StockCount.php:23]. Scan a barcode, or type a SKU at the scanner field, and each scan adds one to that product's count; the camera works here too. Or type the figures and save [src: app/src/StockCount.php:112], [src: app/src/StockCount.php:44].

The shop can keep trading while you count. A line's on hand figure is taken at the moment that line is first counted, and posting moves stock by counted − on hand when counted, applied to whatever the stock is when you post — so a sale rung up between counting a shelf and posting the count is neither lost nor counted twice [src: app/src/StockCount.php:87]. Post differences writes one "count" line in each differing product's ledger with your reason, in one transaction, and closes the count; lines nobody counted are left alone. A posted count cannot be changed. Products made of other products are not counted: their parts are.

Scan to adjust stock (3.3.0)

Inventory → Scan to adjust (managers and administrators, who hold the stock-adjust permission) moves stock one scan at a time. Choose whether each scan adds or takes off, how many, and a reason — damaged, lost or stolen, found, returned to stock, used in the shop or other, with an optional note. Then scan a barcode or type a SKU and press Enter; the camera works here too. The code is matched to an active product by barcode first, then by SKU [src: app/controllers/stockscan.php:13]. Each scan is one adjustment through the same path as a manual one [src: app/controllers/stockscan.php:33], so it is on the product's ledger with "Scan:" and the reason, and taking off more than is on hand is refused unless your settings allow negative stock. Recipe and bundle items keep no stock and are refused; adjust their parts. The page lists the last 25 scan adjustments with what is on hand now.

Landed cost

When a delivery comes with freight, duty or fees, enter them on Receive stock (or send landed_cost_cents over the API). The amount is spread over the delivery's lines in proportion to each line's value — by quantity when every line cost nothing — and the shares add up to the cent [src: app/src/Inventory.php:161]. Each line's share goes into its unit cost before the weighted average is recomputed, so the margin on every later sale counts what the stock really cost you [src: app/src/Inventory.php:196]. The purchase shows each line's share and its landed cost per unit.

Scanning a delivery in, and the margin before you receive it

Receive stock has a scan box (since 3.2.0): a USB or Bluetooth scanner reads a barcode — or you type a SKU — and Enter adds one to that product's line, or starts a line for it [src: app/views/purchase_form.php:12]. Each line shows the product's selling price and the margin it would make at the unit cost you entered, and flags a cost at or above the price [src: app/views/purchase_form.php:27]; the freight you add is spread over the lines on receipt, as above. Recipe items are not offered: they keep no stock of their own.

Barcode labels

Products → Labels (since 3.2.0) prints price labels: the product's name, its price (or not) and a Code 128 barcode of its barcode number — or of its SKU when it has none, so an item that came without a barcode scans at the till too [src: app/controllers/labels.php:20]. Choose how many of each, or one per unit in stock, and the label stock: an A4 sheet of 24, a US Letter sheet of 30 or a 50 × 30 mm label printer, with a "labels already used" count for part-used sheets [src: app/controllers/labels.php:10]. Print at actual size. Any USB or Bluetooth scanner reads them, and so does the till's camera, which reads Code 128 as well as retail barcodes since 3.2.0.

Products made of other products

A product can be a recipe or a bundle (since 3.2.0): on the product form, under Made from other products, list its parts — a latte from a cup, two shots and milk; a gift box from what is in it [src: app/src/Inventory.php:76]. Such a product keeps no stock of its own. Selling it takes its parts off the shelf through the same oversell guard as any sale, and a sale that would run a part out is refused naming the part [src: app/src/Sales.php:101]. Its cost is its parts at their average cost. The till shows how many can be made — the scarcest part decides [src: app/src/Inventory.php:47]. A return puts back the parts that sale actually took, at the cost they left at, even if the recipe has changed since [src: app/src/Sales.php:238]. One level only: a part cannot itself be made of parts, and a product that is a part cannot become a recipe. Such products never show as low or out of stock (their parts do), cannot be received or adjusted, and Restock is told about the parts used, not the recipe.

The customer display

A tablet or a second screen turned to the customer can show the basket as you ring it up, then "thank you" and the change (since 3.2.0). At the till, Display → Make a display link, then open that link (or scan its QR code) on the other screen [src: app/controllers/display.php:12], [src: app/controllers/display.php:55]. It shows item names, quantities and prices — nothing else the till knows: no costs, no customer [src: app/controllers/display.php:34]. The link carries a random token and works until you make a new one. It updates about once a second.

Connected apps: Restock and Loyalora

If you also run Restock (reorder planning) or Loyalora (loyalty) on your own server, Connected apps (administrators only) links the till to them through their own REST API. You enter each app's address and an API key minted in that app; the key is write-only on screen and left out of the shareable backup [src: app/controllers/suite.php:17], [src: app/src/Suite.php:40]. Test asks the app who the key acts as, and says so if the address belongs to the other app [src: app/src/Suite.php:383].

Restock. Every completed sale — from the till, the REST API or your own AI agent over MCP — sends that shop day's units per SKU for the sale's products, and the shelf figure for those SKUs [src: app/controllers/api.php:82], [src: app/src/Suite.php:225]. The day is your shop's day in its own time zone, and the units are what was sold minus what has since been returned, so each send replaces that day in Restock rather than adding to it, and a refund corrects the day it belongs to [src: app/src/Suite.php:150], [src: app/controllers/admin.php:168]. A delivery received and a stock correction send the new shelf figure too [src: app/controllers/admin.php:332], [src: app/controllers/admin.php:282]. Only products with a SKU are sent. Send the last 90 days to Restock is the catch-up for a first connection or after Restock was down [src: app/src/Suite.php:253].

Loyalora. When you pick a customer at the till, the till shows their points balance, or offers to enroll them [src: app/src/Suite.php:298], [src: app/src/Suite.php:311]. A sale rung up for a member earns points by what was spent: the till sends the amount and the sale code, never a points figure, and the done screen shows what was earned and the new balance [src: app/src/Suite.php:332]. Members are matched by email address only; a customer with no email is never guessed at [src: app/src/Suite.php:282]. A refund does not take points back: adjust the balance in Loyalora if you want to.

When an app is down. The sale is already recorded before either app is called, and nothing the apps answer can undo or block it. Each call waits at most four seconds; an app that did not answer is left alone for a minute, so a dead server costs a busy till one wait, not one per sale [src: app/src/Suite.php:27], [src: app/src/Suite.php:115]. Every call — sent, refused or unanswered — is listed in the log on the same screen [src: app/src/Suite.php:126].

Spreadsheets: sales and stock

Sales → Download CSV gives one row per sale line — for the last 30 days, the day you are viewing, or any range with ?from=YYYY-MM-DD&to=YYYY-MM-DD — with quantity, quantity returned, unit price, discount, tax, line total, and the unit and line cost the till captured when it rang the sale [src: app/controllers/admin.php:90], [src: app/src/Exports.php:82]. Products → Stock CSV gives price, weighted-average cost, stock, low-stock level and stock value at cost [src: app/src/Exports.php:117]. The sales file needs the report permission and the stock file the catalog permission, so a cashier gets neither [src: app/controllers/admin.php:92]. Money is written as plain numbers and text cells are guarded against spreadsheet formulas [src: app/src/Exports.php:62].

Customers and Suppliers download as CSV too (since 3.2.0): each person with their contact details and, for a customer, their completed sales and what they spent after refunds, for a supplier, the deliveries received and their value [src: app/src/Exports.php:30], [src: app/src/Exports.php:43]. The customer list is back-office reading: it needs the reports permission, which the till's cashier role does not hold [src: app/controllers/admin.php:123].

Backup and data protection

The whole dataset can be exported as JSON, with fields you nominate redacted, or as a SQLite file [src: app/src/BackupExport.php:53].

For a data-protection request, one subject's data can be exported [src: app/src/Gdpr.php:34] or anonymized in place [src: app/src/Gdpr.php:51].

Data request (4.1.1) sits on each customer's page for anyone allowed to edit customers, the till's cashier role included: download their data as JSON, or erase their details. Erasing replaces their name, email, phone and notes, the address their receipts were emailed to, and those receipt emails, with [removed]. Their sales and the amounts stay: that is the shop's record, and it names nobody once those are gone.

Kept: the audit trail's lines that this customer was added or edited, which carry their name, because the audit trail cannot be edited.

What Vendra does not do

It does not process card payments. It records how a sale was tendered — cash, card or other — and computes the change; the card itself is taken on whatever terminal you already have [src: app/src/Sales.php:101].

It is a single-shop system. The whole installation carries one settings record, written to the row with id 1 [src: app/controllers/admin.php:540], and there is no notion of a second location, a store identifier or a central head office anywhere in the application.

It has no e-commerce side and does not synchronize stock with a website.

It is not a fiscal till. It has no certified security device, no fiscal signature and no connection to any tax authority. Many countries have rules for tills that take payments — Germany, Austria, France, Italy, Spain, Portugal, Poland, Sweden, Norway, Croatia and Greece among them — and where rules like these apply to your business, Vendra on its own does not meet them. Check what applies to you with your tax adviser or tax authority before you use Vendra as your till. A Vendra receipt is a sales receipt, and its day-close (Z-report) totals your takings for your own records; neither is a fiscal document.

Looking after your install (from 4.0)

Updates from inside Vendra

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; Vendra 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. Vendra is unavailable for about a minute while this runs. The update replaces Vendra'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 Vendra 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 Vendra 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: a small shop's products, suppliers, customers and sales to try things on. You can remove it in one click. [src: app/install/index.php:116]. 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 products, suppliers, customers and sales? 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 & restore page (/restore), Back up once a day by itself is on by default; untick it to stop [src: app/views/restore.php:78]. 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 Vendra 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 Vendra keeps data/ (your backups included), src/, config.php and the other private files off the web. Nginx does not read .htaccess, so add these three 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|cron)/ { 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/vendra/app;
    index index.php;

    location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { 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].

Switching from Square: your item library

On the products Import page (/import/products), Switch from another tool takes the file Square exports. In Square: Items > Item library > Actions > Export library > All items, CSV > Export. Upload it and press Read the export (nothing is written yet): the switch report shows how many rows will import, which are already here from an earlier import, which repeat a row above, which have a problem (by line), and which columns do not come across. Then import them in one click. Importing the same file again adds nothing twice [src: app/views/_switch.php].

Switching from Square: your customer directory

On the products Import page (/import/products), Switch from another tool takes the file Square exports. In Square: Customers > Directory > Import / Export > Export customers > All Customers > Export > Download. Upload it and press Read the export (nothing is written yet): the switch report shows how many rows will import, which are already here from an earlier import, which repeat a row above, which have a problem (by line), and which columns do not come across. Then import them in one click. Importing the same file again adds nothing twice [src: app/views/_switch.php].

Connect it to other tools (Zapier, Make, n8n)

Vendra 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 Vendra 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 Vendra'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 Vendra from the console starts a fresh session [src: app/index.php:62].

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:28].

New in 4.1.3: security update

New in 4.1.3 (security update): no other site can put Vendra'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 Vendra 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:12]. If you had Vendra 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 Vendra'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.

← Back to Vendra · Quickstart · API · Test run