Home›Waiverly›Manual
Waiverly · Manual

Waiverly Manual

Waiverly — User Manual

Digital waivers you own outright. Version 4.1.3 [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 afterward [src: app/src/Waiver.php:75]. A template holds the wording; a signing link [src: app/index.php:118] or a front-desk kiosk [src: app/index.php:120] collects the signature; and every completed waiver carries a short code that anybody can use to verify it [src: app/index.php:122].

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:118], and the signed form posts back to the same address [src: app/index.php:119].

A front-desk kiosk. /k/{token} is the same waiver in kiosk mode [src: app/index.php:120]. The difference is what happens afterward: 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:557]. 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:116], 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:122]. 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 acknowledgment, in order [src: app/src/Waiver.php:42].

Two details make that hash trustworthy. The text is canonicalized first — line endings normalized 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 afterward cannot make an old signature appear to cover the new text. The old record still fingerprints the old words.

Questions, show-if rules and consent are sealed too (3.2)

A template can ask its own questions — one per line, Question | type | required | if: Earlier question = answer, where the type is text, yesno, date or choice: A, B, C — and can offer a separate, unticked marketing-consent box [src: app/src/Waiver.php parseQuestions]. The questions and the consent wording are part of the version snapshot and of its content hash; a version without them hashes exactly as it did before 3.2 [src: app/src/Waiver.php contentHash].

When someone signs, a question whose show-if rule does not match is skipped: it is not required and its answer is not stored, whatever was posted. Required questions that are shown must be answered, yes/no must be yes or no, a choice must be one of the options and a date must be a real date [src: app/src/Waiver.php collectAnswers].

The answers (each with the exact question text) and the consent are added to the signed record's fingerprint. That is record format 2 (hash_v = 2); every record signed before 3.2 is format 1 and keeps verifying with the original formula. The format number is itself part of the format-2 input, so relabeling a record as format 1 cannot make an edited one verify [src: app/src/Waiver.php recordHash]. The record page, the PDF and the CSV show the answers and the consent.

Arrivals, photos and the mailing list (3.2)

Arrivals (sidebar) is the front desk's check-in: search a name or email, see each person's best waiver (a valid one wins over a later expired one; a tampered record never counts) and Check in — only a valid, untampered waiver can be checked in, otherwise the page offers the kiosk to sign again [src: app/controllers/arrivals.php]. Today's arrivals are listed with who checked them in. The viewer and member roles may check people in. Other systems ask GET /api/waivers/check?email=…[&template_id=…] with an API key and get valid, expired, superseded or none, with the dates — never the signature, the answers or the code; this is what Gymora's front desk reads.

Photo of the signer is set per template: off (the default), optional or required. The signing page opens the device camera, shrinks the picture to 640 px JPEG in the browser, and the photo is sealed into the record's fingerprint, so a swapped photo shows TAMPERED; the PDF names its hash [src: app/src/Waiver.php validPhoto].

Mailing list CSV (Waivers) exports the signers who ticked the marketing box, one row per address, with Mailchimp's import headings and when consent was given. Anonymizing a waiver erases its answers and its photo along with the signer's details.

Template versions are immutable

Editing a template's text — its title, body, acknowledgments, questions or consent wording — 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 relabeling something else as a PNG does not get it in.

One kiosk for every waiver (3.2)

Each template has its own kiosk link. With more than one waiver, the templates page also offers One kiosk for every waiver: Create the kiosk link once, open it fullscreen on the front-desk tablet, and guests tap the waiver they need, sign it, and the screen comes back to the list for the next guest [src: app/controllers/app.php kiosk_hub]. The link is 48 random characters. A wrong one is refused and counted toward the same lockout as other wrong links. New link replaces it, and the old one stops working on every tablet. It is left out of the shareable backup, so make a new one after restoring on a fresh install.

Signing for a child (3.2)

After an adult signs, the "All set" page offers Sign for a child now [src: app/src/GuardianLink.php]. The form opens with the adult's name as the guardian and their email and phone filled in. The adult types the child's name and date of birth and signs again as guardian. The child's record then shows their own waiver (code), linking to the adult's, and the adult's record lists Also signed for. After a child, the page offers Sign for another child. The details are held in that browser session for 15 minutes only, and opening a signing page any other way forgets them, so on a shared kiosk the next guest never sees the previous one's details. The link is a note on the records; it is not part of either record's fingerprint.

Users, roles and what each may do

Waiverly has three roles [src: app/controllers/v3.php:47].

Permissionviewermemberadmin
waiver.viewyesyesyes
template.viewyesyesyes
audit.viewyesyesyes
waiver.resend—yesyes
view.save—yesyes
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:47].

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

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 afterward are the other way round: an invitation defaults to the viewer role, the most restrictive one [src: app/src/Database.php:146]. 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:512].

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.

Text messages on your own SMS account

Texts are off until you connect an SMS gateway you already pay for, in Settings → Text messages [src: app/controllers/texts.php:49]. Waiverly sells no credits and adds nothing to the price: your SMS provider bills you for every text, at its own rate [src: app/src/Sms.php:4].

  • Any HTTPS gateway. Waiverly sends one HTTPS POST per text, as JSON or form-encoded, with your three field names for the number, the sender and the message (to,from,text by default; Twilio's are To,From,Body), and your key as a Bearer token, as HTTP Basic (user:password; for Twilio, AccountSid:AuthToken) or not at all [src: app/src/Sms.php:73]. Any 2xx answer counts as sent. Send test sends one text and shows exactly what your gateway answered [src: app/controllers/texts.php:75]. The key is write-only [src: app/controllers/texts.php:68] and is left out of the shareable JSON backup.
  • Who gets a text. Only someone who gave you their number to receive the signing link. On Texts, a team member who can hand out signing links (an admin or a member) picks the waiver, types the number and has to tick They gave me this number to receive the signing link; Waiverly stores the tick and who ticked it [src: app/src/Sms.php:260]. Nothing is ever texted to a number just because a signed waiver holds it. Shortcuts open the form already filled in: Text this link on Expiring & re-sign, and Text the link on Arrivals and on Templates.
  • What a text says. Your business name, the waiver's name, the visit date if you gave one, the signing link and a stop link, for example Test Gym: please sign "Day Pass" before your visit on Sat 10 Oct: https://…/w/…?r=… Stop texts: https://…/s/… [src: app/src/Sms.php:187].
  • One reminder, only if they agreed to it. Give the visit date and tick They also agreed to one reminder text…. Waiverly then sends one reminder the set number of days before the visit (1 by default; 0 means on the day), only between the hours you set (09:00 to 20:00 in your time zone by default) [src: app/src/Sms.php:314]. The reminder tick is a condition of the query that picks reminders [src: app/src/Sms.php:343]. No reminder goes once they have signed, through the texted link or with the same phone number on the form [src: app/src/Sms.php:235], or when the link text itself did not go.
  • When reminders go. As people use Waiverly, a few due reminders go after each page has been sent, so nobody waits for them [src: app/index.php:101]. To send them at a fixed time, have a cron job open /texts/run?t= followed by your scheduled-backup token (Backup & restore) [src: app/controllers/texts.php:38]; it keeps the same hours.
  • Stopping texts. Every text ends with a stop link. Opening it only asks; pressing Stop texts puts the number on the stop list [src: app/src/Sms.php:215], and from then on nobody on your team can text it [src: app/src/Sms.php:261]. The stop page shows only part of the number.
  • Each text goes once at most. A text is claimed under its own key before your gateway is called [src: app/src/Sms.php:295], so a double click, two tabs or two runs at the same moment cannot send it twice. When the gateway refuses or cannot be reached, the reason is written next to that text on the Texts page and nothing else is affected [src: app/src/Sms.php:283]. A reminder your gateway refused is tried again on a later day, never twice on one day [src: app/src/Sms.php:298], and a reminder run stops at the first refusal, since a gateway that refused one will likely refuse the next [src: app/src/Sms.php:355]. The consent is read again just before each reminder goes [src: app/src/Sms.php:287].
  • A daily cap. At most 100 reminders go out a day by default (Settings → Text messages, Most reminders a day), so a mistake can never turn into a large bill [src: app/src/Sms.php:351]. A link you send by hand is not counted.
  • The public demo never sends a text [src: app/src/Sms.php:102].
  • What is kept. The links in a text carry a random token that Waiverly keeps only as a SHA-256 hash [src: app/src/Sms.php:302], so a backup holds nothing that opens them. Anonymizing a signer also erases the name and number on the texted request that led to their waiver [src: app/controllers/v3.php:458]; a number on the stop list stays on it, so it is never texted again.

Settings

Settings cover the business and its branding: business name, e-mail, phone and address, website, accent color, timezone, whether to send the signer's copy, and the public base URL used when building links [src: app/controllers/app.php:515].

Text messages have their own card on the same page (see above). Changing any of them requires the settings permission, which only an admin holds [src: app/controllers/v3.php:47].

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:370]. The scheduled backup endpoint writes a backup, prunes old ones and reports what it did [src: app/controllers/v3.php:587].

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:370]. 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:47]:

ToolWhat it does
list_templatesTemplates with their current version and expiry policy
list_waiversSigned waivers, filterable by template and status
waiver_detailOne waiver, with the template text exactly as it was signed
signing_linkThe public signing URL for a template
statsCounts 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 afterward is what the person actually agreed to.

Data protection

A single subject's data can be exported [src: app/src/Gdpr.php] or anonymized in place [src: app/src/Gdpr.php]. Backups can be written on demand as well as on a schedule [src: app/src/BackupExport.php].

Anonymizing a signer (on the waiver's page, once it has expired or been superseded) replaces their name, email, phone, date of birth, the guardian's name and relation, both signature images, the IP address, the answers, the photo and the error text of a failed copy, the name and number on the texted request that led to the waiver, and the copies of their name and email on each arrival check-in. The reference code, the dates, the template version and the hashes stay (4.1.1).

Kept: the signer's phone number on the text stop list, if they asked to stop texts, so they are never texted again.

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.

Looking after your install (from 4.0)

Updates from inside Waiverly

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; Waiverly 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. Waiverly is unavailable for about a minute while this runs. The update replaces Waiverly'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 Waiverly 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 Waiverly 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 climbing gym's waiver templates and six months of signed waivers to try things on. You can remove it in one click. [src: app/install/index.php:111]. 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 waivers and templates? 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 Waiverly 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 Waiverly 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/waiverly/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)

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

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 Waiverly'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 Waiverly 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 Waiverly 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 Waiverly'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].

Export selected on the waivers register now writes what a signer typed as text, as the mailing-list export already did: a signer named =1+1 opens in a spreadsheet as the text '=1+1, never as a formula [src: app/controllers/v3.php:683].

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 Waiverly · Quickstart · API · Test run