Skip to content
Skip to main content
Novel Systems home
Engineering evidence

Don’t take our word for how this is built

Every table on this page is generated from the repository when the site is compiled — the migration files, the npm scripts, the committed Lighthouse reports, the commit this deployment came from. Nothing below is a list somebody maintains by hand. Delete a check and its row disappears. That is the only kind of claim about engineering practice worth publishing.

The platform these artifacts are generated from is Novel Systems, CPQ and field service management software for specialty trade contractors.

What you are looking at right now

These values are injected by the build platform from the commit that triggered the deployment. They are not editable into agreement with anything else on this page, which is the reason to publish them: a commit hash beside a build time is a check on every other number here.

Commit
9f3030d
Branch
main
Environment
production
Compiled
2026-08-19 05:44 UTC

Commit message: docs: record Search Console Domain property verification and the load-bearing apex TXT record

The 25 checks that gate a build

Read from the verify script in package.json, in the order they run. Each one’s purpose is taken from the first paragraph of its own header comment, so a guard whose job changes updates this page by being edited. The same chain runs as prebuild: there is no way to produce a deployment that skipped it.

  1. 01

    typecheck

    The TypeScript compiler over the whole project, with no emit.

  2. 02

    verify:pricing

    Proves the numbers on /pricing rather than trusting them.

  3. 03

    check:margin

    Fails the build if the two pricing engines disagree about the margin floor.

  4. 04

    check:cutlist

    Fails the build if the two pricing engines disagree about the cut-size allowances.

  5. 05

    check:cpq-parity

    Holds the browser engine against fixtures/cut-list-parity.json.

  6. 06

    check:client

    Fails the build if any page or component constructs a Supabase client in its module body.

  7. 07

    check:secrets

    Two rules about every module in this repo that reads a credential from the environment, enforced before every build.

  8. 08

    check:health-honesty

    Fails when /api/health can report health it did not measure.

  9. 09

    check:proof

    Fails the build if lib/customer-proof.ts publishes a customer quotation whose permission artifact is not committed to this repository.

  10. 10

    check:claims

    Fails the build when a published claim is stated as a literal in a file that does not own it, or when a claim this project has already retired comes back.

  11. 11

    check:corrections

    Fails the build when the corrections record points at something that is not there.

  12. 12

    check:engineering

    Fails the build when /security#engineering describes engineering practice this repository no longer follows.

  13. 13

    check:hosts

    Fails when next.config.mjs and lib/constants.ts disagree about the domain.

  14. 14

    check:api-reference

    Fails when the marketing surface documents an API the server does not serve.

  15. 15

    check:diagram

    Fails the build if any docs/<name>.png was rendered from a different docs/<name>.mmd than the one currently checked in, if any diagram source states a perishable status, or if any published copy under public/ has drifted from the file in docs/.

  16. 16

    check:logo

    Generates public/logo.png — the organization logo that Google reads out of the Organization JSON-LD on every page, and the only raster form of the brand mark this repository ships.

  17. 17

    check:schema

    Concatenates the real migrations into public/schema.sql, and fails the build if the published file has drifted from them.

  18. 18

    check:coverage

    Runs the backend test suite with coverage and publishes the result as public/coverage.json, and fails the build if the published result no longer describes the code that is checked in.

  19. 19

    check:pipeline

    Publishes the CI workflows as fetchable artifacts, and fails the build if the published copies have drifted from .github/workflows.

  20. 20

    check:evidence-tracing

    Fails when a route that reads an artifact off disk can render somewhere the artifact will not be.

  21. 21

    check:nav

    Fails when a destination in the header navigation is not an anchor in the header's markup.

  22. 22

    check:phone

    The published telephone number has to be one number, in two notations, and it has to be a number a caller can actually reach.

  23. 23

    check:canonical

    Fails when a route ships no canonical URL, or ships one that contradicts the sitemap.

  24. 24

    check:links

    Link audit — does every href on this site go where it says it goes?

  25. 25

    check:social

    Does the profile we link to actually belong to us?

The schema, and where the pricing floor actually lives

4 migrations, 15 tables, 4 row-level security policies and 35 check constraints. CI applies all of them to an empty Postgres 16 on every push, then applies them again over the previously deployed set and diffs the two resulting schemas — because a migration that succeeds by both routes and leaves two different schemas behind is the failure that reaches production.

MigrationLinesTablesIndexesPoliciesChecks
0001_init414815015
0002_row_level_security2110021
0003_commercial_schema570516116
0004_integrations3062513

The counts above are a summary of something you can read. Open the full migration history — every create table, every policy and every constraint, in the order Postgres applies them, generated from the migration files rather than written alongside them.

The margin floor is a constraint, not a variable

The most reasonable objection to a 50% manufactured-goods margin floor enforced in a pricing engine is that it is a number in a JavaScript file, and anything that writes to the database directly can ignore it. It cannot. Below is the SQL, extracted from the migration that ships it — a row violating either constraint is rejected by Postgres before any application code is involved.

  • margin_rules_at_or_above_code_floor0003_commercial_schema
    check ("min_margin" >= 0.50 and "min_margin" <= 0.9999)
  • quotes_applied_margin_floor_range0003_commercial_schema
    check ("applied_margin_floor" is null or ("applied_margin_floor" >= 0.50 and "applied_margin_floor" <= 0.9999))

A separate CI job stands up Postgres, applies every migration, and then attacks the isolation from two directions: once against the policies directly over a raw connection, and once through the application’s own query path, because a database that would refuse a cross-tenant read still does nothing if the application never tells it which tenant is asking.

Tests, and the floors they have to clear

The gates are flags on the test runner, not a target in a document: below any of them the command exits non-zero and the pipeline stops. There is no path that prints a number and carries on. What follows is the last run of that command — not the promise, the result.

Coverage badge: 97.96% line coverage on the backend test suite

Generated from the run below, not fetched from a badge service. /coverage.svg

Lines
97.96%

gate 90% — the build fails below it

Branches
87.87%

gate 80% — the build fails below it

Functions
94.4%

gate 90% — the build fails below it

409/409 tests passed across 50 suites in 9.1s, with 0 failing and 0 skipped. Run on 2026-08-06 00:33 UTC by node v22.22.0 --test --experimental-test-coverage, invoked as npm --prefix backend run test:coverage.

Branches sits below lines and functions on purpose. A branch here is usually an error path against somebody else’s API, and driving that to parity means writing mocks that assert the shape of a failure nobody has observed.

Why this is a committed file and not a link to a CI run

The repository is private, so a link to the workflow log would return a 404 for exactly the reader who wanted to open it. What stands in for it is the artifact itself, plus a guard against the artifact drifting from the code it measures: the run is fingerprinted with a SHA-256 over the 66 files of backend source, tests and manifest that produced it. Change any one of them and check:coverage fails until the suite is re-run, so the figures above cannot describe a version of the code that no longer exists.

sha256 48bd6a881dcb7ce9048f3c3edfbe43e0db4962a62fab3729eb5dc36f3772ca10

This does not prove the run was honest — nothing published by the party being assessed can. It proves the numbers on this page came out of a command that a reader can name, run against a tree they can fingerprint, and that none of it has moved since. Fetch the raw artifact — the table below is rendered from those exact bytes.

The runner’s own output, unedited

Everything above this line is a summary. This is not: /test-run.txt is the exact stdout of npm --prefix backend run test:coverage — 2,892 lines of TAP, 91 kB, one ok line per assertion, ending in the coverage table the figures above are parsed from.

sha256 76d17f633a80abb34346033172f4dcab5191bb5ce4efe27840f1fc8693d7cdc9

That hash is recorded in /coverage.json and re-checked on every build. Editing the log without editing the summary fails the build; editing both to agree means rewriting 91 kB of TAP so that the per-test durations, the counters and the coverage table all still add up, which is more work than running the suite.

Every file on the coverage run, and how far it is covered

48 files, in the order the runner reports them, with the lines each one leaves uncovered. Long run-lists are truncated here; the artifact carries them whole.

Per-file coverage from the backend test run on 2026-08-06 00:33 UTC
FileLinesBranchesFunctionsUncovered
src/app.ts99.3292.3185.7138
src/config/env.ts94.4157.146041-45 49-51 55
src/controllers/auth.controller.ts99.3592.318036
src/controllers/integrations.controller.ts93.7980.2110046-48 55-56 85-91 93-106 116-117 157-160 163
src/controllers/quotes.controller.ts93.0776.3289.66108-113 135 137-152 154-162 166-170 177-178…
src/controllers/work-orders.controller.ts99.6694.4488.8955
src/db/tenant.ts97.4477.781009-11
src/integrations/connection-store.ts100100100—
src/integrations/crypto.ts97.758877.7813-14 17-18
src/integrations/fan-out.ts87.83907520-33
src/integrations/http.ts10097.37100—
src/integrations/oauth-state.ts98.1595.2410038-39
src/integrations/quickbooks/adapter.ts96.9986.36100161-162 168-170 174-179
src/integrations/quickbooks/oauth.ts10071.43100—
src/integrations/registry.ts90.11907511 28-35
src/integrations/salesforce/adapter.ts98.8685.71100104 112 132
src/integrations/salesforce/oauth.ts10063.16100—
src/integrations/stripe/adapter.ts99.7192.510078
src/integrations/stripe/checkout.ts99.7283.7894.7421
src/integrations/stripe/invoice.ts10091.67100—
src/integrations/stripe/signature.ts10089.66100—
src/integrations/sync-store.ts100100100—
src/integrations/tax.ts96.256066.678 12-13
src/integrations/types.ts100100100—
src/lib/build-info.ts10075100—
src/lib/errors.ts100100100—
src/lib/jwt.ts97.1787.510031-33
src/lib/logger.ts10050100—
src/lib/password.ts10083.33100—
src/middlewares/error-handler.ts10093.1100—
src/middlewares/require-auth.ts97.9694.1210029-30
src/middlewares/require-role.ts100100100—
src/middlewares/validate.ts100100100—
src/models/sku.repository.ts93.8190.9183.3323-28
src/routes/auth.routes.ts10080100—
src/routes/index.ts98.9292.3187.536 53
src/routes/integrations.routes.ts100100100—
src/routes/manifest.ts10096.97100—
src/routes/quotes.routes.ts100100100—
src/routes/work-orders.routes.ts100100100—
src/schemas/auth.schema.ts100100100—
src/schemas/common.schema.ts100100100—
src/schemas/integrations.schema.ts100100100—
src/schemas/quotes.schema.ts100100100—
src/schemas/work-orders.schema.ts100100100—
src/services/cpq-engine.ts99.2793.1810044-47
src/services/money.ts97.7387.585.7111-12
src/services/webhook-dispatcher.ts98.6389.4195.6529-31 98-101
All files97.9687.8794.4against gates 90/80/90

Test files on the coverage run

Read from the coverage command itself rather than a list kept beside it, so a test file that is added and never wired in does not appear here.

  • cpq-engine.test.ts
  • error-handler.test.ts
  • webhook-dispatcher.test.ts
  • route-manifest.test.ts
  • api-integration.test.ts
  • integrations.test.ts
  • adapters.test.ts

Test files that are not on the coverage run

  • rls-policies.test.mjs
  • tenant-isolation.test.ts

These need a live Postgres, so they run in a dedicated CI job rather than on the coverage command. They are named here rather than omitted because a test file that exists and is never run is worse than no test file — it reads as coverage in a directory listing and provides none. One of these sat in this repository in exactly that state until it was found and wired into CI.

What runs on every push

5 jobs across 2 workflows, 40 steps in total, read out of .github/workflows rather than described. The workflows themselves are served verbatim at /pipeline.yml — comments included, because the comments are where the reasoning is.

Why there is no status badge here

The repository is private, so a workflow badge renders “not found” and an Actions run log returns 404 to anyone without a seat. Publishing the pipeline definition is what remains available to us: it states what runs, on what trigger, against what services, and with what thresholds — all checkable — instead of asserting a conclusion nobody can open.

A run history is deliberately not published. Run conclusions live behind an authenticated API, and a file in this repository claiming a particular run passed would be a hand-entered assertion about a system you cannot check — which is the defect this whole page exists to remove. What is published instead is one real execution: /coverage.json records the test command’s achieved figures, when it ran, and a SHA-256 of the backend source tree it ran against, so a stale number fails the build rather than ageing quietly. Run logs are available in the security package on request.

CI

.github/workflows/ci.yml

The gate every change passes before it reaches a deployment. Runs on every push to main and on every pull request.

  • Every push to main
  • Every pull request
  • Manual dispatch
  1. Frontend — typecheck, guards, build

    15 steps · ubuntu-latest

    actions/checkout@v5 · actions/setup-node@v5 · Install · Typecheck · Pricing and sitemap audit · Margin policy · Cut-list parity · CPQ parity against the shared fixture · Client construction · Health endpoint measures what it reports · Proof copy · Host configuration · API reference against the generated spec · Internal links · Build

  2. API — typecheck, schema, tests, coverage

    8 steps · ubuntu-latest

    actions/checkout@v5 · actions/setup-node@v5 · Install · Validate the Prisma schema · Typecheck · OpenAPI spec matches the schemas · Tests and coverage · Build

  3. API — tenant isolation against Postgres

    5 steps · ubuntu-latest

    actions/checkout@v5 · actions/setup-node@v5 · Install · Row-level security policies · Tenant isolation through the application path

  4. API — migrations apply forward and converge

    9 steps · ubuntu-latest

    actions/checkout@v5 · actions/setup-node@v5 · Install · Create the two databases · Fresh — apply every migration to an empty database · Upgrade — apply the previously deployed migrations · Upgrade — apply the new migration on top · Compare the two schemas · Confirm the migration history is fully applied

sha256 1d89ac9390295420… — the same bytes served at /pipeline.yml

Deployed API contract

.github/workflows/deployed-api.yml

A daily check that the running API still serves every path the published OpenAPI contract declares. Scheduled, not triggered by a push, because the API deploys from a different repository.

  • Daily on cron 0 8 * * * (UTC)
  • Manual dispatch
  1. Every declared operation is routed

    3 steps · ubuntu-latest

    actions/checkout@v4 · actions/setup-node@v4 · Check every declared operation against the live host

sha256 4d4b32e9bbb10e46… — the same bytes served at /pipeline.yml

Lighthouse, against the deployed site

Parsed from the committed report files, including the URL each one actually requested and the time it ran. A perfect score with no stated date and no stated URL is a screenshot, not a measurement.

RoutePerformanceAccessibilityBest practicesSEORun
contacthttps://www.novelsystems.ca/contact991001001002026-08-03
homehttps://www.novelsystems.ca/941001001002026-08-03
platform-cpqhttps://www.novelsystems.ca/platform/cpq971001001002026-08-03
pricinghttps://www.novelsystems.ca/pricing991001001002026-08-03
sandboxhttps://sandbox.novelsystems.ca/1001001001002026-08-03

Accessibility is the one category where a shortfall is a defect rather than a trade-off, and it is the reason these runs exist. Performance varies by route: the homepage ships an interactive pricing engine and pays for it.

How the pieces fit

The request path from a browser through to Postgres, including where the tenant identity is set and which boundary each credential stops at. A build check compares this against the deployment configuration it describes.

Field device — offline store

The technician app is offline-first. Work orders, cut lists, and parts draws are written locally and queued; connectivity is treated as an optimisation, never a precondition.

  • SQLCipher AES-256 page encryption on the local database
  • Key sealed in Secure Enclave / StrongBox, never persisted in plaintext
  • Certificate pinning on the sync channel
  • Remote wipe and per-device revocation from the admin console

Stage cryptography: AES-256 · SQLCipher

The full request-flow diagram

The picture above is a summary. This is the whole path: every middleware in the order it runs, both pricing engines and the fixture that holds them together, the row-level-security boundary, and the one SECURITY DEFINER function that is allowed to answer whose record an inbound webhook belongs to.

Request-flow architecture diagram. Anonymous visitors, authenticated operators, technicians on mobile and integrator systems enter through Cloudflare DNS. The Next.js frontend on Vercel serves marketing pages and the browser CPQ engine, and proxies priced quotes to the Express API with a server-side credential. The API runs helmet, CORS allowlist, raw and JSON body parsers and a 300-per-minute rate limiter in that order, then requireAuth and requireRole before the quotes, work-orders and integrations routers. Every database query passes through withTenant(), which sets a per-transaction tenant identifier, and then through row-level security on Postgres 15 in ca-central-1, where every table carries a per-tenant policy. Approving a quote fires both the webhook dispatcher and the integration fan-out to Stripe, QuickBooks Online and Salesforce. Inbound Stripe webhooks are authenticated by signature and resolved to a tenant through a single SECURITY DEFINER function.
Clients through Cloudflare into two Vercel projects, every query crossing the row-level-security boundary inside a transaction that has declared its tenant, and the three system-of-record providers that quote.approved fans out to. 38 nodes and 54 edges, rendered from architecture.mmd — the source is published beside it, so the picture is checkable rather than something you take our word for.

What the diagram does not state is which of the three system-of-record providers can transact today. That changes when a credential is set on a deployment, and a diagram re-rendered by hand is the wrong carrier for a fact that moves — it was wrong about all three for a day before anyone checked. The live answer is derived per request and published on the integrations page.

Inside the pricing engine

The diagram above stops at the boundary of the CPQ engine. This one is inside it: a single call to calculateQuote(), the margin gate that runs before any price exists, the five component costs and the two different rounding modes they use, the cut list derived from the finished opening alone, and the fixture that pins two engines which are forbidden from importing each other.

CPQ subsystem diagram. Two callers enter two engines that cannot import each other: the marketing sandbox calls lib/cpq-engine.ts, which is bundled to the browser and carries list prices only, and POST /api/v1/quotes/calculate calls the server engine, which is never bundled to a browser. Inside withTenant(), the server engine reads inventory_skus for cost per unit, yield factor and SKU type, and labor_tiers for hourly rate and multiplier. Before any price exists, a margin gate rejects an applied margin above 0.95, and rejects one below the 0.50 floor without an approved override, with a 400 rather than clamping. Each line item is then validated — a 6 inch minimum, a 144 inch width limit, a 180 inch drop limit, 500 units per line, and SKU types checked rather than assumed — and its opening area computed as width times drop divided by 144. Five component costs follow, each rounded half-up to the cent: fabric as opening times yield factor times a 1.15 waste allowance times quantity; hardware and motor as yield factor times quantity; trim as width times yield factor times quantity; and labour as 0.75 hours per unit plus 0.05 hours per square foot plus half an hour when motorised, priced at the tier's hourly rate multiplied by its multiplier — the multiplier scales the rate, never the hours. The line cost is the sum of the already-rounded components, and the line price is that cost divided by one minus the margin, rounded up. Separately, the cut list is derived from the finished width and drop alone: tube is width minus 1.25 inches, hembar width minus 1.00, fabric width minus 1.50, with 4 inches of wrap added to the drop. Quote totals are the sum of the rounded line items, and a final assertion rejects a realised margin below the floor — a case that should be unreachable. The response carries line items, components, cut list, totals, assumptions, and both the margin floor and the applied margin. Persistence runs in one transaction with one declared tenant, and quote.approved fires after commit into the webhook dispatcher and integration fan-out. Along the bottom, a parity fixture that neither engine imports is held against both by check:cutlist-parity, check:cpq-parity, the backend engine test and check:margin-policy.
One call to calculateQuote(): the margin gate before any price exists, the five component costs and the two rounding modes, the cut list derived from the opening alone, and the parity fixture that pins two engines which cannot import each other. 29 nodes and 36 edges, rendered from cpq-subsystem.mmd — the source is published beside it, so the picture is checkable rather than something you take our word for.

The two engines are duplicated on purpose and the duplication is the risk. One is bundled into the browser so the public calculator answers without a network round trip; the other reads rate cards and must never reach a browser, because a rate card is a customer’s commercial position. They cannot import from each other, so what holds them together is the row of checks along the bottom — a fixture neither engine imports, held against both on every build. It pins the answers rather than the constants, because identical constants once produced a 30.1″ opening cut to 29.00″ in the browser and 28.85″ on the server.

The rest of the evidence

This page covers how the software is built. Four other surfaces cover why it was built this way and how it behaves once it is running, and none of them are marketing pages.

Novel Systems publishes this because the alternative is asking a reviewer to trust an assertion about a codebase they cannot see. If something on this page does not match what you find, that is a defect and we want to hear about it.