Dev log · 2026-07-07

An offline HRIS for a community police office 👮

This project is a separate experiment, apart from plastron. It tests CRDT SQLite for offline merge before we add CRDT SQLite to the plastron substrate. The plan is that the substrate also operates offline.

An HRIS is a human resources information system. This proof of concept is one self-contained HTML file. It uses no server and no network, and it encrypts the data at rest. The demo shows a fictional Juárez Police Department. The use case is any small community police unit that cannot depend on enterprise cloud HR.

Do not load real personnel data into this app. The app is a proof of concept only. It has no completed Privacy Impact Assessment, threat and risk assessment, or formal security authorization. The Juárez roster is fully synthetic. Load real data only after the organization completes the governance requirements (see below).

An experiment apart from plastron

The primary work here is plastron. Plastron is a polyglot reactive substrate (a spreadsheet kernel and a desktop shell, hosted at plastron.ca). This HRIS prototype is a separate experiment. It is a self-contained experiment with CRDT SQLite (cr-sqlite) for offline-first data. This data must merge correctly after a day of offline changes.

Today, plastron.ca hosts plastron, and a server stores the data. The goal is still the zero-server, offline-capable build in one file. (Offline plastron has no OPFS at this time.)

Before we add CRDT SQLite to the full kernel, we wanted a smaller test case with difficult conditions:

This build gave us lessons about changeset export, the merge procedure, audit tables, and CSP-safe inlined WASM. The plan is to put these lessons into plastron as a future cr-sqlite integration. They will not replace the spreadsheet model.

The offline HRIS is a practical test of a capability that will be necessary for plastron. The Juárez names and data are only the story for the demo.

The problem

A community police office must plan its staffing. The plan must answer these questions:

Enterprise HRIS products assume a connection that is always on, identity federation, and a vendor security boundary. A neighbourhood command post, a borrowed laptop, or a planning cell that works from USB drives has none of these.

The design question was direct. Can several planners keep one consistent set of personnel data with no server, no public internet, and no plaintext rows on disk at any time?

The idea

The idea is to supply one file: a built index.html. The file embeds React, SQLite, and the cr-sqlite CRDT extension as inlined WebAssembly. Operators double-click the file. The live database exists only in memory. Only encrypted blobs go to disk. These blobs are the morning truth.hrisdump file and the evening lastname.hrischanges delta files.

The Content Security Policy sets connect-src 'none'. Thus, even if an attacker takes control of script execution, the page has no fetch, WebSocket, or beacon path off the machine. This is a deliberate trade-off. It is good for an air-gapped daily procedure. It makes SaaS analytics impossible.

The demo shows Juárez PD: Zona Centro, Anapra, fictional badge numbers, and invented salaries. There are two reasons. The original spreadsheet model came from a Canadian police HR layout. We wanted a story that is clearly not a production system. The operation of the app does not depend on the jurisdiction. Only the labels and the seed data change.

The daily cycle

The daily cycle uses the same model as git, but for staffing data. Each planner changes a local copy, and then the coordinator merges the changes. Two things are different from git. The app encrypts the merged file. The coordinator can do the merge with only a browser and no administrator rights. Figure 1 shows the cycle.

The daily cycle of the offline HRIS In the morning, the coordinator distributes the truth file and the passphrase. During the day, each planner works offline. In the evening, each planner exports encrypted changes, and the coordinator merges them into the truth file for the next morning. Morning The coordinator distributes the truth file and the passphrase, through a separate channel Day Each planner works offline with the database only in memory Evening Each planner exports encrypted changes one .hrischanges file for each planner The coordinator merges the changes into the morning truth file The coordinator exports the merged truth the truth file for the next day Next morning
Figure 1. The daily cycle: one truth file in the morning, offline work during the day, one merge in the evening.

Morning — one truth file

  1. The coordinator distributes truth-YYYYMMDD.hrisdump.
  2. The coordinator gives the organization passphrase for the day through a separate channel, never together with the file.
  3. Each planner opens the HTML file on the local machine.
  4. Each planner decrypts the truth file.
  5. Each planner passes the user gate: the planner selects an identity from a pre-provisioned operator list.
  6. The app records a session_open event and writes a morning_db_version watermark.

Day — offline work

Evening — merge

  1. Each planner uses Export my changes to make an encrypted .hrischanges file. This file is not a full database dump.
  2. The coordinator opens the same HTML file and loads the morning truth file.
  3. The coordinator uses Merge operator changes on the Data & Security screen to apply each delta file.
  4. cr-sqlite merges the changes, and the result does not depend on the order. Changes that conflict converge by the CRDT rules. The merge does not use last-writer-wins file replacement.
  5. The coordinator exports the merged truth file for the next day.

An optional Node script, merge-hris.mjs, exists for automation. The script is not necessary to operate the cycle.

CRDT solves data convergence. It does not automatically solve accountability. For this reason, change_event and session_log are full tables in the database, and they sync through the same merge mechanism.

Protected B and CSE cryptography

Personnel files are not open data. They contain names, assignments, and compensation. In the Government of Canada categories, personnel files are usually Protected B. Protected B means that unauthorized disclosure can cause serious injury outside the national interest. See the TBS Standard on Security Categorization (Appendix J) and the 2023 direction on personal information in the aggregate.

A community police office that handles officer PII has the same injury model. This is true even when the formal framework is municipal privacy law and not Treasury Board policy.

Technical requirements that the app obeys

For encryption algorithms, CSE publishes ITSP.40.111 (Cryptographic algorithms for UNCLASSIFIED, Protected A, and Protected B information). For a file at rest, the dump format uses the strongest practical set of algorithms from that document:

The Directive on Security Management, Appendix B §B.2.3.4–§B.2.3.6 states the rules for storage and transit of sensitive electronic media. Data at rest, in transit, and on portable media must have protection that agrees with its sensitivity. Encryption is necessary on public networks and on networks with risk. This app does the at-rest part for portable dumps. Coordinators must still have a procedure for USB handoff or for approved encrypted channels.

What the app enforces in software

What the organization must do

AES alone does not make an app correct for Protected B. The other requirements are for the organization, not for the software. The browser cannot implement the controls in this list. A jurisdiction must still have these documented controls before operational use with real officers:

Controls of the app and controls of the organization The app enforces: no plaintext export path, a memory-only database, an operator identity gate, append-only audit tables, and encrypted changeset export. The organization supplies a Privacy Impact Assessment, a threat and risk assessment, an authorization to operate, a need-to-know roster, and a passphrase ceremony. The organization also supplies a media policy, a breach response plan, a coordinator SOP, and operator training. Both groups are necessary before operational use with real personnel data. The app enforces No plaintext export path Memory-only database Operator identity gate Append-only audit tables Encrypted changeset export The organization supplies Privacy Impact Assessment Threat and risk assessment Authorization to operate Need-to-know roster Passphrase ceremony Media policy Breach response plan Coordinator SOP Operator training Both groups are necessary. Operational use with real personnel data
Figure 2. The controls that the app enforces and the controls that the organization must supply.

The project documents put these requirements in two separate groups: the build requirements in the app, and the governance procedure requirements. We deliberately delayed the governance group until the prototype becomes stable.

Why use a browser tab?

The rules of a community office can prohibit the installation of server software or database engines on managed PCs. A static HTML file with inlined WASM is unusual. But it has properties that enterprise software rarely gives together:

The GitHub Pages host (demo) is a convenience for evaluation. The intended deployment is different. You download the file and run it locally on a controlled machine, from file:// with no backend.

Status

The repository rheophile10/offline-Protected-B-HRIS includes these items:

The project examines how much of a personnel HRIS can go into one encrypted, mergeable, offline file. It is not a production authorization. It is not a separate branch of the product roadmap.

The next step for plastron is to put these CRDT SQLite patterns into the substrate. Then cels, sheets, and shared state can keep offline changes and merge them in the same way. If this experiment continues, the next steps for it are:

How to use the app

Each video below shows a Playwright test from tests/hris.spec.ts. The test runs on the real built app, and a screen recorder records it. The recording has an 800 ms slow-motion delay between actions, so each step is easy to follow. The how-to guide of the repository includes the same videos. USER-STORIES.md contains the user stories. The README contains the setup notes and the build notes.

Export your changes before you lock or close the app. The app discards unsaved work. This behaviour is intentional. The app is one file. Download the built index.html and double-click it. Then each procedure below works offline. A server, an installation, and administrator rights are not necessary.

Start a session US-1

You open the app, load the data for today with a passphrase, and select your operator identity.

  1. Open the file. The app shows the locked gate first.
  2. Keep the Demo data selection.
  3. Enter a passphrase.
  4. Click Start session.
  5. Select an operator. The dashboard shows 27 officers and the staffing gaps.

Add an officer US-2

You add a new hire. The roster and the dashboard update, and the audit log attributes the change.

  1. Open Officers.
  2. Click + Add officer.
  3. Enter the badge and the name.
  4. Click Save. The headcount increases. The app attributes and audits the change.

Assign an officer to a position US-3

This procedure keeps the staffing levels and the vacancy deficits current.

  1. Open Assignments.
  2. Click + Assign officer.
  3. Select the officer and the position.
  4. Click Assign. The staffing views recompute automatically.

Query data and export an encrypted CSV file US-4

You run ad-hoc SQL and share the numbers. No plaintext goes to disk at any time.

  1. Open SQL Console.
  2. Write a query.
  3. Click Run.
  4. Export the result. The export makes only a .csv.enc file.
  5. Decrypt the file later from Data & Security.

Export changes and merge them (no Node) US-5

The coordinator merges the offline changes of all operators in the same HTML file.

  1. Operator: click Export my changes. The app makes an encrypted .hrischanges file.
  2. Coordinator: open the morning truth file.
  3. Coordinator: click Merge operator changes.
  4. Coordinator: select the delta files. CRDT makes the divergent changes converge.
  5. Coordinator: click Export merged truth to make the truth file for the next day.

Recruit and hire an applicant US-7

You move applicants through the recruitment stages and change a successful applicant into an officer.

  1. Open the Recruitment kanban.
  2. Add applicants.
  3. Move an applicant through the stages to Offer.
  4. Click Hire →. The app creates an officer record and an active assignment. The headcount updates.

Training and compliance US-8

You see the certifications that expire, and you renew certifications.

  1. Open Compliance.
  2. Read the Expired, Expiring, and Valid counts and the firearms-current ratio.
  3. Filter by KPI.
  4. Click Renew or + Record certification.

Leave and absence US-9

You monitor the leave requests, the approvals, and the persons who are on leave today.

  1. Open Leave.
  2. Read the counts: on-leave today, pending, and upcoming.
  3. Click + Request leave to add a request.
  4. Click Approve or Deny on each pending row.

Workforce planning US-10

You see the projected vacancies before the retirements occur.

  1. Open Planning.
  2. Read the projected deficit at the +1/+2/+3/+5-year horizons.
  3. Read the list of officers who are near the pension milestone. Use this list for hire-ahead decisions.

Audit trail US-11

You see who changed each item. This attribution stays after the merge.

  1. Open Audit.
  2. Read the change log (operator, entity, action) and the session events.

The logs are CRRs. They go with each changeset into the merge.

Officer file US-12

The officer file shows emergency contacts, performance reviews, and conduct records in one place.

  1. Open Officer File.
  2. Select an officer.
  3. Read the contacts, the reviews, and the conduct records.
  4. Add a review or update a disposition.

Equipment register US-13

You monitor who holds each firearm, vehicle, radio, or camera.

  1. Open Equipment.
  2. Read the in-service, issued, and available counts.
  3. Click Issue or Return for an asset.
  4. Mark an asset as Maintenance or Retired.

Lock the session US-6

You erase the decrypted data from memory when you go away from the machine.

  1. Export your changes first. The app discards unsaved work, and this is intentional.
  2. Click Lock session. The app goes back to the gate. It erases the database, the key, and the identity from memory.

The full guide is a separate page: rheophile10.github.io/offline-Protected-B-HRIS/how-to.html

References

More from the dev log