Menura Manual
Menura — User Manual
Menus and price lists turned into structured, exportable items. Version 4.1.3 [src: app/controllers/api.php:71].
About this manual
Every statement here was written by reading Menura'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 — which matters more here than in most products, because one of the fields is allergens.
What Menura is
A menu or wholesale price list goes in as a photograph or a PDF. A vision model reads it. What comes out is a clean, canonical menu: sections, names, descriptions, integer-cent prices, modifiers, allergens and dietary flags [src: app/controllers/mcp.php:42].
The model reads; a deterministic layer does the structuring, with no network call, so the structure and the prices you export are trustworthy regardless of which model produced them [src: app/src/Extraction.php:8].
Nothing in that layer touches the database or the network, which is what makes the money and menu-structure logic testable with no API key at all [src: app/src/Extraction.php:11].
⚠️ Allergens: Menura reports gaps and never guesses
This is the most important thing in the product, and the source states it without hedging.
The allergen report tells you which items have no allergen data recorded. It is completeness only — it reports gaps, and it never infers an allergen [src: app/controllers/mcp.php:143].
The report carries its own explanation, and it is worth quoting in full because it is the right policy: "Gaps only. Menura never infers an allergen — a wrong allergen claim is a safety issue, so an unknown stays unknown until a person fills it in." [src: app/controllers/mcp.php:165]
An empty allergen list therefore means "nobody has recorded this yet", never "this dish contains nothing". Treat a gap as work outstanding, not as an answer.
The allergen matrix (3.2.0)
The review page's Export menu has Allergen matrix (print): every dish on the menu against every allergen and dietary tag the menu lists, grouped by section, on an A4 landscape page [src: app/controllers/v320.php:73]. A dish that is off today (86'd) is still listed, and marked. A dish with no allergens recorded says "No allergens recorded: ask the kitchen" across its row, and the heading counts them [src: app/src/QrMenu.php:101]. An empty cell means only that the dish does not list that allergen, and the page says so; it never calls a dish free of anything. Viewers can open it.
Prices
Prices parse to integer cents from the formats a menu actually uses — $12.50, 1.234,56 €, spaces as thousands separators, parentheses or a leading minus for negatives, and raw numbers [src: app/src/Extraction.php:30].
"Market Price", "MP" and an em dash return nothing rather than zero, so the item shows as price-on-request [src: app/src/Extraction.php:31]. A dish with no fixed price never becomes a free one.
Price changes on a date (3.2.0)
After previewing a price change (a percentage or an amount, with rounding), you can Apply it now or Schedule it for a later date [src: app/controllers/v320.php:225]. A scheduled change applies itself on its date, on the first page, QR scan or API call that day, so no cron job is needed; it is claimed before it runs, so it applies once even if two requests arrive together [src: app/controllers/v320.php:208]. It goes through the same per-item rule and single transaction as Apply, and is recorded in the price history as "scheduled by" its author [src: app/controllers/v3.php:258]. A change that has not run can be canceled; a past date is refused (use Apply).
Every menu at once (3.3.0)
Tick On every menu, not just this one before previewing: the preview lists every dish on every menu with its menu, and Apply (or Schedule) changes them all in one transaction, so a failure on any menu changes none [src: app/controllers/v3.php:266]. The QR menus show the new prices at once; re-export the till files and PDFs to publish them there. A scheduled change for every menu shows on each menu's price page.
A menu for another location (3.2.0)
Copy this menu (Export menu) makes a new, unpublished menu with every dish, its prices, allergens, tags, modifiers and 86 state, its own copy of the source file, the approved translations and the till settings [src: app/controllers/v320.php:261]. Change what differs at the other location; the original is not touched. Publish the copy's own QR page when it is ready.
Lists — modifiers, allergens, dietary flags
All three are normalized the same way [src: app/src/Extraction.php:144]. A comma, semicolon, pipe or newline-delimited string is accepted, as is an array of strings or of objects.
The rules are worth knowing because they determine what your export looks like:
- Entries are trimmed and blanks dropped.
- Duplicates are removed case-insensitively, and the first spelling wins.
- Input order is preserved.
- Each entry is clipped to 190 characters and the list capped at 50 — both to defend the database row against oversized model output [src: app/src/Extraction.php:148].
Menu status
A menu moves uploaded → extracted → reviewed, with extracting and error as the states in between [src: app/controllers/mcp.php:44]. A human reviews what the model extracted; "reviewed" means a person has checked it.
Exporting
Three shapes [src: app/src/Extraction.php:322]:
Nested CSV — the rich export, keeping section, modifiers, allergens and dietary columns [src: app/src/Extraction.php:319].
POS CSV — one clean row per sellable item with the minimal columns a point-of-sale system expects: SKU, name, category, price, description [src: app/src/Extraction.php:351]. The SKU is a stable slug of section plus name, so a re-import matches what you imported before [src: app/src/Extraction.php:349].
JSON — the full canonical structure [src: app/src/Extraction.php:384].
Till files: Square, Toast, Lightspeed, Vendra (3.2.0)
The review page's Export menu has Till file, which writes the menu as the sheet a named till's own importer takes [src: app/controllers/v320.php:35]. The column names follow each till's help pages as read on 26 September 2026 [src: app/src/TillPresets.php:62]:
- Square (item library): Token (blank for a new item), Item Name, Variation Name ("Regular"), SKU, Description, Categories, Price. A blank price makes a variable-priced item in Square. Square's own template also carries your locations and taxes, so for an exact file paste its first line.
- Toast (menu bulk import, basic template): each dish is created as a menu item under a menu group that already exists in Toast. You give the Toast ID of the group for each section; a section without one is left out, and the page says which.
- Lightspeed Restaurant (K-Series): SKU, Name, Type ("Item"), Accounting group (one you name, since it carries the taxes) and Default price. Lightspeed wants unique names, so a repeated name is flagged.
- Vendra: Name, SKU, Price, Category. There is no description column, because Vendra reads "Description" as another word for the name.
- Another till: paste the first line of its import template; Menura fills the columns it recognizes (name, price, category, description, SKU, in their usual spellings) and leaves the rest blank [src: app/src/TillPresets.php:130].
Tills change their templates. The page shows the first rows before you download, and every setting (the pasted line, the Toast IDs, the accounting group) is remembered for the menu, so the next price change is one download. As with the POS CSV, no till file carries allergens.
Importing a menu from a spreadsheet, lists included (3.2.0)
Import CSV matches columns by name in any order; name and price are needed. Since 3.2.0 it also takes modifiers, allergens and dietary, several in one cell separated by ; (or ,, |), cleaned by the same list rule the AI extraction uses (trimmed, repeats dropped whatever their case) [src: app/controllers/api.php:354]. The nested CSV export's own columns import back as they are, so a menu can move between installs or be edited in a spreadsheet and brought back [src: app/controllers/api.php:361]. An empty cell stays empty, which the allergen reports count as "no allergens recorded".
⚠️ The POS export carries no allergen columns
Stated because it is easy to miss and matters given the subject. The flat POS layout deliberately has no nesting and no modifier or allergen columns — the section becomes the category and that is all [src: app/src/Extraction.php:348].
If allergen data needs to reach your till system, the POS export is not the route. Use the nested CSV or the JSON.
Users and roles
Three roles [src: app/controllers/app.php:86]. The source names the point of the product's own reviewer seat: correcting what the model extracted, and exporting it [src: app/controllers/app.php:89].
This table was produced by executing the product's own permission function against every permission the routes ask for.
| Permission | viewer | member | admin |
|---|---|---|---|
menu.view and other .view permissions | yes | yes | yes |
audit.view | yes | yes | yes |
item.edit | — | yes | yes |
menu.export | — | yes | yes |
menu.upload | — | yes | yes |
extraction.run | — | — | yes |
menu.delete | — | — | yes |
settings.edit | — | — | yes |
team.manage | — | — | yes |
Running an extraction is admin-only [src: app/controllers/app.php:92], because it spends the owner's own model credits.
⚠️ The audit trail is readable by a viewer
Menura's viewer role is granted *.view — a wildcard rather than a list [src: app/controllers/app.php:90]. Because the audit permission is named audit.view, the wildcard matches it, so a viewer can read the audit trail.
The comment on the line below lists audit.view among what the admin role covers via its own wildcard [src: app/controllers/app.php:92], which reads as though it were admin-only. The behavior and that comment do not agree, and the behavior is what this manual describes. It has been raised against the product.
Backups follow the model
Menura's database backup downloads are permission-gated, requiring the settings permission [src: app/controllers/api.php:235]. A viewer cannot download the database.
The API and agent access
Five tools are exposed to an agent, of which one writes [src: app/controllers/mcp.php:47]: listing menus, menu detail, export rows and the allergen report on the read side; correcting an item on the write side.
Extraction is not available to an agent — it spends the owner's own model credits and is triggered from the application [src: app/controllers/mcp.php:45].
Models and files
The default model per provider is stated once: gpt-4o-mini for OpenAI, claude-haiku-4-5-20251001 for Anthropic and llama3.2-vision for Ollama [src: app/src/Llm.php:44]. Under the model field, Settings suggests model ids for the chosen provider; for messy or handwritten menus it names Sonnet 5 (claude-sonnet-5). A model the provider refuses — retired, misspelt, or not pulled into Ollama — fails with a message naming the model and the setting to change [src: app/src/Llm.php:87].
Anthropic and OpenAI read PDFs; OpenAI receives the PDF as a file part [src: app/src/Llm.php:115]. Ollama reads images only, so a PDF is refused before any call with a message to convert the pages to PNG or JPEG [src: app/src/Llm.php:107].
The QR menu
A reviewed menu can be published as a mobile page at /m/<code> on your own install, with a table QR code to print. The code is 24 random hex characters and stays the same if you take the page offline and publish it again [src: app/src/QrMenu.php:30]. Publishing and 86-ing need the item-editing permission [src: app/controllers/qrmenu.php:28]; the guest page needs no login and serves a menu only while it is published [src: app/controllers/qrmenu.php:52].
86 an item and it leaves the guest page at once; it stays on the menu, in the exports and in the PDF [src: app/src/QrMenu.php:52]. The guest page shows sections, names, descriptions, prices, modifiers, dietary tags and listed allergens, and nothing else [src: app/src/QrMenu.php:65]. It does not take orders or payments.
Guests can filter the page (3.2.0). Above the dishes, the page offers the allergens and the dietary tags this menu lists: Hide dishes that list a chosen allergen, and Show only dishes carrying a chosen tag [src: app/src/QrMenu.php:124]. It works with plain links, so any phone can use it. The filter reads what each dish lists and says so on the page: it never calls a dish free of anything. A dish with no allergens listed stays visible, marked "No allergens are listed for this dish — ask staff", and the footer still asks guests to tell staff about any allergy.
Table cards (3.2.0). From the QR page, Print table cards gives a sheet of up to 60 cards, each with a table number, your name and the QR code, ready for A4 [src: app/controllers/qrmenu.php:85]. Every card opens the same menu; the number is for your tables.
Translations (3.2.0). Export → Translations puts a reviewed menu into a language you name, on your own AI key, up to 60 dishes per click [src: app/controllers/v320.php:128]. Only section names, dish names and descriptions are sent; prices, allergens, dietary tags and modifiers are never sent and never change [src: app/src/Translate.php:38]. The instruction asks for a faithful translation that adds nothing, keeps dish names that are usually left in their own language, and changes no ingredient, allergen, price or claim [src: app/src/Translate.php:28]. What comes back is checked: a dish id that was not asked for is dropped, and a description is never added to a dish that had none [src: app/src/Translate.php:51]. Every translation is generated until someone who can edit the menu saves and approves it, after editing if needed [src: app/controllers/v320.php:171]. The guest page then offers the language beside Original; a dish without an approved translation shows in the original, and allergens and dietary notes always show exactly as recorded, with a line saying so. The page's own labels (the filter, the footer) stay in English. Running a translation needs the same permission as an extraction, because it spends your credits.
What Menura does not do
It never infers an allergen [src: app/controllers/mcp.php:165]. A gap stays a gap until a person fills it.
It does not tell you a dish is safe. It tells you whether anybody has recorded what is in it.
It does not price a "market price" dish [src: app/src/Extraction.php:31]. No price becomes no price, not zero.
It does not let an agent spend your model credits [src: app/controllers/mcp.php:45].
It does not carry allergens into the POS export [src: app/src/Extraction.php:348]. That layout is deliberately minimal.
Looking after your install (from 4.0)
Updates from inside Menura
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; Menura 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. Menura is unavailable for about a minute while this runs. The update replaces Menura'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 Menura 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 Menura 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 sample menus with their items to try things on. You can remove it in one click. [src: app/install/index.php:148]. 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 menus and items? 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 Backups page (/restore), Back up once a day by itself is on by default; untick it to stop [src: app/views/restore.php:71]. 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 Menura 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 Menura 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/menura/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].
Connect it to other tools (Zapier, Make, n8n)
Menura 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 Menura 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 Menura'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 Menura from the console starts a fresh session [src: app/index.php:57].
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:29].
New in 4.1.3: security update
New in 4.1.3 (security update): no other site can put Menura'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 Menura 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 Menura 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 Menura'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.