Documentation
Security and privacy
What this covers
Kestrel is a student information system: enrolment, attendance, assessment, wellbeing and medical records on one student record. That is children's personal information, some of it health information. Some of it, such as parenting orders and child-safety records, is among the most sensitive material a school keeps.
This document describes the posture as it stands, so a school can see what protects its records without asking us.
Where the data lives
One virtual machine runs the application container and one PostgreSQL instance. Each school's records are kept in their own Postgres schema inside that one database. There is no document store and no file upload: everything a school keeps here is a database record. No data is sent to any third party for processing, analytics or model training. A school's own tags on its public enrolment pages run in the visitor's browser, after the visitor accepts, and the Privacy Policy describes them.
The host admits SSH, HTTP and HTTPS from the internet and refuses everything else, so the database and the application are reachable only through the site. An intrusion-prevention agent reads the host's authentication log and bans an address that keeps failing to sign in to it. Sign-in to the host itself is by key alone.
Encryption, and what each layer protects against
The second column is the one worth reading. "Encrypted in transit and at rest" is true of almost all products and tells a reader nothing about which attacks it stops.
- In transit, browser to Kestrel
- The site offers TLS 1.3, and still accepts TLS 1.2 for the programs that call the API. It offers no older version. The 1.2 cipher suites are ECDHE with AEAD only, so both versions are forward-secret. Certificates are issued by Let's Encrypt and renewed automatically by the reverse proxy. HSTS is set for two years, including subdomains.Protects against Anyone on the same network as the school office reading or altering a session.
- In transit, inside the host
- The application reaches its database over TLS 1.3. Before it sends anything, it checks the certificate against the one authority allowed to have issued it. Both ends of this hop are ours, so the hop requires the current version: the application will not open a connection below 1.3, and the database will not accept one. The database also refuses a connection that is not encrypted at all. Neither the application nor the database is published to the internet.Protects against Another program on the same host reading the database traffic, or answering in the database's place.
- At rest
- The application itself encrypts the fields classified restricted with AES-256-GCM, under a separate key for each school, before they reach the database. The key that unwraps a school's key is kept outside the database and is not in any backup of it. Destroying one school's key makes that school's records unreadable and leaves the records of all other schools untouched.Protects against A stolen database dump, a backup that ends up somewhere it should not, and anybody whose access to the database is broader than their access to the application.
- Secrets
- Secrets are resolved at deploy time and age-encrypted to a key generated on the server, which never leaves it. The operator's own copy is age-encrypted too, to a key the operator holds, and each command that reads it decrypts it into its own memory. A deploy refuses to run while a plain-text copy is on the operator's machine. No secret is in git or in the container image.Protects against A leaked repository, a leaked image, or a stolen laptop.
- In the browser
- The Content-Security-Policy permits no external origin in the console or the family portal. Fonts are self-hosted, icons are bundled server-side, and there is no analytics, tag manager or third-party script of any kind there. Inline script is refused unless it has a nonce this server minted for that single response, so a script injected into a page does not run. Framing is refused, except that a school's public enrolment pages may be framed by the websites the school lists, at most ten, each named in full. The confirmation on those pages may load the school's own Tag Manager container or Meta pixel once the visitor accepts, and the policy names those hosts on that page alone, so a Tag Manager Custom HTML tag does not run.Protects against Cross-site scripting, clickjacking, and data leaving to a third party by accident.
Access control
- One Postgres schema per school
- Each school's record tables are in a schema of their own, reached through a database role that has privileges on that schema and no other. The role is assumed for the length of one transaction and dropped at commit. Postgres itself refuses the crossing, so the refusal does not depend on our query builder getting it right.
- Proved by attempting the crossing
- The test suite connects as the least-privilege application role against a real Postgres and tries to read another school's rows directly. It asserts the database refuses. A separate test asserts the suite is not running as a superuser, because that assertion would otherwise pass for the wrong reason.
- Nothing outside the data layer speaks SQL
- A build check fails if any handler imports the database driver, keeps a connection open, writes raw SQL or names a schema. That check stops a single handler from stepping around the isolation while all the isolation tests still pass.
- Passwordless sign-in
- Sign-in is by passkey, with a one-time email code as the fallback. No password is stored, so there is no password to leak, reuse or phish. A school that has finished enrolling passkeys can turn the emailed code off. Kestrel refuses to turn it off while somebody at that school still has no key.
- A school can require a second factor
- An emailed code proves one thing, that the person can read a mailbox. A school that must show multi-factor authentication turns on one setting, under Settings and Sign-in, and each member of staff then signs in with a passkey. The passkey ceremony has to verify the person with a PIN or a biometric on the device, so the passkey is two factors: the device and the person who unlocks it. Kestrel asks the browser for that verification and checks the device's answer before it makes a session, so a script cannot skip the step. A session made by an emailed code or by a directory sign-in reaches nothing at that school. Its owner is sent to a page where they add a passkey to the device or sign in with the one they have, and the new session opens the console. The setting takes effect on the next request, so a session opened before it was turned on is asked for a passkey too. Families using the portal are not asked.
- Sign-in through the school's own directory
- A school can point Kestrel at Microsoft Entra ID, Google Workspace or any other provider that speaks OpenID Connect or SAML 2.0. Staff then sign in with the credentials they already hold, and leaving the directory closes the door here too. The directory's groups decide standing and the roles the school maps to them at each sign-in, so a person taken out of a group has their standing narrowed and that group's roles removed the next time they sign in. A role the school granted by hand stays until the school removes it. Kestrel checks the token's signature against the keys the directory publishes and ties the token to the browser that began the sign-in. It refuses an account outside the school's own domain. The client secret is stored as ciphertext under the school's own key and is never sent back to a screen.
- Server-side authorisation on each route that serves student data
- Each guarded route checks the session on the server, and a build check fails if a handler omits the check. The browser-side route guard turns away a visitor with no session as a convenience, and the server relies on its own check.
- Configuration is an administrator's act
- A person holds standing at one school, as an administrator or as staff, and that standing does not extend to another school they also work at. Only an administrator of that school can change what the whole school sees, such as its name, its palette, who else works here and whether an emailed code is a way in. The server refuses the change from anyone else, and a build check fails if a configuration route omits the check. The browser also hides the section from anyone else. That is a courtesy, and the server's refusal is the control.
- Seraco's operator view
- Seraco reads how often each screen is opened across the schools on a deployment, at /operator. The view reads the anonymised counts the Privacy Policy describes and the number of staff at each school, from the shared platform schema alone. Each endpoint of the view binds no school, so it never assumes a school's database role, and a test fails the build if one does. Operator standing is granted and revoked at a terminal on the server, by a named person, and by nothing in the product. A school's administrator has no operator standing, so the server refuses them, as it refuses each other account. An operator's session reaches the view only when a passkey with the person verified on the device made it. Each grant, each revocation, each operator sign-in and each read of the view is written to the operator trail, which names the operator and the time, and the database refuses a change to or a deletion of an entry there.
- Classification declared per field
- Each field an extension can read has one classification: internal, sensitive or restricted. A capability granted at one tier reads fields at that tier and below. A field without a classification is returned to no-one, so a new column reaches no extension until somebody classifies it. The same declaration decides what is encrypted: a field classified restricted is stored as ciphertext, and a build check fails if one is not. A death recorded against a person and the reason someone left the school are both restricted, and an ordinary read capability reaches neither.
The audit trail
Kestrel records each read and each change of a record. The entry names who did it, which records it reached, when, the highest confidentiality tier in the answer, and whether it was allowed or refused. Refusals are recorded alongside successes, because a caller repeatedly asking for something they may not have is the signal worth keeping. A copy taken out of Kestrel needs a stated reason, and that reason is recorded with it.
The request pipeline writes the trail from a declaration on each endpoint, so an endpoint added later is covered by the same rule. The trail is in the school's own database schema, behind the same isolation as the records it describes, and it moves with the school's records.
Nothing in Kestrel can edit or delete an entry once it exists, and Kestrel checks that no entry has been changed. Each entry is fingerprinted over the fingerprint before it, so an entry that was altered, removed or reordered breaks all the fingerprints after it. An administrator can check a date range from the screen. The screen tells them what happened, which entry it happened at, and what to do about it. Disposal removes an aged stretch as a whole and records what went and where the chain resumes, so a lawful disposal can be told apart from an alteration. The chain proves the trail is internally consistent. It is not anchored anywhere outside this host, and no copy of the fingerprints is published elsewhere.
An entry names a record by reference and never reproduces it, and any count it records is a number with no content. A trail entry outlives the record it names, so removing a person does not remove the evidence of who read them. Entries are kept as their own retention class, listed below.
Third-party plugins are recorded under their own identity, so a scheduled run at two in the morning is attributed to the plugin that ran it.
Retention and disposal
Each record has a retention class. Removing a record from the working set and disposing of it are separate acts. A registrar removing a duplicate has made no decision about a forty-five year obligation, and should not be able to.
- Child-safety records: 45 years, counted from the child's date of birth, because the obligation is owed to the child.
- Attendance registers and NCCD evidence: 7 years past the year they record.
- Immunisation status: 3 years past the enrolment it was collected for.
- The audit trail: 7 years, counted from the access, because a privacy incident is often found years after the access that caused it. A trail entry outlives the record it names.
- An enquiry that never became an application: 2 years after it closes. A person is created at first contact, so most person records belong to people with no ongoing relationship to the school. Keeping them indefinitely is what the Children's Online Privacy Code exists to stop. Kestrel closes an enquiry nobody has pursued, and the period runs from then.
- Legal hold overrides all of it. A hold is checked before a record's age and before whether it was deleted, because a hold that applied only to records past their date would protect nothing.
Where a record has more than one class, the longest wins. A person's file and the names and affiliations on it are one record for this purpose, so nothing on it is disposed of while any part of it is still owed.
Disposal runs each night at half past two in your school's own time zone. It takes records that have been deleted and are past their period. A record that is closed but not deleted stays until someone deletes it, unless the area that owns it deletes it after a period your school sets: the details of a family who enquired or applied and never started are deleted on the periods under Settings, Applicant records, and the rest of the record after them. Each run records how many records went, under which class, and how many stayed and why. It records counts and none of the records. A list of what was disposed of would be a copy of what we had just accepted an obligation to destroy.
Reaching, correcting and deleting a person's record
A person can ask to see what a school keeps about them, ask for it to be corrected, and ask for it to be deleted. Each request is recorded in the school's own schema from the day it arrives, with a reference the school quotes back. Two dates apply to it: acknowledged within 5 business days, answered within 30 days. Both are fixed when the request arrives, so a later change to either period does not move a date somebody is already waiting on.
The register is at /app/settings/privacy-requests, behind two capabilities of its own. The settings page shows how many requests are outstanding and how many are past a date. Reading the register, recording a request and recording what was done about it are each in the audit trail. The row contains references, dates and what was decided. It never contains the name, address or phone number of whoever asked, and the correspondence stays in the mailbox it arrived in.
A deletion request runs into the retention periods above. The answer says which parts of a record are being kept, which obligation keeps them and when that period ends. The Privacy Policy tells a parent, a student or a member of staff how to make the request.
Backup and recovery
Production is backed up three times a day, at 03:15, 11:15 and 19:15 Melbourne time. Each run dumps the whole database in one transaction-consistent snapshot, which captures all schools at the same instant, then compresses and encrypts it. If the job cannot encrypt the copy, it refuses to run. Eight days are kept.
The restore is tested. On the first of each month the most recent backup is decrypted into a scratch database beside the live one. The schema count and the school registry are then checked against what was recorded when the dump was taken. A backup job that has failed silently for six weeks looks like one that works, and so does a bundle that restores into an empty database. Each run of both jobs records whether it succeeded and the time it finished, as a metric the host collects. The last good backup and the last good verification are therefore on record.
The backups are kept on the host that runs the database, encrypted, for eight days. They cover a dropped volume, a bad migration, a deleted container and an operator mistake.
The recovery point is up to eight hours, which is the gap between scheduled backups. A recovery restores whichever backup you choose. Shortening that gap means archiving the write-ahead log, which would put the archive on the same host as the database it protects. That would improve recovery from the faults the backups already cover and do nothing for the loss of the host. Eight hours is a decision taken on that basis, and the reasoning behind it is written down.
The recovery time is measured on a real restore. The monthly verification is the only full restore of the production database anyone performs on a schedule, so the number comes from it. The verification records how long the bundle took to decrypt and load, and an alert reaches us when a restore runs longer than thirty minutes. That is the figure we commit to, and it covers the database coming back. Rebuilding the host underneath it is a separate job. That job has not been timed, and its duration is outside the number.
The backups meet the recovery obligation and none of the retention ones. Eight days of bundles is how far back a mistake can be undone. The forty-five year child-safety retention and the shorter classes above are met by the live database, where the records are, and by the nightly disposal that applies each class. The same eight days make a disposal real: a record destroyed tonight is gone from all backups within eight days. A longer archive would keep a copy of what a school had just accepted an obligation to destroy.
The test environment is not backed up. It contains invented data and exists to be rebuilt.
Availability
The host asks the application every minute whether it can serve, and records the answer. It asks twice. The first question goes to the internal port, which answers only when the database is reachable and the schema is at the version this build expects. The second goes to the public address, through the reverse proxy and its certificate. The two are asked separately because they fail apart. A healthy application behind a certificate that stopped renewing is an outage for a school, and it cannot be seen from inside the host.
An alert reaches us when either answer has been refused for five minutes, which is long enough that an ordinary release does not raise one. The probe runs on the host it is asking about, so it cannot see a network fault between that host and a school.
A second probe runs on a separate server outside the production host. It asks the public address every 30 seconds and tells us when 2 checks in a row go unanswered, so an outage of the host, or of the network in front of it, reaches us within two minutes.
The same alert sends your school's nominated contacts a message saying Kestrel is not available, and a second message on the recovery saying how long the outage lasted. The addresses are held on the host that runs the deployment, outside the application, so the message still goes out while Kestrel itself cannot serve. Tell us who should be on that list and we will set it.
The target is that Kestrel answers for 99.9% of the school day, which is 7:00 am to 6:00 pm on a weekday, in the school's own time zone, outside planned maintenance. The Terms of Service state it, and missing it gives a school no service credit. The status page at kestrel.hoverpoint.io/status.html shows whether Kestrel is answering, its availability on each of the last 90 days, and planned maintenance before it starts.
Capacity
The deployment serves forty people using Kestrel at once, across the schools it serves, and it has been measured serving half as many again. That is the 50 per cent headroom above the figure. A person here is a member of staff working in the console with a pause of about five seconds between one request and the next, which is faster than somebody reading a screen before they act on it.
The figure comes from a measurement. The production build of the application runs in a container limited to the memory the production container is given, with one processor, beside a database limited to its production memory and one processor, with the same number of database connections production uses. The school it serves is generated: a thousand students with their families, staff, timetable and a term of attendance. Each simulated person is a teacher on the roll screen who opens the roll, saves a mark, reads the roll again and opens a student's record. People are added in steps, and a step counts only when no request fails and nineteen requests in twenty are answered within one second. The published figure is at most two thirds of the largest step the run sustained.
It was last measured on 16 September 2026, on our own hardware under those limits and away from the production host. A change to either memory limit or to the number of database connections stops our build until the measurement has been taken again.
One application container serves the deployment. The rate limits count requests inside that container, so adding a second container is a change to the application and cannot be made by configuration alone.
Incident response
If we become aware of unauthorised access to a school's data, we contact that school's nominated privacy contact directly, in writing, with what we know and what we do not. Australia's Notifiable Data Breaches scheme requires an assessment within thirty days, and notification to affected individuals and the OAIC where serious harm is likely. The school has the relationship with those families, so our job is to give it what it needs to make that assessment quickly.
Reach us at security
Sub-processors
This is the complete list, and nothing else receives any data. The Privacy Policy shows the same list, because both pages read it from one place.
- FlowVPS
- Runs the virtual machine that hosts the application and its database.Organisation Alpha Layer Pty LtdContact admin@flowvps.comReceives All of it, as the operator of the host.Lawful basis The school's records are stored on the host to provide the service the school contracted for, which is the purpose they were collected for (APP 6.1). The host is in Australia, so APP 8 does not apply.In AustraliaFrom a school in England A school in England's records are stored on this host in Australia. The UK has no adequacy regulations for Australia, so the school and Hoverpoint sign the ICO's International Data Transfer Agreement, with a transfer risk assessment, before the school's records are loaded.
- Let's Encrypt
- Issues the TLS certificate that secures the connection to the site.Organisation Internet Security Research GroupContact privacy@abetterinternet.orgReceives The domain name. No personal information.Lawful basis No personal information reaches it, so no privacy-law basis is needed.In United StatesFrom a school in England No personal information reaches it, so no transfer rule applies.
- Resend
- Delivers the one-time code that signs a member of staff in by email.Organisation Plus Five Five, Inc.Contact support@resend.comReceives The email address the code goes to, and the code. No student information.Lawful basis The address is used to send the code the person asked for, which is the purpose it was collected for (APP 6.1). Resend is outside Australia, so Hoverpoint stays accountable for its handling under APP 8.1, and a data processing addendum with Resend binds it to protect the address.In United StatesFrom a school in England The address of a member of staff at a school in England reaches Resend in the United States. Resend's Data Processing Addendum covers that transfer with the UK standard contractual clauses and the EU-US Data Privacy Framework.
There is no analytics provider, no error-reporting service, no customer-messaging widget and no advertising network. The content policy blocks them. A Tag Manager container or a Meta pixel a school sets for its own public enrolment pages is the school's, loaded in the visitor's browser once they accept, and Kestrel sends it nothing.
Children's Online Privacy Code
The OAIC must register the Children's Online Privacy Code by 10 December 2026. Its start date and any transition are for the registered Code to set. It is expected to cover an online service a child is likely to use, which includes Kestrel and a school's own pages on it. Three design decisions are made with it in mind. A person exists from first contact, so the enquiry retention class above decides how long the record of someone who only enquired is kept. Kestrel sends no data for analytics or model training. A school's own advertising tags load only on the confirmation of an enquiry or a tour booking, after the adult accepts, and never on a form a student fills in, so a child's answers reach no tag.
Hoverpoint assesses the privacy impact of Kestrel on children and publishes the register of those assessments, with the finding of each, at Privacy impact assessments.
For a school in England
A school in England is under the UK GDPR and the Data Protection Act 2018. The school is the controller, and Hoverpoint Pty Ltd is its processor under Article 28, acting only on the school's written instructions. Everything above applies to its records too.
- Where the data lives. A school in England's records are stored in Australia, on the host above. The UK has no adequacy regulations for Australia, so before the school's records are loaded, the school and Hoverpoint sign the ICO's International Data Transfer Agreement, with a transfer risk assessment.
- Who receives it. A school in England's personal information reaches FlowVPS in Australia, Resend in United States. The sub-processor list above gives the safeguard for each.
- Incident response. We tell the school without undue delay after we become aware of a breach of its data, so that it can report the breach to the ICO within 72 hours.
- Reaching a record. The school answers a subject access request within one month, or within three months where it tells the person inside the first month that the request is complex. The privacy register fixes the answer date on the request on the day it arrives.
Questions this does not answer
Send them to security