Skip to content

Seals & integrity

Sealing is Axowl’s core differentiator: consequential records are hash-chained and sealed so that tampering is detectable and decisions are independently verifiable.

Entities extending AuthIntegrityBase carry integrity fields (AuthIntegrityBase.cs):

  • RecordHash (on the base GlobalBaseEntity), PreviousHash, SequenceNumber — the hash chain.
  • SecurityMode (default General), TpmSignature (hardware seal), SealedAt — the seal.
  • NotarizedStatus / NotarizedHash / NotarizedAt — a second PUF-notarization layer.
  • General tier → NotarizedStatus stays "None"; the software seal (hash chain) is sufficient.
  • Standard / Iron tiers → after sealing, a central PUF signs again and NotarizedStatus becomes Notarized (via the Iron seal provider).
  • Tamper detection — a background scanner re-verifies stored hashes; a mismatch raises an integrity violation.
  • Independent verification — hashing is reproducible client-side, so a consumer can verify a record on their own device (the WASM core; see Integrity SDK).
  • Raw-SQL migrations bypass the interceptor → blank RecordHash='' in the migration so the seeder’s reseal pass re-seals via the interceptor.
  • jsonb columns must be canonicalized before hashing to avoid false-positive tamper alerts.

Two kinds of seal — cryptographic vs. visual ✅ Built

Section titled “Two kinds of seal — cryptographic vs. visual ✅ Built”

“Seal” means two different things in Axowl. Keep them distinct:

Cryptographic seal (everything above)Visual seal (the stamp you design)
What it isA hash chain + TPM/passkey key that makes a record tamper-evidentA rendered SVG stamp (round stamp / signature) drawn onto a document
Org-levelOrganization.OrgSealKeyId — the master seal key; gates every mutation (OrgSealGuardMiddleware)Organization.SealUrl — the corporate seal image
Person-levelUser.PersonalSealKeyId — first passkey; the member-identity anchorUser.SignatureSpec — the designed personal signature/seal
Mutable?ImmutableSetOrganizationSeal / SetPersonalSeal throw once setRe-editable anytime
PurposeIntegrity + authorizationA human-recognizable mark on approval documents

The two are independent: a member designs a visual seal that appears on documents, and separately registers a cryptographic personal seal (a passkey) that anchors identity and authorizes actions. The cryptographic side is documented in Seals (reference); the visual designer is below.

Owners/admins design the corporate seal in Org Settings → General → “Create Seal” (OrganizationSettings.General.cs). The designer produces a SealSpec (Components/Shared/CorporateSeal.cs):

  • Stylewestern (round-double / oval / boxed; arc text + a center icon) or asian (round / square seal-script stamp with a per-glyph grid layout).
  • Script & reading order — Traditional / Simplified Chinese, Japanese, Korean; traditional (right-to-left, top-to-bottom) or modern order.
  • Font — OFL webfonts (Noto Serif CJK, Gowun Batang, Nanum Brush…) plus the HJ Hanjeon (hanjeon) seal-script font for Korean; loaded in index.html and applied via @font-face.
  • Ink / text / icon / sliders — hex ink, top/middle/bottom/center text, a center icon (bowtie/star/…), and border-width / ring-gap / letter-gap / glyph-fill sliders.

CorporateSeal.RenderBytesEmbeddedAsync serializes the spec to a 256-px SVG and embeds the fonts it uses (@font-face woff2 base64 — the self-hosted seal-script face whole, Google families as a css2 text= glyph subset), so the stored artifact renders identically anywhere: <img>, approval documents, PDF, email. It is uploaded via POST /api/org/{slug}/settings/seal (OrgLogoEndpoints.cs) to R2 at logos/orgs/{id}/seal-{ticks}.{ext} (only the logos/ prefix is publicly served); the URL is stored on Organization.SealUrl and the change emits OrganizationSettingsUpdatedEvent (Category Seal). A pre-made raster/SVG file may be uploaded instead of using the designer.

Its role: the company seal is the issuer stamp on every approval document. ApprovalDocument fetches the SVG and inlines it (PersonalSignature.InlineSizedSvg) so the page @font-face applies — a raster or CORS-failed fetch falls back to <img>. This is a visual mark only; it is not the OrgSealKeyId master key that gates mutations.

Personal signature / seal — created in Member Detail

Section titled “Personal signature / seal — created in Member Detail”

Each member designs their own mark in Member Detail → “Personal Signature / Seal”self-only (MemberDetail.SignatureDesigner.cs; even Owner/Admin cannot edit another member’s). Four kinds, stored as JSON on User.SignatureSpec:

kindWhat it renders
signatureThe member’s name in a handwriting webfont (Great Vibes, Sacramento…)
sealA personal round asian stamp — reuses CorporateSeal with Template=round
drawA mouse/touch-drawn signature captured to vector SVG (< 8 KB)
imageAn uploaded PNG/JPEG/WebP/SVG (R2 logos/users/{uid}/signature.{ext})

Saved via PUT /api/org/{slug}/members/{connectedId}/signature {kind, spec} (8 KB cap, JSON-validated) or POST .../signature/image for uploads; both emit org.member.signature_changed (ORG025). The spec is stored once at User level — shared across every org the person belongs to — and surfaced on /me (UserInfo.SignatureSpec). It is rendered everywhere by the single-source Components/Shared/PersonalSignature component (font whitelist, hex-sanitized ink, XSS-stripped SVG); a member with no spec falls back to a name-印 red circle.

Its role: the personal seal is what gets stamped into an approval document’s seal columns — the drafter’s seal auto-stamps the drafter (staff) column on submit, and each approver’s seal fills their column when they stamp. See Teams & reporting → how a request flows.