Waiverly Manual, as shipped in the download
Waiverly — User Manual
Digital waivers you own outright. Version 3.1.4 [src: app/controllers/api.php:23].
About this manual
Every statement here was written by reading Waiverly'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 Waiverly is
Waiverly takes signatures on a document and keeps a record you can prove was not altered afterwards [src: app/src/Waiver.php:75]. A template holds the wording; a signing link [src: app/index.php:107] or a front-desk kiosk [src: app/index.php:109] collects the signature; and every completed waiver carries a short code that anybody can use to verify it [src: app/index.php:111].
Signing: the two ways in
There are two public signing routes and they share the same form.
A link you send. /w/{token} presents the waiver for signing [src: app/index.php:107], and the signed form posts back to the same address [src: app/index.php:108].
A front-desk kiosk. /k/{token} is the same waiver in kiosk mode [src: app/index.php:109]. The difference is what happens afterwards: the kiosk page carries the address to return to, so the screen resets itself for the next person in the queue rather than leaving the previous signer's details on display [src: app/views/sign.php:12].
Both are deliberately reachable without signing in. That is the product: a signer is not a user of your system and never needs an account.
A QR poster for walk-ins. Every template on the Templates page has a QR poster button: a printable page with the template's title, your business name and the share link as a QR code, so people in the queue can scan it with their own phone and sign there [src: app/controllers/app.php:552]. The poster uses the public address from Settings when one is set, otherwise the address you are signed in on. The kiosk link is never printed on it.
Prefilled share links. The share link accepts ?name= and ?email= — for example /w/{token}?name=Sam%20Lee&email=sam@example.com — and opens with those two fields already typed; the signer can still change them before signing [src: app/views/sign.php:36]. The kiosk ignores them, so the next person in the queue never inherits a name.
Returning signers
Before the form is submitted, Waiverly can check whether this person has signed before. The check matches on the template, the e-mail address lower-cased, and the date of birth exactly, and returns the most recent match with its status and expiry [src: app/src/Waiver.php:310]. It runs as its own request [src: app/index.php:105], so the front desk can be told "already covered until March" without the signer filling the form in again.
Verifying a waiver
Every signed waiver gets a short code, and /verify looks a waiver up by it [src: app/index.php:111]. The page is public, because the point of a verification code is that somebody without an account can use it.
The codes are built to be read aloud and typed by hand. They are ten characters drawn from an alphabet that deliberately omits the letters and digits that get confused — no I, L, O, zero or one [src: app/src/Waiver.php:30]. A code is checked for uniqueness as it is generated, retried if it collides, and the generator throws rather than returning a duplicate if it cannot find a free one [src: app/src/Waiver.php:136].
How Waiverly proves a record was not altered
This is the part that matters if a waiver is ever disputed, and it is worth understanding. Two hashes are involved, and an integrity check recomputes the second and compares it with the stored value [src: app/src/Waiver.php:75].
The wording is hashed
Each template version has a content hash: a SHA-256 over the title, the body and every acknowledgement, in order [src: app/src/Waiver.php:42].
Two details make that hash trustworthy. The text is canonicalised first — line endings normalised and surrounding whitespace trimmed — so a document edited on Windows and the same document edited on a Mac produce the same hash [src: app/src/Waiver.php:36]. And the parts are joined with a unit-separator character rather than a plain newline, so a body that itself contains a line break cannot be confused with the boundary between two fields [src: app/src/Waiver.php:42].
The signed record is hashed
When a waiver is signed, a second SHA-256 is taken over the record itself — its code, the content hash of the wording that was on screen, the signer's name, e-mail, phone and date of birth, whether they were a minor, and the guardian's name [src: app/src/Waiver.php:50].
Checking integrity later re-computes that fingerprint and compares it with the stored one, using a timing-safe comparison [src: app/src/Waiver.php:75].
The consequence is the useful bit: because the record hash includes the content hash, altering the wording of a template afterwards cannot make an old signature appear to cover the new text. The old record still fingerprints the old words.
Template versions are immutable
Editing a template's text — its title, body or acknowledgements — publishes a new immutable version. Editing only its metadata — the name, the age rules, the expiry policy, the flags — does not [src: app/src/Waiver.php:154].
That distinction is what keeps the version history meaningful: a version bump means the legal wording changed, and nothing else causes one.
Status: valid, expired or superseded
A waiver's status is derived every time it is asked for and never stored [src: app/src/Waiver.php:110].
The precedence is deliberate. If the template requires re-signing and the waiver was signed against an older version, it is superseded — and that is checked first, because a venue requiring a fresh signature wants one regardless of dates. Otherwise, if there is an expiry date and today is past it, the waiver is expired. Otherwise it is valid [src: app/src/Waiver.php:110].
Expiry dates and the end of the month
Where a template expires after a number of months, the date is advanced by whole calendar months and then clamped to the end of the target month [src: app/src/Waiver.php:94]. A waiver signed on 31 January with a one-month policy expires on 28 or 29 February — not on 3 March, which is what naive date arithmetic produces.
Ages
Age is computed as full years completed on a given date [src: app/src/Waiver.php:84]. A 29 February birthday completes its year on 1 March in a non-leap year, which the source states explicitly rather than leaving to chance [src: app/src/Waiver.php:84].
Templates can require a guardian below a minimum age, and the signed record carries whether the signer was a minor and who signed for them [src: app/src/Waiver.php:50].
Signatures
A signature is accepted only if it is genuinely a PNG. The submitted value must be a PNG data URL, must decode cleanly, must be at least a hundred bytes, must not exceed the size limit [src: app/src/Waiver.php:28], and must begin with the actual PNG magic bytes [src: app/src/Waiver.php:124].
That last check is the one that matters: the file is identified by its contents rather than by what it claims to be, so relabelling something else as a PNG does not get it in.
Users, roles and what each may do
Waiverly has three roles [src: app/controllers/v3.php:46].
| Permission | viewer | member | admin |
|---|---|---|---|
waiver.view | yes | yes | yes |
template.view | yes | yes | yes |
audit.view | yes | yes | yes |
waiver.resend | — | yes | yes |
view.save | — | yes | yes |
waiver.delete | — | — | yes |
template.edit | — | — | yes |
user.edit | — | — | yes |
settings.edit | — | — | yes |
That table was produced by running the application's own permission map rather than by reading it [src: app/src/RolesKit.php:31].
The source explains the shape in one line: a viewer is a front-desk seat that can take signatures and look things up but cannot change the legal text, and templates are the legal text while settings hold the credentials — so both are admin only [src: app/controllers/v3.php:46].
Routes are gated by a permission check that first requires a signed-in user and then tests the permission, refusing with a plain message rather than a blank page [src: app/controllers/v3.php:64].
What happens to roles when you upgrade
Waiverly did not originally have roles at all. When the role column was added it was given a default of admin, deliberately, so that upgrading never strips access from somebody who already had it [src: app/src/Database.php:97].
New people invited afterwards are the other way round: an invitation defaults to the viewer role, the most restrictive one [src: app/src/Database.php:134]. Existing users keep what they had; new ones start with the least.
Notifications and the signer's copy
Mail is sent from your own server. You supply the SMTP host, port, user, password, sender address and security mode, along with a notification address [src: app/controllers/app.php:507].
The signer's copy — the e-mail that goes to the person who signed, carrying their verification link [src: app/src/SignerCopy.php:65] — is off unless you switch it on, and that is the migration default too [src: app/src/Database.php:109]. Sending mail to a member of the public is always a decision somebody made.
Settings
Settings cover the business and its branding: business name, e-mail, phone and address, website, accent colour, timezone, whether to send the signer's copy, and the public base URL used when building links [src: app/controllers/app.php:510].
Changing any of them requires the settings permission, which only an admin holds [src: app/controllers/v3.php:46].
The calendar feed and scheduled backups
Two endpoints are reachable without signing in and are not public: each is protected by a secret token compared against a stored hash.
The calendar feed at /calendar.ics publishes upcoming expiries so they appear in your own calendar application [src: app/controllers/v3.php:369]. The scheduled backup endpoint writes a backup, prunes old ones and reports what it did [src: app/controllers/v3.php:581].
Both refuse with a forbidden response unless the supplied token matches, and — importantly — both refuse when no token has been configured at all [src: app/controllers/v3.php:369]. An installation that has not set one up is closed rather than open.
The API and agent access
Machine access uses a key rather than a session [src: app/src/Api.php:25].
Five tools are exposed to an agent, and every one of them is read-only [src: app/controllers/v3.php:46]:
| Tool | What it does |
|---|---|
list_templates | Templates with their current version and expiry policy |
list_waivers | Signed waivers, filterable by template and status |
waiver_detail | One waiver, with the template text exactly as it was signed |
signing_link | The public signing URL for a template |
stats | Counts by status and template, plus what expires in the next N days |
There is no tool that signs anything. An agent can hand out a signing link and read what came back; it cannot put a signature on a document on somebody's behalf.
waiver_detail returning the wording as it was signed rather than the current wording is the same principle as the content hash: what matters afterwards is what the person actually agreed to.
Data protection
A single subject's data can be exported [src: app/src/Gdpr.php] or anonymised in place [src: app/src/Gdpr.php]. Backups can be written on demand as well as on a schedule [src: app/src/BackupExport.php].
What Waiverly does not do
It does not verify anybody's identity. The signed record holds what the person typed — name, e-mail, phone, date of birth, whether they were a minor and any guardian's name — and fingerprints exactly that [src: app/src/Waiver.php:50]. There is no identity check anywhere in that path, so proving the record is unaltered is a different claim from proving who they were.
It is not legal advice, and the wording of your waiver is yours: Waiverly hashes and versions whatever you put in a template, and takes no view on whether it is enforceable where you are.