<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Vasileios Ntoufoudis</title>
    <description>The latest articles on Forem by Vasileios Ntoufoudis (@ntoufoudis).</description>
    <link>https://forem.com/ntoufoudis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3810362%2F90e73016-66be-4bfd-b826-89fbf9f445db.jpeg</url>
      <title>Forem: Vasileios Ntoufoudis</title>
      <link>https://forem.com/ntoufoudis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ntoufoudis"/>
    <language>en</language>
    <item>
      <title>I Built a Verifiable Audit Log for Laravel</title>
      <dc:creator>Vasileios Ntoufoudis</dc:creator>
      <pubDate>Fri, 06 Mar 2026 18:05:35 +0000</pubDate>
      <link>https://forem.com/ntoufoudis/i-built-a-verifiable-audit-log-for-laravel-1621</link>
      <guid>https://forem.com/ntoufoudis/i-built-a-verifiable-audit-log-for-laravel-1621</guid>
      <description>&lt;p&gt;Most Laravel applications record important events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user logins&lt;/li&gt;
&lt;li&gt;orders created&lt;/li&gt;
&lt;li&gt;payments processed&lt;/li&gt;
&lt;li&gt;settings changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These events are usually stored in an &lt;strong&gt;activity log table&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;id | action | user_id | created_at&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;At first glance, this seems fine.&lt;/p&gt;

&lt;p&gt;But there’s a serious problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Audit Logs Can Be Modified
&lt;/h2&gt;

&lt;p&gt;In most applications, audit logs are just database records.&lt;/p&gt;

&lt;p&gt;That means they can usually be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modified&lt;/li&gt;
&lt;li&gt;deleted&lt;/li&gt;
&lt;li&gt;reordered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If someone with database access changes a record, there’s often &lt;strong&gt;no reliable way to detect it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For many systems this is unacceptable.&lt;/p&gt;

&lt;p&gt;Especially for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;financial systems&lt;/li&gt;
&lt;li&gt;healthcare platforms&lt;/li&gt;
&lt;li&gt;security-sensitive applications&lt;/li&gt;
&lt;li&gt;compliance reporting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These systems need &lt;strong&gt;tamper-detectable audit trails&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea: Turn Logs into a Ledger
&lt;/h2&gt;

&lt;p&gt;Instead of recording events as simple database records, I built a system that records them in a &lt;strong&gt;cryptographically verifiable ledger&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I called it &lt;strong&gt;Chronicle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Chronicle is an &lt;strong&gt;append-only audit ledger for Laravel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each entry is linked to the previous one using a &lt;strong&gt;hash chain&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If any entry is modified or removed, the ledger verification fails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recording an Event
&lt;/h2&gt;

&lt;p&gt;Using Chronicle looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Chronicle\Facades\Chronicle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Chronicle&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order.created'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'amount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'currency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each entry records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;who performed the action&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;what happened&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;what entity was affected&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Entries are immutable once recorded.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hash Chaining
&lt;/h2&gt;

&lt;p&gt;Chronicle protects the ledger using a &lt;strong&gt;hash chain&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each entry references the previous entry:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chain_hash(n) = SHA256(chain_hash(n-1) + payload_hash(n))&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This creates a cryptographic chain across the entire audit history.&lt;/p&gt;

&lt;p&gt;If any entry is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modified&lt;/li&gt;
&lt;li&gt;deleted&lt;/li&gt;
&lt;li&gt;reordered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the chain verification fails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Detecting Tampering
&lt;/h2&gt;

&lt;p&gt;Chronicle includes a verification command:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;php artisan chronicle:verify&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If the ledger was tampered with, verification fails.&lt;/p&gt;

&lt;p&gt;This allows systems to &lt;strong&gt;detect unauthorized changes to audit logs&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exporting a Verifiable Dataset
&lt;/h2&gt;

&lt;p&gt;Chronicle can export the ledger as a &lt;strong&gt;verifiable dataset&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan chronicle:export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exports include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entries.ndjson
manifest.json
signature.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dataset contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a dataset hash&lt;/li&gt;
&lt;li&gt;a digital signature&lt;/li&gt;
&lt;li&gt;ledger boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows the exported audit log to be &lt;strong&gt;verified independently&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Querying the Ledger
&lt;/h2&gt;

&lt;p&gt;Chronicle includes query helpers for retrieving entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Chronicle\Models\Entry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forActor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forSubject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order.created'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cursor pagination&lt;/li&gt;
&lt;li&gt;streaming large ledgers&lt;/li&gt;
&lt;li&gt;indexed queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it practical to use in real systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;Many applications rely on audit logs to answer questions like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happened?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But traditional logs can’t always answer a more important question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we &lt;strong&gt;prove&lt;/strong&gt; what happened?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Chronicle attempts to solve that problem.&lt;/p&gt;

&lt;p&gt;Instead of storing events as editable records, it records &lt;strong&gt;application history as a ledger&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open Source
&lt;/h2&gt;

&lt;p&gt;Chronicle is open source and available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/laravel-chronicle/core" rel="noopener noreferrer"&gt;https://github.com/laravel-chronicle/core&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the idea sounds interesting, feel free to check it out.&lt;/p&gt;

&lt;p&gt;Feedback and contributions are welcome.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>security</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
