<?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: Jean-Marc Strauven</title>
    <description>The latest articles on Forem by Jean-Marc Strauven (@grazulex).</description>
    <link>https://forem.com/grazulex</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%2F3601103%2Fed4af1b9-c6f5-4909-a4f5-4ee30552bb52.png</url>
      <title>Forem: Jean-Marc Strauven</title>
      <link>https://forem.com/grazulex</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/grazulex"/>
    <language>en</language>
    <item>
      <title>How I built nis2you with Laravel 12 + Livewire 3 — a NIS2 compliance SaaS for European SMEs</title>
      <dc:creator>Jean-Marc Strauven</dc:creator>
      <pubDate>Tue, 05 May 2026 19:12:08 +0000</pubDate>
      <link>https://forem.com/grazulex/how-i-built-nis2you-with-laravel-12-livewire-3-a-nis2-compliance-saas-for-european-smes-j38</link>
      <guid>https://forem.com/grazulex/how-i-built-nis2you-with-laravel-12-livewire-3-a-nis2-compliance-saas-for-european-smes-j38</guid>
      <description>&lt;h2&gt;
  
  
  The problem nobody wants to deal with
&lt;/h2&gt;

&lt;p&gt;Late 2024, the EU's NIS2 directive started biting. Thousands of mid-sized European companies that never thought of themselves as "critical infrastructure" suddenly found themselves in scope: manufacturers, logistics providers, MSPs, mid-tier SaaS vendors. The directive itself is broad, the national transpositions vary, but the practical question on the ground is the same everywhere: &lt;strong&gt;where do we even start?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're a 60-person Belgian manufacturer, you don't have a CISO. You probably don't have a dedicated security analyst either. You have an IT manager already wearing three hats, and now they need to deliver a risk assessment, an incident response plan, and a board-level reporting framework — against a deadline that has already passed in some Member States.&lt;/p&gt;

&lt;p&gt;The big consultancies will gladly sell you a six-figure engagement. The big GRC platforms (OneTrust, ServiceNow IRM and friends) are priced for enterprise procurement teams, not SMEs. Between "expensive consultant" and "ignore the problem and hope you don't get audited," there is a gap. That gap is where I've been building for the last few months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Laravel 12 + Livewire 3 (and what I rejected)
&lt;/h2&gt;

&lt;p&gt;I'm a long-time Laravel developer. I maintain a few open-source packages under the Grazulex namespace, so the framework choice was never really up for debate — but I want to walk through &lt;em&gt;why&lt;/em&gt; Laravel 12 + Livewire 3 holds up specifically for a regulated B2B SaaS with a single founder-developer.&lt;/p&gt;

&lt;p&gt;The stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 12&lt;/strong&gt; — backend. Eloquent, queues, policies, broadcasting, the whole thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Livewire 3&lt;/strong&gt; — server-rendered reactivity. No SPA, no separate API, no client-side state to keep in sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filament 3&lt;/strong&gt; — internal admin panel. Tenants, audits, support tickets, billing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — styling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL 8&lt;/strong&gt; — primary store. Postgres would have been fine; hosting defaults pushed MySQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel Horizon&lt;/strong&gt; — dashboard for the async work (PDF generation, scheduled assessments, email).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two stacks I seriously considered and dropped:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Laravel + Inertia + Vue.&lt;/strong&gt; I like Inertia, but the moment you cross into Vue components you need build tooling, type definitions, and a parallel mental model. For a solo dev shipping fast, that's a tax with no matching benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js + Supabase.&lt;/strong&gt; Fashionable, fast to prototype, painful for compliance work. Row-level security in Postgres is great — until you need a deep audit trail with field-level diff history and predictable, framework-native authorization. I wanted Laravel's policy + Eloquent observers ecosystem.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Livewire 3 in 2026 is genuinely good. The &lt;code&gt;wire:model&lt;/code&gt; improvements, morph-based DOM diffing, and Alpine.js integration mean I can build a multi-step risk assessment wizard with conditional fields, validation, and live progress indicators — without writing a single line of dedicated JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four technical decisions worth talking about
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. State machines, hardcoded — not configurable
&lt;/h3&gt;

&lt;p&gt;Every compliance object in nis2you (assessments, incidents, audits, action plans) goes through a defined lifecycle: &lt;code&gt;draft → in_review → validated → published → archived&lt;/code&gt;. Early on, I was tempted to make this configurable per tenant. &lt;em&gt;"Some clients might want a different workflow."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I didn't, and I'm glad I didn't. Configurable workflows mean a workflow engine, which means YAML files, which means a UI to edit them, which means support tickets when someone breaks the YAML. For a single-developer SaaS, hardcoded states with transition guards in plain PHP are dramatically simpler:&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="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AssessmentState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;DRAFT&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;IN_REVIEW&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'in_review'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;VALIDATED&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'validated'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;PUBLISHED&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;ARCHIVED&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'archived'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;canTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DRAFT&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IN_REVIEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ARCHIVED&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IN_REVIEW&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DRAFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;VALIDATED&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;VALIDATED&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PUBLISHED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IN_REVIEW&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PUBLISHED&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$to&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ARCHIVED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ARCHIVED&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt;          &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with an Eloquent observer that throws on illegal transitions, this is ~80 lines of code replacing what would be a 2,000-line workflow engine. If a tenant ever genuinely needs a different workflow, that's a one-on-one conversation, not a config screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. PDF generation via DomPDF, queued through Horizon
&lt;/h3&gt;

&lt;p&gt;NIS2 deliverables are documents. Risk assessments, incident reports, supplier inventories — at some point, somebody needs a signed PDF. PDF generation in PHP is slow and memory-heavy. Doing it inline kills the request.&lt;/p&gt;

&lt;p&gt;The setup: a Blade template renders the document, DomPDF turns it into a PDF, the whole thing runs as a queued job, and Livewire listens for completion:&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="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateAssessmentPdf&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;Assessment&lt;/span&gt; &lt;span class="nv"&gt;$assessment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pdf.assessment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'assessment'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;assessment&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;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pdf&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;loadHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$html&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;setPaper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a4'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;disk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'s3'&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;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;"tenants/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;assessment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/assessments/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;assessment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$pdf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;assessment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'pdf_generated_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

        &lt;span class="nc"&gt;AssessmentPdfReady&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;assessment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Horizon gives me retries, failure tracking, and a real-time dashboard. The Livewire component listens for &lt;code&gt;AssessmentPdfReady&lt;/code&gt; and updates the UI without a refresh. Users see &lt;em&gt;"generating..."&lt;/em&gt; → &lt;em&gt;"ready, click to download"&lt;/em&gt; without ever holding a long-running HTTP request open.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Audit trail via Spatie ActivityLog — extended for field-level diffs
&lt;/h3&gt;

&lt;p&gt;The default &lt;code&gt;spatie/laravel-activitylog&lt;/code&gt; package logs creates / updates / deletes per model. For NIS2, that's not enough. Auditors want to know &lt;em&gt;which field&lt;/em&gt; changed, &lt;em&gt;who&lt;/em&gt; changed it, and &lt;em&gt;what the previous value was&lt;/em&gt;. Two-year retention is the practical baseline.&lt;/p&gt;

&lt;p&gt;I extended the default with a small &lt;code&gt;Auditable&lt;/code&gt; trait, applied to every auditable model:&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="kd"&gt;trait&lt;/span&gt; &lt;span class="nc"&gt;Auditable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getActivitylogOptions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;LogOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;LogOptions&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;defaults&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;logFillable&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;logOnlyDirty&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;dontSubmitEmptyLogs&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;setDescriptionForEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMorphClass&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;tapActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Activity&lt;/span&gt; &lt;span class="nv"&gt;$activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$activity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$activity&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'tenant_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'ip'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;request&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;ip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'user_id'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;auth&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;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The activity log lives in a dedicated table partitioned monthly. Partitions older than twelve months get archived to S3. This was the single highest-ROI piece of plumbing in the whole app — auditors love it, and it has already answered &lt;em&gt;"did the user actually approve this?"&lt;/em&gt; three times in beta.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Multi-tenancy: column-scoped now, schema-per-tenant on the V1.5 roadmap
&lt;/h3&gt;

&lt;p&gt;Every tenant-bound model has a &lt;code&gt;tenant_id&lt;/code&gt;. Global scopes apply automatically based on the authenticated session. It's the simplest possible model, and it works perfectly at current scale.&lt;/p&gt;

&lt;p&gt;I deliberately did &lt;em&gt;not&lt;/em&gt; ship schema-per-tenant in V1. The argument for it (hard data isolation, easier per-customer backups) is real but premature for a pre-revenue product. When a regulated customer eventually pushes for true schema isolation, V1.5 will introduce it via &lt;code&gt;stancl/tenancy&lt;/code&gt;. Until then, column scoping plus exhaustive Pest tests on every policy is enough — and it keeps the operational story simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three honest lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Filament saved me at least four weeks.&lt;/strong&gt; I almost built a custom admin panel "because Filament might be limiting." It isn't. The internal panel for managing tenants, support tickets, and audit reviews took a weekend with Filament. A custom build would have eaten the entire month of February.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Livewire's gotchas are still real.&lt;/strong&gt; Specifically: large arrays bound through &lt;code&gt;wire:model&lt;/code&gt; cause noticeable hydration cost. I learned to flatten complex nested structures and to lift heavy state to the server through explicit method calls rather than blanket two-way binding. The "drop-in reactivity" promise has a ceiling — and you need to know where it is &lt;em&gt;before&lt;/em&gt; a user complains the wizard feels sluggish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Compliance UX is its own discipline.&lt;/strong&gt; Devs love forms; compliance officers tolerate them. The single biggest UX investment was a "guided assessment" mode that walks the user question-by-question, with context, examples, and an explicit &lt;em&gt;skip for now&lt;/em&gt; button. Completion rate tripled the week I shipped it. The lesson: in regulated B2B, the form is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;nis2you is live in private beta with a handful of Belgian SMEs and consultants. The stack — Laravel 12, Livewire 3, Filament, Horizon — is unglamorous, productive, and cheap to operate. If you're shipping a regulated B2B SaaS as a solo developer, I cannot recommend it strongly enough.&lt;/p&gt;

&lt;p&gt;Genuinely curious what stack you'd reach for in this domain — drop a comment with the trade-offs you'd weigh differently. And if NIS2 happens to be on your customers' radar, &lt;a href="https://nis2you.com" rel="noopener noreferrer"&gt;nis2you.com&lt;/a&gt; is where the work lives.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Jean-Marc Strauven — Belgian Laravel dev, maintainer of the &lt;a href="https://github.com/Grazulex" rel="noopener noreferrer"&gt;Grazulex&lt;/a&gt; packages.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>saas</category>
    </item>
    <item>
      <title>I Built a Task Manager for the AI Coding Era (and It's Just Markdown Files)</title>
      <dc:creator>Jean-Marc Strauven</dc:creator>
      <pubDate>Fri, 07 Nov 2025 11:47:59 +0000</pubDate>
      <link>https://forem.com/grazulex/i-built-a-task-manager-for-the-ai-coding-era-and-its-just-markdown-files-69b</link>
      <guid>https://forem.com/grazulex/i-built-a-task-manager-for-the-ai-coding-era-and-its-just-markdown-files-69b</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Task Managers Weren't Built for AI Collaboration
&lt;/h2&gt;

&lt;p&gt;I've been coding professionally for over 15 years, working with everything from Laravel to complex enterprise systems. Recently, like many developers, I've started "vibe coding" with AI assistants like Claude and ChatGPT. It's transformed how I work.&lt;/p&gt;

&lt;p&gt;But here's the thing: &lt;strong&gt;traditional task managers don't understand this new workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When I'm working with an AI assistant, I need separate spaces for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My personal task breakdown and thinking&lt;/li&gt;
&lt;li&gt;The AI's planning and approach&lt;/li&gt;
&lt;li&gt;Documentation that the AI generates&lt;/li&gt;
&lt;li&gt;Self-review notes from the AI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools like Todoist, Trello, or Linear force everything into the same bucket. It becomes messy fast. Plus, they're cloud-dependent, use proprietary formats, and treat AI as an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: BackMark
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://backmark.tech" rel="noopener noreferrer"&gt;BackMark&lt;/a&gt; to solve this exact problem. It's a CLI task manager with one core principle: &lt;strong&gt;your tasks are just Markdown files&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Markdown?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Implement&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;authentication"&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_progress"&lt;/span&gt;
&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high"&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;security"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;## Description&lt;/span&gt;
&lt;span class="s"&gt;Build JWT-based authentication system with refresh tokens.&lt;/span&gt;

&lt;span class="c1"&gt;## AI Plan&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Set up Laravel Sanctum&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Create user migration and model&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Build auth controllers&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Write tests&lt;/span&gt;

&lt;span class="c1"&gt;## AI Notes&lt;/span&gt;
&lt;span class="s"&gt;Using Sanctum instead of Passport for simpler token management.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. A plain &lt;code&gt;.md&lt;/code&gt; file with YAML frontmatter. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edit it in any text editor&lt;/li&gt;
&lt;li&gt;Track it with Git&lt;/li&gt;
&lt;li&gt;Read it in 50 years&lt;/li&gt;
&lt;li&gt;Own it forever (no vendor lock-in)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI-First Design
&lt;/h3&gt;

&lt;p&gt;BackMark treats AI as a &lt;strong&gt;first-class team member&lt;/strong&gt; with four dedicated spaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ai_plan&lt;/code&gt; - Where your AI assistant outlines its approach&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ai_notes&lt;/code&gt; - Working notes and decision rationale&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ai_documentation&lt;/code&gt; - Generated docs and explanations
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ai_review&lt;/code&gt; - Self-review and validation checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps human thinking and AI planning beautifully separated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blazing Fast Performance
&lt;/h3&gt;

&lt;p&gt;When my task list grew past 1,000 items, I needed speed. I integrated LokiJS for in-memory indexing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;backmark list &lt;span class="nt"&gt;--tag&lt;/span&gt; backend
⚡ Found 247 tasks &lt;span class="k"&gt;in &lt;/span&gt;8ms

&lt;span class="nv"&gt;$ &lt;/span&gt;backmark search &lt;span class="s2"&gt;"authentication"&lt;/span&gt;
⚡ 12 matches &lt;span class="k"&gt;in &lt;/span&gt;6ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;strong&gt;50-250x faster&lt;/strong&gt; than scanning files directly. Sub-10ms queries on large collections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beautiful CLI Experience
&lt;/h3&gt;

&lt;p&gt;I wanted the CLI to feel delightful, not like a chore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;backmark board

┌─────────────────────────────────────────────────┐
│  📋 TODO &lt;span class="o"&gt;(&lt;/span&gt;23&lt;span class="o"&gt;)&lt;/span&gt;    🚧 IN PROGRESS &lt;span class="o"&gt;(&lt;/span&gt;5&lt;span class="o"&gt;)&lt;/span&gt;    ✅ DONE  │
├─────────────────────────────────────────────────┤
│  • Fix login bug  │  • User dashboard  │  • Tests│
│  • Add logging    │  • API endpoints   │  • Docs │
│  • Update deps    │  • Database        │         │
└─────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Colorful, readable output&lt;/li&gt;
&lt;li&gt;Interactive Kanban board&lt;/li&gt;
&lt;li&gt;Fuzzy search&lt;/li&gt;
&lt;li&gt;Smart prompts and autocomplete&lt;/li&gt;
&lt;li&gt;Task complexity estimation&lt;/li&gt;
&lt;li&gt;AI-powered task breakdown&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Philosophy: Privacy &amp;amp; Ownership
&lt;/h2&gt;

&lt;p&gt;BackMark is &lt;strong&gt;100% offline&lt;/strong&gt;. No cloud sync, no accounts, no telemetry. Your data never leaves your machine.&lt;/p&gt;

&lt;p&gt;In an era where every tool wants to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store your data in their cloud&lt;/li&gt;
&lt;li&gt;Charge monthly subscriptions&lt;/li&gt;
&lt;li&gt;Lock you into their ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BackMark is refreshingly simple: local Markdown files you completely own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Usage
&lt;/h2&gt;

&lt;p&gt;Here's my typical workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a task: &lt;code&gt;backmark add "Build payment integration"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Let Claude plan it in the &lt;code&gt;ai_plan&lt;/code&gt; section&lt;/li&gt;
&lt;li&gt;Work through it, updating status as I go&lt;/li&gt;
&lt;li&gt;Have Claude document in &lt;code&gt;ai_documentation&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Review with Claude adding notes to &lt;code&gt;ai_review&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything stays organized, nothing gets lost, and I have a perfect audit trail in Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Highlights
&lt;/h2&gt;

&lt;p&gt;For the developers curious about the stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; - Cross-platform CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LokiJS&lt;/strong&gt; - In-memory indexing for speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commander.js&lt;/strong&gt; - CLI framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chalk&lt;/strong&gt; - Colorful terminal output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100% TypeScript&lt;/strong&gt; - Type safety throughout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is simple: watch Markdown files, index with LokiJS, provide fast queries. No databases, no complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; backmark

&lt;span class="c"&gt;# Initialize in your project&lt;/span&gt;
backmark init

&lt;span class="c"&gt;# Create your first task&lt;/span&gt;
backmark add &lt;span class="s2"&gt;"My first task"&lt;/span&gt;

&lt;span class="c"&gt;# View your board&lt;/span&gt;
backmark board
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the &lt;a href="https://backmark.tech" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; for more.&lt;/p&gt;

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

&lt;p&gt;I'm a Chapter Lead at BNP Paribas Fortis and maintain several open-source Laravel packages. I've tried every productivity tool out there. But when AI became part of my daily coding workflow, nothing fit.&lt;/p&gt;

&lt;p&gt;BackMark is the tool I wish existed when I started vibe coding. It's built by a developer, for developers, with a deep respect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your data ownership&lt;/li&gt;
&lt;li&gt;Your privacy&lt;/li&gt;
&lt;li&gt;Your freedom from vendor lock-in&lt;/li&gt;
&lt;li&gt;Your time (hence the speed)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I'm planning to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better Git integration (auto-commit task changes)&lt;/li&gt;
&lt;li&gt;Time tracking features&lt;/li&gt;
&lt;li&gt;Integration with popular AI coding tools&lt;/li&gt;
&lt;li&gt;Team collaboration (still 100% local, using Git)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're coding with AI assistants and feeling the pain of messy task management, give BackMark a try. It's free, open-source, and built for exactly this use case.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://backmark.tech" rel="noopener noreferrer"&gt;backmark.tech&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/Grazulex/backmark" rel="noopener noreferrer"&gt;Star the repo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product Hunt: &lt;a href="https://producthunt.com" rel="noopener noreferrer"&gt;Support the launch&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What do you think? Are you using AI assistants in your daily coding? How do you manage tasks in this new workflow? Drop a comment below! 👇&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. - If you enjoyed this article, follow me for more insights on Laravel development, AI-assisted coding, and building developer tools.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>cli</category>
      <category>ai</category>
      <category>markdown</category>
    </item>
  </channel>
</rss>
