<?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>DEV Community: Oleksandr Tuta</title>
    <description>The latest articles on DEV Community by Oleksandr Tuta (@oleksandr_tuta).</description>
    <link>https://dev.to/oleksandr_tuta</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1771870%2Fa743077d-bbb3-4a13-870c-54ad937fa086.jpg</url>
      <title>DEV Community: Oleksandr Tuta</title>
      <link>https://dev.to/oleksandr_tuta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oleksandr_tuta"/>
    <language>en</language>
    <item>
      <title>One write path, five front doors: how our Laravel admin got an AI agent and a mobile app without a second backend</title>
      <dc:creator>Oleksandr Tuta</dc:creator>
      <pubDate>Mon, 09 Nov 2026 14:46:47 +0000</pubDate>
      <link>https://dev.to/oleksandr_tuta/one-write-path-five-front-doors-how-our-laravel-admin-got-an-ai-agent-and-a-mobile-app-without-a-3n8</link>
      <guid>https://dev.to/oleksandr_tuta/one-write-path-five-front-doors-how-our-laravel-admin-got-an-ai-agent-and-a-mobile-app-without-a-3n8</guid>
      <description>&lt;p&gt;Every product I have worked on grew the same way.&lt;/p&gt;

&lt;p&gt;First comes the &lt;strong&gt;admin panel&lt;/strong&gt;. Forms, validation, "editors may publish, authors may only write drafts", a slug that must be unique, an email when an order is placed. It works.&lt;/p&gt;

&lt;p&gt;Then the client wants a &lt;strong&gt;mobile app&lt;/strong&gt;. So an API appears next to the admin. The rules get written again in API controllers — mostly the same, slightly different. The coupon that applies to one category works in the admin but not in the app, and nobody notices for a month.&lt;/p&gt;

&lt;p&gt;Then somebody wants &lt;strong&gt;AI&lt;/strong&gt; to do the boring work: fill in missing meta descriptions, redirect the pages that return 404, answer "how did the shop do this week". So a third set of endpoints appears, with a third copy of the rules, and a big question nobody wants to answer: &lt;em&gt;what exactly is this agent allowed to touch?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three backends. Three places where "an author may not publish" is written down. Three places to forget.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://kitloom.dev/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=one-admin&amp;amp;utm_content=intro" rel="noopener noreferrer"&gt;kitloom&lt;/a&gt;, a Laravel CMS with Filament or Nova as the admin, a site in Blade, Vue or React, an MCP server for AI agents and, since this month, a phone app for its shops. We could not afford three backends per feature — kitloom has about seventy modules, each with &lt;strong&gt;two&lt;/strong&gt; admins. So we made one rule early and kept it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The admin is not a UI. The admin is a write path. Every screen, endpoint, tool and app is a thin door in front of it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is about what that rule looks like in code, and what it bought us: an AI agent with 200+ tools that are safe by construction, and a mobile app whose backend took two days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Filament     Nova     REST /api/        MCP agent      Phone app
     │           │            │               │              │
     └───────────┴──── thin doors: forms, controllers, tools ┘
                              │
            writers + permissions + events, with an actor
            (PostWriter, TermWriter, Cart, Checkout, Redirections…)
                              │
                       Eloquent models → database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rules keep the doors thin.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Every write goes through a writer, and the writer gets the acting user
&lt;/h3&gt;

&lt;p&gt;Not &lt;code&gt;Post::create()&lt;/code&gt;. Not &lt;code&gt;$post-&amp;gt;update()&lt;/code&gt; in a controller. A writer:&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="nv"&gt;$post&lt;/span&gt; &lt;span class="o"&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;writer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'title'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Spring collection'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'status'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$actor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PostWriter&lt;/code&gt; makes the slug unique, fills the dates, assigns the terms, cleans the HTML of a user who may not post scripts, fires &lt;code&gt;PostSaved&lt;/code&gt; / &lt;code&gt;PostPublished&lt;/code&gt; — and &lt;strong&gt;refuses&lt;/strong&gt; what &lt;code&gt;$actor&lt;/code&gt; may not do.&lt;/p&gt;

&lt;p&gt;In our code base this exact call is made by the Filament create page, the Nova resource, the REST API's &lt;code&gt;PostsController&lt;/code&gt; and the AI agent's &lt;code&gt;create_content&lt;/code&gt; tool. Four doors, one place where the rules live: a fix to how slugs are made is a fix for all four.&lt;/p&gt;

&lt;p&gt;The shop does the same: the site's checkout form and the phone's &lt;code&gt;POST /checkout&lt;/code&gt; both end in &lt;code&gt;Checkout::placeOrder()&lt;/code&gt;; the site's cart and the phone's cart quote are both priced by the shop's own &lt;code&gt;Cart&lt;/code&gt;. A coupon, a shipping zone, "free delivery from $60" — written once.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Modules plug in through container tags, not through each other
&lt;/h3&gt;

&lt;p&gt;A module does not know the agent exists. It offers a class and tags it:&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="c1"&gt;// RedirectsServiceProvider::register()&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;app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nc"&gt;RedirectsList&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RedirectsCreate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RedirectsUpdate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RedirectsTrash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;NotFoundList&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RedirectFromNotFound&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nc"&gt;AgentTool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the tool itself is a door — about forty lines, almost all of it description:&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="c1"&gt;// Trimmed: module() and schema() left out.&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RedirectsCreate&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;AgentTool&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;private&lt;/span&gt; &lt;span class="kt"&gt;Redirections&lt;/span&gt; &lt;span class="nv"&gt;$redirections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;RedirectData&lt;/span&gt; &lt;span class="nv"&gt;$data&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;name&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'redirects_create'&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;description&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'A new redirect rule, active at once: the paths it catches, where they go, '&lt;/span&gt;
            &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'the code (301 by default). Look with redirects_list first: a rule for the path may exist.'&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;permission&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&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;Redirections&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PERMISSION&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;writes&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="kc"&gt;true&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;array&lt;/span&gt; &lt;span class="nv"&gt;$arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Authenticatable&lt;/span&gt; &lt;span class="nv"&gt;$actor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&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;data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;present&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;redirections&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&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;data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$arguments&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$actor&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 AI module collects everything tagged &lt;code&gt;AgentTool::TAG&lt;/code&gt; and serves it over MCP. Without the AI module, nothing reads the tag and nothing breaks.&lt;/p&gt;

&lt;p&gt;The same trick works for everything that crosses modules: account sections (&lt;code&gt;AccountSection::TAG&lt;/code&gt;), notification topics, webhook topics, columns on every list in the admin, permissions. &lt;code&gt;grep -rn 'const TAG'&lt;/code&gt; is our extension-point catalogue.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Every page already answers as JSON
&lt;/h3&gt;

&lt;p&gt;The site's Vue and React themes are Inertia, so every page has its data as JSON anyway. We made it public: ask any page with &lt;code&gt;Accept: application/json&lt;/code&gt; and you get exactly what the theme draws — a product, a category with its filters, a search, the front page.&lt;/p&gt;

&lt;p&gt;That turned out to be the most important decision for the phone app, and we did not make it for the phone app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it bought us: the AI agent
&lt;/h2&gt;

&lt;p&gt;Because a tool is a door to a writer, and a writer already asks &lt;em&gt;"may this user do this?"&lt;/em&gt;, the agent's security model is not a separate system. It is the user's.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent signs in (OAuth, or a key) &lt;strong&gt;as a user&lt;/strong&gt;. An author's agent writes drafts. A read-only key gets only the reading tools.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;writes(): false&lt;/code&gt; tools are the only ones a read-only key ever sees.&lt;/li&gt;
&lt;li&gt;Nothing is deleted for good: tools trash and restore, permanent deletion stays with a person.&lt;/li&gt;
&lt;li&gt;Every change goes through the writers, so it lands in the activity log under the agent's user — the same log that shows what editors did.&lt;/li&gt;
&lt;li&gt;Settings that decide who gets in (roles, registration, two-factor, agent access itself) are locked for agents, because a page the agent reads could try to talk it into changing them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Today that is &lt;strong&gt;209 tools across the modules&lt;/strong&gt;: content, SEO, redirects and the 404 log, menus, forms and their messages, the shop's products and orders, bookings, translations, backups, Site Health… Most are a few dozen lines, because the logic was already there. You can ask for whole jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Which addresses gave 404 this week? Redirect the ones that have a new page."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Fill in the SEO descriptions of the products that have none — as drafts, I'll check them."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"How did the shop do this month, and which products sold best?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the editor can read in the activity log exactly what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it bought us: the mobile app
&lt;/h2&gt;

&lt;p&gt;This is the part that surprised us.&lt;/p&gt;

&lt;p&gt;The app is one Expo (React Native) project for any kitloom shop: a config file with the site's address, name, colours and icon, and an EAS build under the shop owner's own Apple and Google accounts. Home with banners and shelves, a catalogue with the site's filters, product pages with galleries and reviews, a cart, a three-step checkout, the customer's account, push when the order ships.&lt;/p&gt;

&lt;p&gt;The site's side of it — the &lt;code&gt;mobile-apps&lt;/code&gt; module — is about &lt;strong&gt;2,200 lines of PHP&lt;/strong&gt; in &lt;code&gt;src/&lt;/code&gt;, plus its tests. Here is why it is that small:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The phone needs&lt;/th&gt;
&lt;th&gt;Where it comes from&lt;/th&gt;
&lt;th&gt;New backend code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalogue, product, search, front page&lt;/td&gt;
&lt;td&gt;the site's own pages as JSON&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filters (style, strength, price…)&lt;/td&gt;
&lt;td&gt;the category page's JSON&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart totals, coupons, shipping&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /cart/quote&lt;/code&gt; → the shop's &lt;code&gt;Cart&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a thin controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /checkout&lt;/code&gt; → &lt;code&gt;Checkout::placeOrder()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a thin controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sign-in&lt;/td&gt;
&lt;td&gt;the login form's own &lt;code&gt;PasswordCheck&lt;/code&gt; (the same locks against guessing) + an API token&lt;/td&gt;
&lt;td&gt;extracted, not rewritten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Account: orders, addresses, downloads, bookings&lt;/td&gt;
&lt;td&gt;whatever is tagged &lt;code&gt;AccountSection::TAG&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Your order is on its way"&lt;/td&gt;
&lt;td&gt;a listener on the shop's &lt;code&gt;OrderStatusChanged&lt;/code&gt; → queued push via Expo&lt;/td&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phones list, push settings, install links&lt;/td&gt;
&lt;td&gt;a Filament page + a Nova tool&lt;/td&gt;
&lt;td&gt;new, started from the generator&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The account row is my favourite. The app names no module. When a shop installs the bookings module, its &lt;em&gt;Appointments&lt;/em&gt; section appears on the site's account page &lt;strong&gt;and on the phone&lt;/strong&gt;, with no app release, because the app draws whatever sections the site lists.&lt;/p&gt;

&lt;p&gt;From the approved plan to the module in a tagged release took &lt;strong&gt;two days&lt;/strong&gt;, with the first Android build on a real phone at the end of the second. The admin screens for it exist in both Filament and Nova, because the generator writes both.&lt;/p&gt;

&lt;h2&gt;
  
  
  And the other AI: the one writing the code
&lt;/h2&gt;

&lt;p&gt;There is a second reason the timings above are short, and it is the same architecture seen from the other side.&lt;/p&gt;

&lt;p&gt;We write most of kitloom with an AI coding assistant (Claude Code). The main lesson so far: &lt;strong&gt;an AI assistant copies a pattern very well and invents one badly.&lt;/strong&gt; Ask it to "add a mobile API" to a code base with three ways of doing writes, and it will happily add a fourth.&lt;/p&gt;

&lt;p&gt;So we spent the effort on making every kind of change have exactly one pattern, and on writing the patterns down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; at the repository root.&lt;/strong&gt; Not a style guide — a list of &lt;em&gt;"to add X, do Y"&lt;/em&gt;: a module, a permission, a REST namespace, an agent tool, an account section, a notification. Every rule says where the example lives in the code. The assistant reads it before the first edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generators instead of copying.&lt;/strong&gt; &lt;code&gt;php artisan kitloom:make-module Reviews&lt;/code&gt; writes the domain package and both admin packages — provider, migration, model registration, permissions, settings page, agent tools, tests — and the result passes every check as generated. The assistant starts from correct code instead of from a blank file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks that say no.&lt;/strong&gt; &lt;code&gt;tools/check.sh&lt;/code&gt; runs package-dependency checks (a module may use another's classes only if it requires it), PSR-4, Pint, PHPStan with a baseline that may not grow, and the tests of both admins. &lt;code&gt;--affected&lt;/code&gt; runs only what a change can reach, so the loop stays fast; the full run is the gate before a commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests live in the package.&lt;/strong&gt; The assistant changes &lt;code&gt;mobile-apps&lt;/code&gt; and runs &lt;code&gt;mobile-apps&lt;/code&gt;'s tests, not the whole suite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thin-door architecture and the AI assistant reinforce each other. When the right place for logic is always "the writer", the assistant puts it there. When a new door is always "a class with a tag", the assistant writes the class and the tag. The review is then about &lt;em&gt;what&lt;/em&gt; the code does, not &lt;em&gt;where&lt;/em&gt; it went.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;It is not free, and I'd rather say so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discipline at the doors.&lt;/strong&gt; The moment someone writes &lt;code&gt;$model-&amp;gt;save()&lt;/code&gt; in a controller "just this once", the guarantees are gone. Our checks don't see it; code review has to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writers grow.&lt;/strong&gt; &lt;code&gt;PostWriter&lt;/code&gt; knows a lot. Revisions and HTML cleaning already live in classes of their own, and it is still one of the biggest classes we have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app depends on page JSON.&lt;/strong&gt; Changing what a product page returns is now an API change. We version the app's own endpoints (&lt;code&gt;app/v1&lt;/code&gt;) and the app feature-detects what the site supports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platforms have their own rules.&lt;/strong&gt; Apple wants digital goods sold through its own purchases, so the app does not sell downloads; an account with a second factor is asked to sign in on the site for now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is a reason to go back to three backends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What kitloom is, its modules, themes and editions: &lt;a href="https://kitloom.dev/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=one-admin&amp;amp;utm_content=home" rel="noopener noreferrer"&gt;kitloom.dev&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A sandbox with demo content, one click, no sign-up: &lt;a href="https://filament.kitloom.dev/new?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=one-admin&amp;amp;utm_content=new-filament" rel="noopener noreferrer"&gt;Filament&lt;/a&gt; or &lt;a href="https://nova.kitloom.dev/new?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=one-admin&amp;amp;utm_content=new-nova" rel="noopener noreferrer"&gt;Nova&lt;/a&gt;. Give your Claude or Cursor its address and let it work.&lt;/li&gt;
&lt;li&gt;The shop the app runs against: &lt;a href="https://pantry.kitloom.dev/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=one-admin&amp;amp;utm_content=pantry" rel="noopener noreferrer"&gt;Prairie Hops&lt;/a&gt;, a demo craft-beer store.&lt;/li&gt;
&lt;li&gt;How the agent is wired: &lt;a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=one-admin&amp;amp;utm_content=agent" rel="noopener noreferrer"&gt;Run your site with an AI agent&lt;/a&gt;. How the app is wired: &lt;a href="https://kitloom.dev/blog/mobile-app-for-an-online-store-on-laravel.html?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=one-admin&amp;amp;utm_content=app" rel="noopener noreferrer"&gt;A mobile app for your online store&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have built something similar — one write path behind several clients — I'd like to hear where it broke for you. Ours broke at "just this once".&lt;/p&gt;

</description>
      <category>ai</category>
      <category>laravel</category>
      <category>inertiajs</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
