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.
Sealed entities (AuthIntegrityBase)
Section titled “Sealed entities (AuthIntegrityBase)”Entities extending AuthIntegrityBase carry integrity fields (AuthIntegrityBase.cs):
RecordHash(on the baseGlobalBaseEntity),PreviousHash,SequenceNumber— the hash chain.SecurityMode(defaultGeneral),TpmSignature(hardware seal),SealedAt— the seal.NotarizedStatus/NotarizedHash/NotarizedAt— a second PUF-notarization layer.
Sealed vs. notarized (by tier)
Section titled “Sealed vs. notarized (by tier)”- General tier →
NotarizedStatusstays"None"; the software seal (hash chain) is sufficient. - Standard / Iron tiers → after sealing, a central PUF signs again and
NotarizedStatusbecomesNotarized(via the Iron seal provider).
What this enables
Section titled “What this enables”- 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).
Operational notes
Section titled “Operational notes”- Raw-SQL migrations bypass the interceptor → blank
RecordHash=''in the migration so the seeder’s reseal pass re-seals via the interceptor. jsonbcolumns 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 is | A hash chain + TPM/passkey key that makes a record tamper-evident | A rendered SVG stamp (round stamp / signature) drawn onto a document |
| Org-level | Organization.OrgSealKeyId — the master seal key; gates every mutation (OrgSealGuardMiddleware) | Organization.SealUrl — the corporate seal image |
| Person-level | User.PersonalSealKeyId — first passkey; the member-identity anchor | User.SignatureSpec — the designed personal signature/seal |
| Mutable? | Immutable — SetOrganizationSeal / SetPersonalSeal throw once set | Re-editable anytime |
| Purpose | Integrity + authorization | A 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.
The visual seal designer 🎨 ✅ Built
Section titled “The visual seal designer 🎨 ✅ Built”Company seal — created in Org Settings
Section titled “Company seal — created in Org Settings”Owners/admins design the corporate seal in Org Settings → General → “Create Seal” (OrganizationSettings.General.cs). The designer produces a SealSpec (Components/Shared/CorporateSeal.cs):
- Style —
western(round-double / oval / boxed; arc text + a center icon) orasian(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) ormodernorder. - Font — OFL webfonts (Noto Serif CJK, Gowun Batang, Nanum Brush…) plus the HJ Hanjeon (
hanjeon) seal-script font for Korean; loaded inindex.htmland 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:
kind | What it renders |
|---|---|
signature | The member’s name in a handwriting webfont (Great Vibes, Sacramento…) |
seal | A personal round asian stamp — reuses CorporateSeal with Template=round |
draw | A mouse/touch-drawn signature captured to vector SVG (< 8 KB) |
image | An 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.