---
title: "Canonical format — Sentinel for Laravel | Roundly"
description: "The frozen sentinel.seal/1 format: what gets MAC’d, how every value type is normalized, the seal document and the field tags."
url: https://roundly-consulting.com/open-source/docs/sentinel-for-laravel/canonical-format
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Sentinel for Laravel](https://roundly-consulting.com/open-source/docs/sentinel-for-laravel.md)

# Canonical format

These are the exact bytes Sentinel MACs or signs. The format is frozen: any change ships as sentinel.seal/2 beside version 1, verifiers keep every older format’s code path, and a file of known-answer vectors fails the build on any drift. It matters because a seal can be re-verified outside Sentinel — an auditor’s script, another language — and a seal written on SQLite verifies on PostgreSQL or MySQL.

## Seal what the database holds

Attribute values are always read from the database in raw driver form — when sealing, with SELECT … FOR UPDATE inside the sealing transaction, after the host’s write; when verifying, with a fresh query or the retrieved model’s raw original attributes. Database defaults, triggers and generated columns are therefore covered, and write and read-back are identical by construction: jsonb re-ordering, numeric padding, SQLite REAL affinity, boolean representation and datetime rounding never cause a false alarm.

## Normalisation per tag

Every rule rejects with CanonicalizationException rather than guessing. Null keeps the declared tag, so null ≠ '' ≠ '0' ≠ 0 ≠ false:

| Tag | Canonical value | Rejects |
| --- | --- | --- |
| `str` | The exact bytes; must be valid UTF-8. | invalid UTF-8 (invalid\_utf8) — declare binary() |
| `int` | Decimal without + or leading zeros; -0 → 0; arbitrary length kept exact. | anything else (not\_integer) |
| `dec:N` | Sign + integer part + . + exactly N digits; a float becomes its shortest round-trip digits, never an exponent; -0.00 → 0.00. | more than N non-zero fractional digits (not\_representable), NaN/INF, exponent strings |
| `flt:N` | Rounded half-even to N, .-separated, no thousands separator (lossy by declaration). | NaN/INF |
| `bool` | '1' / '0' | anything but bool, 0/1, '0'/'1', t or true (any case) — f and false too: Laravel’s boolean cast reads them as true, so they never stand for a sealed false (not\_boolean) |
| `dt` | A zone-less value is taken as written (Y-m-d\\TH:i:s.uuuuuu); an offset-bearing value is converted to UTC (…\\Z). The two forms never coincide. | unparseable (invalid\_datetime) |
| `date` | Y-m-d for a date or midnight; any other time is kept, in the dt form. | invalid\_date |
| `json` | JCS of the decoded value (big integers kept as strings, depth ≤ 64). | invalid JSON (invalid\_json), depth > 64 |
| `bin` | base64url | — |
| `auto` | int → int, bool → bool, string → str, null → null. | float (float\_requires\_declaration) |

## The seal document

```json
{"alg":"hmac-sha256","at":"2026-10-02T18:30:00.000000Z","ctx":"","f":[["a:amount","dec:2","10.50"],["a:paid","bool","1"],["a:status","str","paid"],["c:lines","json","[{\"qty\":1,\"sku\":\"A-1\"}]"]],"id":"42","kid":"default-20261002-k3f9qa","prev":null,"ring":"default","scope":"","seal":"financial","table":"invoices","type":"App\\Models\\Invoice","v":"sentinel.seal/1","ver":"7"}
```

- Members (all strings or null, keys in JCS order): alg, at (sealed\_at in UTC with microseconds), ctx (sentinel.context), f (field tuples sorted by name), id, kid, ring, prev (base64url SHA-256 of the previous seal’s MAC, or null), scope, seal, table (unprefixed), type (the morph class), v = sentinel.seal/1 and ver.
- The MAC input is the UTF-8 bytes of that JCS text — RFC 8785, with exact integers instead of rounding beyond 2⁵³.
- Domain separation binds row, model, table, seal, tenant scope, app context, version and chain; the other documents use their own v (sentinel.seal-attributes/1, sentinel.ledger/1, sentinel.checkpoint/1, sentinel.field/1, sentinel.anchor/1), and HKDF info strings differ per purpose.
- Not bound: the connection name, which is rename-fragile — use scope() for database-per-tenant setups.

## Field tags

Each field tag is the first 16 bytes of an HMAC-SHA-256 over the field’s identity and value, base64url-encoded and stored as field\_tags. They are produced only for HMAC keys with field tags on; asymmetric seals store null. On a failed MAC the changed attributes are the names whose recomputed tag differs. Tampering with tags only misleads the diagnostics — no decision is ever taken from them: whether only computed values drifted is proven by a separate attribute MAC.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/sentinel-for-laravel.md)
