Onboardora Manual
Onboardora — User Manual
An onboarding register: a new starter's first ninety days as a dated plan — who owns each task, when it falls due, what is late, and what has actually been done. Version 4.1.3 [src: app/src/App.php:8].
About this manual
Every statement here was written by reading Onboardora'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.
Nothing is described that the code does not do. Where something is deliberately limited, or currently wrong, that is said plainly rather than left out.
⭐ Four properties the product is built on
The engine states them at the top of the file, and each one is worth knowing before you plan anybody's first week [src: app/src/Onboard.php:5].
A starter's plan is a snapshot. When somebody is added from a template, the template's tasks are copied onto them. Editing the template afterward changes what the next person gets and can never rewrite a plan already in flight [src: app/src/Onboard.php:7] — the source compares it to the discipline a signed document gets, for the same reason: somebody is working to that list today.
Due dates are derived, never stored. A task carries an offset in days from the start date, so the date is computed at read time [src: app/src/Onboard.php:12].
"Late" is derived too — a task is late when it is not done and its derived due date has passed. There is no is_late column to go stale overnight and no job that has to run for the dashboard to be right [src: app/src/Onboard.php:17].
Completion is idempotent and keeps the first answer. Ticking a done task again does not move the time it was done or the name of who did it, because two people working the same checklist is the normal case, not the edge case [src: app/src/Onboard.php:21].
⭐ Moving a start date moves the whole plan
That is the practical consequence of derived due dates, and the source names what it avoids: no migration, and nothing left pointing at the old Monday [src: app/src/Onboard.php:14].
Executed — the same plan against two start dates:
start 2026-09-14 offset −7 -> 2026-09-07 offset 30 -> 2026-10-14
start 2026-09-21 offset −7 -> 2026-09-14 offset 30 -> 2026-10-21
Day −7 is a week before they arrive; day 0 is their first morning [src: app/src/Onboard.php:13].
One labeling detail worth knowing so you do not read the plan off by one: offset 0 is displayed as "day 1" — a starter's first morning is day one, not day zero. Executed:
offset −7 -> "7 days before" offset 1 -> "day 2"
offset 0 -> "day 1" offset 30 -> "day 31"
Offsets outside the plan window are treated as typos
A plan lives between day −60 and day 365; outside that, an offset is a typo rather than a plan [src: app/src/Onboard.php:29], and the value is clamped rather than accepted. Executed:
−61 -> −60 365 -> 365 'abc' -> 0
−60 -> −60 366 -> 365 '30' -> 30
⭐ Calendar days, and a weekend that is shown rather than hidden
Phases are calendar days, not working days, and the reasoning is given in full [src: app/src/Onboard.php:67]: "day 30" in an onboarding plan means a month in, and a plan that quietly shifted because May had a bank holiday would be worse than one that did not.
The consequence is stated rather than smoothed over: a task that lands on a weekend is shown as landing on a weekend — the manager can see it and move it, which is more honest than the product moving it silently [src: app/src/Onboard.php:69].
Executed against a Monday start:
start 2026-09-14 (Mon) + day 5 -> 2026-09-19 (Sat)
+ day 6 -> 2026-09-20 (Sun)
+ day 12 -> 2026-09-26 (Sat)
The plan groups by the phrases a manager actually uses rather than by raw day numbers [src: app/src/Onboard.php:64]. Executed:
day −7, −1 -> before day 14, 30 -> month-one
day 0 -> day-one day 60, 90, 200 -> ninety
day 1, 7 -> week-one
Who owns a task
Six owner kinds [src: app/src/Onboard.php:33]: manager, HR, IT, buddy, the starter themselves, or a named person who carries their own name and email. The first five resolve off the starter's own record, so reassigning a manager does not leave a task pointing at somebody who left.
Reassignment is its own operation rather than a free-text edit [src: app/src/Onboard.php:269].
Tags and "only if tagged" tasks (1.1)
A starter can carry tags — "remote", "driver", "night shift" — typed with commas on the add and edit forms. Tags are stored lower case, whole words, at most ten [src: app/src/Onboard.php, tagList()].
A checklist task can be marked only if tagged with one tag. When the checklist is copied on to a starter, that task goes only to a starter with that tag; a task with the box left blank goes to everyone, as every 1.0 task does [src: app/src/Onboard.php, applyTemplate(), taskApplies()]. The match is the whole tag: "remote" does not match "remote-first".
The plan is still a copy (property 1). Tagging somebody later adds nothing by itself; apply the checklist again and it adds exactly the tasks that now match, and nothing twice. Removing a tag never removes a task that is already on the plan.
Tags are part of the person's record: they are in the API's starter (tags) and are cleared when the person is erased.
Users and roles
Three roles [src: app/controllers/v3.php:60]. A member can edit a starter, tick tasks and edit them; admin holds everything [src: app/controllers/v3.php:61].
A viewer is granted audit.view explicitly, written into the role by name rather than picked up by a wildcard [src: app/controllers/v3.php:60], and the audit page requires exactly that [src: app/controllers/v3.php:379]. In an onboarding register that reads as deliberate — who ticked what on somebody's induction is part of the record — but if you expected read-only accounts to be excluded from it, change the role.
Backups are gated on settings.edit [src: app/controllers/api.php:217], which no role below admin holds [src: app/controllers/api.php:222].
Every way of ticking a task fires the webhook
Four events are offered for subscription [src: app/src/Webhook.php]: starter.created, task.completed, task.reopened and, since 1.1, document.received. The two task events fire from every path that ticks or un-ticks a task — the browser, the bulk "tick selected" action (one task.completed per task it ticks, ob_tasks_bulk_complete() [src: app/controllers/v3.php]), the REST route and the agent tool complete_task — through one helper, ob_fire_task_event() [src: app/controllers/v3.php], and only when the task's state actually changed, the same rule the audit trail follows. Before 1.0.3 only the REST route fired them, so a manager working down the checklist on screen sent nothing to an integration.
The starter's own page (1.1)
A link bound to ONE starter opens their own page [src: app/controllers/welcome.php, welcome_page()]. The link is a long random token; only its SHA-256 is stored, it is shown once, and making a new link or switching it off stops the old one at once. It takes no login and creates no account.
On it the starter sees their timeline and can:
- tick the tasks that are theirs — only tasks owned by "the starter"; IT's laptop task stays IT's [src: app/controllers/welcome.php,
welcome_task()]; - upload a document you asked for — PDF, PNG, JPEG or WebP under 10 MB; the file's own bytes decide the type, not its name. Files are stored under
data/, served to the team only, and replaced by a new upload [src: app/controllers/welcome.php,ob_upload_refusal()]; - acknowledge a document — by ticking "I have read it" and typing their full name as it appears on the page; the name typed and the time are recorded once [src: app/controllers/welcome.php,
welcome_acknowledge()].
Each change is written through the same helpers a manager's click uses and lands in the audit trail as starter:<name>. A document arriving fires document.received.
When adding a document to a plan you choose what the starter does with it: hand it back (you tick it, as in 1.0), upload it, or read and acknowledge it.
A webhook can post to your team chat (1.1)
Each webhook endpoint chooses what to send: the signed JSON document, or a chat message — one readable line as {"text": …}, the shape a Slack incoming webhook takes [src: app/src/Webhook.php, deliver()]. The line is written by one function, ob_chat_line() [src: app/controllers/v3.php], and names the starter and the task or document: "New starter: Priya Raman starts … as Software Engineer.", "Onboarding task done for …: …", "Document back from …: … (uploaded)". It never carries an email address, a note, a file or anything the starter typed. Endpoints added before 1.1 stay signed JSON.
People for Trainora (1.1)
Starters → People for Trainora downloads a CSV with name, email (work email), role and started_on — the columns Trainora's Import people reads without mapping [src: app/controllers/app.php, starters_trainora_csv()]. Canceled and erased starters are left out; a cell that starts like a formula is written as text. Onboardora sends nothing to Trainora: you download the file and import it there, where training and certificates are tracked.
The API and agent access
Six tools [src: app/controllers/v3.php:148]: four read — the starter list, one starter's plan, what is late, and the checklist templates — and two write, completing a task [src: app/controllers/v3.php:209] and reassigning one [src: app/controllers/v3.php:232].
Note that whats_late [src: app/controllers/v3.php:177] answers from the same derived rule the dashboard uses, so an agent and a manager cannot disagree about what is overdue.
What Onboardora does not do
It does not let an edited template rewrite a plan already in flight [src: app/src/Onboard.php:7].
It does not store a due date [src: app/src/Onboard.php:12] — move the start date and the plan moves with it.
It does not store a "late" flag [src: app/src/Onboard.php:17].
It does not overwrite who completed a task when it is ticked again [src: app/src/Onboard.php:21].
It does not silently move a task off a weekend [src: app/src/Onboard.php:69].
It does not accept an offset outside the plan window [src: app/src/Onboard.php:29].
Subject access and erasure: what an erase removes and what it keeps (4.1.1)
A starter's page carries the erase for an admin once their onboarding is complete or canceled. Erasing clears their name, both email addresses, any note and their tags, the names they typed and the files they uploaded, the notes on their documents and equipment, their name on the tasks they ticked, and the reminders about them in the mail log. The plan, its dates and its completion record stay, so your figures do not change.
Kept: the audit trail's lines for what they did on their own welcome page, which record them as the one who did it, by name, because the audit trail cannot be edited.
Looking after your install (from 4.0)
Updates from inside Onboardora
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; Onboardora 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. Onboardora is unavailable for about a minute while this runs. The update replaces Onboardora'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 Onboardora 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 Onboardora 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: four new starters at different points of their first ninety days to try things on. You can remove it in one click. [src: app/install/index.php:117]. 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 starters and tasks? 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 Onboardora 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 Onboardora 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/onboardora/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)
Onboardora 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 Onboardora 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 Onboardora'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 Onboardora from the console starts a fresh session [src: app/index.php:51].
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 Onboardora'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 Onboardora 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 Onboardora 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 Onboardora'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.