Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EA QMS — Change Control

A Change Control module for a Quality Management System. A change is raised, approved, implemented with evidence, approved again and closed — or rejected at either gate and sent back for rework. Every decision is signed with the acting user's own credentials, and every state change is written to an audit trail nothing in the application can amend.

It is built the way validated systems are built, because that is the discipline it comes from: specification first, signed off before any code existed, around one rule — a change with no audit trail is a change that did not happen.

Backend · Frontend · Specification

The dashboard, signed in as a CC Owner

Run it

There is no hosted demo. There is one command.

git clone https://github.com/lain-the-coder/ea-qms
cd ea-qms/deploy/docker
docker compose up -d

Then open http://localhost:8080.

Role Email Password
Admin [email protected] DevPassw0rd!
CC Owner [email protected] DevPassw0rd!
Approver [email protected] DevPassw0rd!
Approver [email protected] DevPassw0rd!
Viewer [email protected] DevPassw0rd!

The second approver is assigned to nothing, and exists so one rule can be seen rather than described: sign in as them and open a record assigned to the other approver.

qms-migrator Exited is correct, not an error — it creates the tables and stops. A one-shot container then seeds seven change controls covering all six states, one of them rejected at both gates and eventually closed, by signing in and driving the real API rather than inserting rows — so those records' signatures and audit entries are the ones the application actually wrote. DEMO_SEED=0 docker compose up -d gives a clean system instead.

The API reference is at http://localhost:1304/docs. Never installed Docker? deploy/docker/README.md starts from zero.

The API reference, served by the API itself at localhost:1304/docs

What you are looking at

                    ┌──────────────── T5 reject ───────────────┐
                    │                                          │
                    ▼                                          │
  (T1) ──▶ Initiated ──── T2 ────▶ Pending Impl Approval ─────┘
              │                              │
              │ T3                           │ T4 approve
              ▼                              │
          Cancelled                          │       ┌─ T8 reject ─┐
                                             ▼       ▼             │
                                        In Implementation          │
                                             │                     │
                                             │ T6                  │
                                             ▼                     │
                                   Pending Final Approval ─────────┘
                                             │
                                             │ T7 approve
                                             ▼
                                           Closed
# From To Action Actor E-signature
T1 — Initiated Create CC Owner No
T2 Initiated Pending Implementation Approval Submit for Approval CC Owner Yes
T3 Initiated Cancelled Cancel CC Owner Yes
T4 Pending Implementation Approval In Implementation Approve Approver Yes
T5 Pending Implementation Approval Initiated Reject Approver Yes
T6 In Implementation Pending Final Approval Submit for Final Approval CC Owner Yes
T7 Pending Final Approval Closed Approve Approver Yes
T8 Pending Final Approval In Implementation Reject Approver Yes

Permissions are per role, per state — 49 of 50 fields. What you may edit depends on your role and the record's state, and at the two gates on a third thing: whether you are the assigned approver, not merely an approver. A CC Owner edits twenty-four fields while a record is Initiated and none afterwards. The fiftieth field, supporting_documents, is defined in the specification and descoped from release 1 — the more interesting half of the number.

CC-001 in Initiated, as its owner — the editable fields, with the mandatory ones marked

Submitting an incomplete record returns every unmet requirement at once

Validation is collected, not fail-fast: a submission that is short of sixteen mandatory fields says so once, rather than sixteen times.

Every transition but creation is signed. The signer re-enters their own email and password, the system records what they attested to, and failed attempts are recorded too. Signatures are never overwritten: a record through a rejection loop carries both signatures at the gate it used twice.

The signature dialog — two components, and only the signed-in user's own credentials are accepted

CC-005, rejected at both gates and eventually closed — eight signatures retained

The Admin manages people, not records. They create users, change roles and deactivate accounts — but cannot change a user's role, or deactivate them, while that user is attached to a record still in progress. Nothing can reassign a record, so either change would strand it at a gate nobody can act on.

User management, as the Admin

Three repositories, and why

What it holds Why it is separate
Change-Control-HTML-Design The BRD, the security matrix, the database design, the state, sequence and ER diagrams, and an HTML prototype of every screen It was finished before the code began and it is a deliverable in its own right
ea-qms-backend Go REST API. 23 endpoints, PostgreSQL, sqlc, goose, argon2id, JWT with opaque refresh tokens Its own image, its own lifecycle, and consumable without a UI
ea-qms-frontend Svelte 5 SPA, built in 18 verified steps. Caddy serves it and proxies /api/* onto one origin Same

Three dates say more about the method than a description would:

  • 20 April 2026 — the BRD is signed off.
  • 18 July 2026 — chore: scaffold Go backend + users table migration, the first line of Go. Eighty-nine days later.
  • 16 August 2026 — docs(brd): V1.2 — align the BRD with what was built, nine amendments recording where the implementation departed from the specification, each with its reason.

How it was built

Specification, design, build, verification, with traceability between them, and the document corrected when the build proved it wrong rather than the deviation left undocumented. The build ran as numbered, independently verifiable steps. A step was not finished when it was written but when a stated check passed — something to click, an endpoint to call, a SELECT to run — and nothing moved on before that. Every decision was recorded with its reasoning and the alternative that was rejected, including the ones that later reversed earlier decisions, which are left in place rather than tidied away. Deferred items are kept in a register separate from defects, because conflating the two is how real problems get lost among accepted trade-offs.

The code was written with Claude Code, under a review gate: every line read and approved before it was committed, and no step begun before the previous one was verified.

The logs are the argument, not this paragraph: backend build log · frontend build log · and the coding guide's closing table, the four failures that produced most of these rules — an audit scope assumed as 24 fields when the spec said 9; a password trimmed on create but not on login; a sqlc query that compiled against a column that did not exist; a field left out of a keyed struct literal, writing NULL in silence.

Four decisions

A failed e-signature is not retried, because the retry would falsify an audit trail. Every rejected signature returns 401 and writes a SignatureFailed audit row. 401 is also what an expired token returns, and the client refreshes and retries that one — so the client matches on the response body, not the status. A blind retry would write two failure rows into a regulated record for one thing the user did once. The care runs the other way too: a blank email or password is a 400 caught before the record is even read, and writes nothing — the trail records attempts to sign, not failures to fill in a form, which is what makes a count of failed signatures mean anything. (frontend decision 13)

Authentication is a compile-time property. type authedHandler func(http.ResponseWriter, *http.Request, database.User) — a three-parameter function is not an http.HandlerFunc, so it cannot be registered on the mux at all without the middleware that supplies the user. Forgetting the auth check stops being a thing a reviewer has to catch. (coding guide §10)

A failed signature's audit row is written outside the transaction — with cfg.db, not qtx. The single deliberate violation of the codebase's own rule that everything inside a transaction uses the transaction, because the rollback that reverts the attempted transition would otherwise erase the evidence that the attempt happened. (backend decision #31)

The security matrix says "Approver"; the API compares assigned_approver_id. The specification described a role and the system enforces an identity. Following the matrix literally would have offered two of three approvers a button guaranteed to 403 — the spec was right about intent and imprecise about mechanism, and the gap is recorded rather than silently closed.

The approvals queue — only the records assigned to the signed-in approver

An approver who is not the assigned approver can read the record and cannot act on it

Qualification and acceptance testing

Nine protocols, executed against this stack and passed — not against a development database, because what they test is the integrated system: the SPA, the proxy, the API and PostgreSQL together.

They are split by who executes them, which is the distinction a validated environment actually makes. Operational qualification is technical and run by whoever installed the system, with a terminal and a database session. User acceptance testing is run by a business user, in a browser, with nothing else — because a UAT script that asks its tester to open a terminal is an engineer's test wearing a UAT header, and the person whose acceptance is being sought cannot execute it.

The split is also why the best evidence in the project is recorded at all. The application has no audit-log screen, so one failed signature writing exactly one audit row and not two cannot be seen from the browser by anyone. It is OQ-04 §4, and UAT-04 points at it.

Both use the format computerised system validation uses in a regulated environment: an ID, an objective, prerequisites, numbered steps with an expected result, an actual result and a pass/fail, signed and dated. A protocol is evidence of execution; a step with no recorded actual result is not evidence of anything.

ID Title Result Executed
OQ-01 Installation and startup Pass 2026-09-21
OQ-02 Audit trail and record integrity Pass 2026-09-21
OQ-03 Electronic signature records Pass 2026-09-21
OQ-04 Failed signature recording Pass 2026-09-21
UAT-01 Change control lifecycle, approved at both gates Pass 2026-09-21
UAT-02 Rejection at both gates, and recovery Pass 2026-09-21
UAT-03 Field-level permissions by role and state Pass 2026-09-21
UAT-04 Electronic signature controls Pass 2026-09-21
UAT-05 Administration and cancellation Pass 2026-09-21

All nine were executed by the author. In a section that argues about who executes what, that has to be said plainly: the UAT scripts are written to be run by a business user with nothing but a browser, and they have not yet been run by an independent one.

What is not here

There is no HTTP-level test suite. There are nine unit tests over pure functions, a Postman collection carrying 174 saved responses, and a seeder that drives all eight transitions plus an evidence upload and download through the real API on every clean start. None of those is a test harness, and that gap is the most obvious next piece of work.

Date rules compute in UTC, so between midnight and 04:00 in Abu Dhabi the two business-day rules are a day lenient. Found by running into it, flagged, and deferred with the fix named rather than quietly patched.

There is no CSP, and it cannot simply be added as a proxy header: the policy and the SPA's build output have to agree, which makes it a change to the frontend build rather than a line in a Caddyfile.

Password reset, email notifications, saved searches and approver reassignment are out of scope by decision, recorded before the build began rather than discovered as absences afterwards.

All of it is written down — 103 flagged items across the two registers, each with why it was deferred and what would close it.

Context

A solo project, built against a specification that came first. Its shape — the security matrix per role per state, the e-signature rules, the insistence that a failed attempt is still a record — comes from administering and validating systems that work this way, in an industry where they have to. That background explains why the project looks like this. It does not stand in for the code, which is what is being judged.


MIT licensed. See LICENSE.

About

Change Control module for a Quality Management System — Go API, Svelte SPA, PostgreSQL. E-signatures on every decision, an audit trail the app cannot amend. Runs with one docker compose command; OQ and UAT protocols executed.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages