<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>TSQL.APP Blog</title>
    <link>https://static.tsql.app/blog/</link>
    <description>Notes on SQL-first application development, the TSQL.APP framework, and building business software directly in the database.</description>
    <language>en</language>
    <lastBuildDate>Thu, 24 Sep 2026 17:05:28 +0000</lastBuildDate>
    <atom:link href="https://static.tsql.app/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>The assistant lives in the database too</title>
      <link>https://static.tsql.app/blog/the-assistant-lives-in-the-database-too/</link>
      <guid isPermaLink="true">https://static.tsql.app/blog/the-assistant-lives-in-the-database-too/</guid>
      <pubDate>Thu, 24 Sep 2026 15:24:00 +0000</pubDate>
      <description>Most AI features bolted onto business software keep a stale copy of your schema and a guardrail written in English. If the application already lives in the database, the assistant can live there too — fenced by SQL Server itself.</description>
      <category>architecture</category>
      <category>ai</category>
      <category>t-sql</category>
      <content:encoded><![CDATA[<p>The usual way to add an assistant to a business application goes something like
this. Export the schema into a vector store. Write a document explaining the
domain to the model. Give it a database connection, then add a paragraph to the
prompt asking it politely never to run an <code>UPDATE</code>. Ship it, and watch the
support queue.</p>
<p>Two things are wrong with that, and they are the same two things wrong with the
frontend-backend split we <a href="/blog/the-database-is-the-application/">wrote about last time</a>.
The truth about the application now exists in two places, and one of them goes
stale the moment someone edits a button. And the only thing standing between the
model and your production data is a sentence written in English.</p>
<p><strong>If the application already lives in the database, the assistant can live there
too — and then its limits are enforced by the database engine instead of by a
paragraph of instructions.</strong></p>
<p>In TSQL.APP that assistant is <code>ask_ai</code>: a set of stored procedures, tables and
views inside the Solution's own project database, one per Solution. Not a service
running beside the app. There is no separate process to deploy, and nothing to
keep in sync.</p>
<h2 id="talking-to-her">Talking to her<a class="heading-anchor" href="#talking-to-her" aria-hidden="true">#</a></h2>
<p>Users reach her from a button in the app. But because she is just objects in a
database, plain T-SQL is a first-class way in:</p>
<pre><code class="language-sql"><div class="code-block"><span class="code-lang">sql</span><button class="copy-btn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code><span class="k">EXEC</span><span class="w"> </span><span class="n">dbo</span><span class="p">.</span><span class="n">sp_ask_ai</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;How many cards does this application have?&#39;</span><span class="p">;</span>
</code></pre></div>
</code></pre>
<p>That is the whole interface — from SSMS, from <code>sqlcmd</code>, from an Agent job. It is
deliberately <em>not</em> a second implementation of the agent: the call posts to the
same Service Broker queue the app posts to, and everything downstream is the same
code a user gets in the chat modal. Same worker, same conversation history, same
tools, same fence. A test path that exercised a private copy would prove nothing.</p>
<p>What she can do from there is read: the catalog, the metadata that describes every
card, field and button, the body of any action script, and the business tables
themselves through <code>SELECT</code>. She can keep lessons about what she learns. And she
can write a button.</p>
<h2 id="the-obvious-objection">The obvious objection<a class="heading-anchor" href="#the-obvious-objection" aria-hidden="true">#</a></h2>
<p>Nobody sane wants a language model holding a connection to their production
database. So it does not have one.</p>
<p>Every query she writes is executed impersonated as a dedicated login with
read-only rights, and <code>DENY</code> on the things she must never see — the API key
settings, the token tables, and the raw conversation table. She reads her own
threads through a view scoped to the caller, so one user's conversation cannot
surface in another's.</p>
<p>The interesting part is what happens when she needs to create something. She owns
exactly one schema, called <code>ai</code>, and has <code>ALTER</code> on nothing else. So when she
issues DDL, SQL Server refuses anything outside that schema <strong>on its own
authority</strong>. There is a string check in front of it as well, but its job is only
to give her a readable error message — if the check and the permission ever
disagree, the permission is right.</p>
<p><strong>The fence is ownership, not parsing.</strong> That distinction matters, because a fence
made of string inspection is a fence you have to be cleverer than the caller to
maintain, forever. Ownership also has a useful second effect: it deliberately
breaks ownership chaining into <code>dbo</code>, so a view she creates cannot read anything
she could not have read directly.</p>
<p>Writing a button is fenced differently again, because an action script runs with
the application's own rights — far more than hers. So that path is: refuse
anything protected, snapshot the current version, write, compile-check the result,
and <strong>automatically put the snapshot back if it does not compile</strong>. Every version
is kept and restoring one is a tool she has. A failed write costs nothing and
returns SQL Server's own error message, phrased as an instruction, and she is
expected to fix it herself on the same turn.</p>
<p>And a button she wrote does nothing at all until a human presses it. Compiling is
not the same as being correct, and we do not pretend otherwise.</p>
<h2 id="the-part-that-surprises-people">The part that surprises people<a class="heading-anchor" href="#the-part-that-surprises-people" aria-hidden="true">#</a></h2>
<p>Here is the mechanism most people do not guess, and it is the one that decides
whether an assistant like this is still useful in a year.</p>
<p><strong>Structure is read live. Meaning is stored.</strong></p>
<p>Nothing about <em>which cards, buttons and fields exist</em> is ever copied into her
memory. The tool that lets her explore an application reads the metadata tables
straight off the live database, every time she asks. Same for object definitions,
which come back with their immediate callers and callees attached, derived from
the catalog at the moment of reading.</p>
<p>That is a deliberate refusal of the standard pattern. A stored copy of a schema is
wrong as soon as someone adds a column, and it fails silently — the model answers
confidently from the copy. Worse, a &quot;mind map&quot; of a database with a node per table
and an edge per foreign key is not a mind map at all. It is a second, worse copy of
something the catalog already holds perfectly.</p>
<p>So what does get stored is only the part no catalog can produce: what a screen is
<em>for</em>, what this particular business calls a concept, which button actually does
the job someone described in their own words. Her graph is meaning, and the
procedure that writes relationships into it will actively <strong>refuse</strong> an edge the
catalog could have derived itself.</p>
<p>The consequence is that her knowledge of your application cannot go stale, because
she does not have any. She has a way of looking, and a store of what looking has
taught her. Rename a column and she is not wrong — she is simply looking at the
new name.</p>
<p>The same honesty applies to the limits of what the catalog knows. Action scripts
are dynamic SQL, which dependency tracking cannot see, so her tooling reports
&quot;nothing static references this&quot; rather than &quot;nothing uses this&quot;. A tool that
overstates its own certainty is how you get a confident, wrong answer.</p>
<h2 id="why-build-it-this-way">Why build it this way<a class="heading-anchor" href="#why-build-it-this-way" aria-hidden="true">#</a></h2>
<p>The design test we apply to every change is whether it lives in the structure or
in the wording. Ownership, permissions, and what a tool deterministically returns
hold no matter which model arrives next year. Tool descriptions and prompt phrasing
are persuasion — useful, but the next model may simply ignore them.</p>
<p>Which sets the goal: make the harness good enough that even a modest model gives
excellent answers. When results depend heavily on which model is behind it, that is
a gap in the harness, not a problem with the model.</p>
<p>There is <a href="/documentation">documentation</a> and a <a href="/docs">programming reference</a>
online, and you can always <a href="/#contact">book a demo</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The database is the application</title>
      <link>https://static.tsql.app/blog/the-database-is-the-application/</link>
      <guid isPermaLink="true">https://static.tsql.app/blog/the-database-is-the-application/</guid>
      <pubDate>Thu, 24 Sep 2026 10:49:00 +0000</pubDate>
      <description>TSQL.APP puts the whole application inside SQL Server — UI, business logic, permissions and all. Here is what that actually means, and why the frontend-backend divide turns out to be optional.</description>
      <category>architecture</category>
      <category>t-sql</category>
      <content:encoded><![CDATA[<p>Most business software spends its life moving data out of a database and then
putting it back. A query becomes a DTO, the DTO becomes JSON, the JSON becomes a
component, the component becomes a form, and the form becomes an <code>UPDATE</code> — with
validation rules written twice on the way, once in the browser and once on the
server, and a good chance they disagree.</p>
<p>TSQL.APP starts from a different premise: <strong>the data already lives in a database
that can hold logic, enforce permissions and run transactions. Build the
application there.</strong></p>
<h2 id="what-that-looks-like">What that looks like<a class="heading-anchor" href="#what-that-looks-like" aria-hidden="true">#</a></h2>
<p>Here is a complete, working Hello World — a title, an input, a button, validation
and feedback. All of it is T-SQL:</p>
<pre><code class="language-sql"><div class="code-block"><span class="code-lang">sql</span><button class="copy-btn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code><span class="k">DECLARE</span><span class="w"> </span><span class="o">@</span><span class="n">Name</span><span class="w"> </span><span class="n">NVARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">);</span>
<span class="k">DECLARE</span><span class="w"> </span><span class="o">@</span><span class="n">GreetButton</span><span class="w"> </span><span class="n">NVARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">);</span>

<span class="k">EXEC</span><span class="w"> </span><span class="n">sp_api_modal_get_value</span><span class="w"> </span><span class="o">@</span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;@Name&#39;</span><span class="p">,</span><span class="w">        </span><span class="o">@</span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">@</span><span class="n">Name</span><span class="w">       </span><span class="k">OUT</span><span class="p">;</span>
<span class="k">EXEC</span><span class="w"> </span><span class="n">sp_api_modal_get_value</span><span class="w"> </span><span class="o">@</span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;@GreetButton&#39;</span><span class="p">,</span><span class="w"> </span><span class="o">@</span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">@</span><span class="n">GreetButton</span><span class="w"> </span><span class="k">OUT</span><span class="p">;</span>

<span class="k">EXEC</span><span class="w"> </span><span class="n">sp_api_modal_text</span><span class="w">  </span><span class="o">@</span><span class="nb">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;Hello World Example&#39;</span><span class="p">;</span>

<span class="k">EXEC</span><span class="w"> </span><span class="n">sp_api_modal_input</span><span class="w"> </span><span class="o">@</span><span class="n">name</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;@Name&#39;</span><span class="p">,</span>
<span class="w">                        </span><span class="o">@</span><span class="n">value</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="o">@</span><span class="n">Name</span><span class="w"> </span><span class="k">OUT</span><span class="p">,</span>
<span class="w">                        </span><span class="o">@</span><span class="n">placeholder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;Enter your name&#39;</span><span class="p">;</span>

<span class="k">EXEC</span><span class="w"> </span><span class="n">sp_api_modal_button</span><span class="w"> </span><span class="o">@</span><span class="n">name</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;@GreetButton&#39;</span><span class="p">,</span>
<span class="w">                         </span><span class="o">@</span><span class="n">value</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;Greet&#39;</span><span class="p">,</span>
<span class="w">                         </span><span class="o">@</span><span class="n">valueout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">@</span><span class="n">GreetButton</span><span class="w"> </span><span class="k">OUT</span><span class="p">,</span>
<span class="w">                         </span><span class="o">@</span><span class="k">class</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;btn-primary&#39;</span><span class="p">;</span>

<span class="k">IF</span><span class="w"> </span><span class="o">@</span><span class="n">GreetButton</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span>
<span class="k">BEGIN</span>
<span class="w">    </span><span class="k">IF</span><span class="w"> </span><span class="n">LEN</span><span class="p">(</span><span class="k">TRIM</span><span class="p">(</span><span class="k">ISNULL</span><span class="p">(</span><span class="o">@</span><span class="n">Name</span><span class="p">,</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;&#39;</span><span class="p">)))</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span>
<span class="w">        </span><span class="k">EXEC</span><span class="w"> </span><span class="n">sp_api_toast</span><span class="w"> </span><span class="o">@</span><span class="nb">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;Please enter your name&#39;</span><span class="p">,</span><span class="w"> </span><span class="o">@</span><span class="k">class</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;btn-warning&#39;</span><span class="p">;</span>
<span class="w">    </span><span class="k">ELSE</span>
<span class="w">    </span><span class="k">BEGIN</span>
<span class="w">        </span><span class="k">DECLARE</span><span class="w"> </span><span class="o">@</span><span class="n">Greeting</span><span class="w"> </span><span class="n">NVARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">CONCAT</span><span class="p">(</span><span class="n">N</span><span class="s1">&#39;Hello, &#39;</span><span class="p">,</span><span class="w"> </span><span class="o">@</span><span class="n">Name</span><span class="p">,</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;!&#39;</span><span class="p">);</span>
<span class="w">        </span><span class="k">EXEC</span><span class="w"> </span><span class="n">sp_api_toast</span><span class="w"> </span><span class="o">@</span><span class="nb">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">@</span><span class="n">Greeting</span><span class="p">,</span><span class="w"> </span><span class="o">@</span><span class="k">class</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">N</span><span class="s1">&#39;btn-success&#39;</span><span class="p">;</span>
<span class="w">    </span><span class="k">END</span>
<span class="k">END</span>
</code></pre></div>
</code></pre>
<p>There is no HTML here, and no JavaScript. There is no controller, no route, no
view model, no build step. The script declares what it wants on screen and reads
back what the user did. That is the whole interface.</p>
<h2 id="two-databases-and-why-your-data-stays-put">Two databases, and why your data stays put<a class="heading-anchor" href="#two-databases-and-why-your-data-stays-put" aria-hidden="true">#</a></h2>
<p>The obvious objection is that nobody wants a framework rewriting their production
schema. So TSQL.APP does not touch it.</p>
<p>An application is <strong>two</strong> databases. Your existing one — call it <code>app</code> — keeps
your tables exactly as they are. A second one, <code>app_proj</code>, holds the framework:
the <code>sp_api_*</code> procedures and the <code>api_*</code> metadata tables that describe cards,
fields, buttons and permissions. It reaches your data through synonyms.</p>
<p>Business data on one side, presentation and configuration on the other, and a
clean line between them. Point the framework at a database that has been running
for fifteen years and it will render it without asking you to migrate anything.</p>
<h2 id="everything-else-is-a-messenger">Everything else is a messenger<a class="heading-anchor" href="#everything-else-is-a-messenger" aria-hidden="true">#</a></h2>
<p>Around the database sit three small things, and none of them is allowed an
opinion.</p>
<p>The <strong>.NET API</strong> flattens every HTTP request into a single four-column table and
hands it to one stored procedure. It contains no domain knowledge at all — you
can read the whole thing in an afternoon and learn nothing about the business it
serves, which is the point.</p>
<p>The <strong>React client</strong> renders whatever named result sets come back. It does not
know what a sales order is. It knows how to draw a list, a detail view and a
modal, and it is told which to draw.</p>
<p>The <strong>dispatcher</strong> performs the IO that SQL Server cannot do for itself: email,
HTTP fetches, Excel, PDF, SFTP, printing. Work is queued through Service Broker
and picked up asynchronously.</p>
<p>So when someone asks how a feature works, the answer is always a stored procedure
or an action script. Never C#, never JavaScript. There is exactly one place to
look.</p>
<h2 id="the-part-that-surprises-people">The part that surprises people<a class="heading-anchor" href="#the-part-that-surprises-people" aria-hidden="true">#</a></h2>
<p>Here is the mechanism that makes a multi-step wizard work, and it is not what
almost anyone guesses.</p>
<p><strong>An action script has no resume point.</strong> It is re-executed from the top on every
round trip. There are no coroutines, no saved instruction pointer, no server-side
run state, and no step counter anywhere.</p>
<p>The answers the user has given so far travel as a JSON bag, out through the
browser and back again with the next request. Each collector — an input, a
button, a picker — prefers the posted answer over its own default. Before the
click, <code>@GreetButton</code> is <code>NULL</code>; after it, it holds the caption, and the <code>IF</code>
below it fires.</p>
<p>Which means &quot;step 3 of 5&quot; is never stored anywhere. It is <em>emergent</em>: step N is
simply the first question whose name is not yet in the bag.</p>
<p>Once that clicks, a lot of the framework stops looking clever and starts looking
inevitable. It is also why a script must be safe to run repeatedly from the top —
easily the most important thing to internalise before writing a complicated one.</p>
<h2 id="where-this-goes">Where this goes<a class="heading-anchor" href="#where-this-goes" aria-hidden="true">#</a></h2>
<p>This blog is where the details get written down properly: how cards turn metadata
into generated SQL, what the transaction and severity channel actually guarantee,
the sharp edges worth knowing about before you find them yourself, and what we
learn running all of this in production for real customers.</p>
<p>If you would rather see it than read about it, there is
<a href="/documentation">documentation</a> and a <a href="/docs">programming reference</a> online, and
you can always <a href="/#contact">book a demo</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
