<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:fh="http://purl.org/syndication/history/1.0"><channel><title>Laju Go | Blog</title><description>High-performance SaaS boilerplate: Go Fiber + Svelte 5 + Inertia.js + SQLite + templ. Auth, uploads, migrations, tests, Docker — wired end to end.</description><link>https://laju.dev/</link><language>en</language><fh:complete/><atom:link rel="self" href="https://laju.dev/blog/rss.xml"/><item><title>Why Go — not Node</title><link>https://laju.dev/blog/why-go/</link><guid isPermaLink="true">https://laju.dev/blog/why-go/</guid><description>Compiled binary, goroutines, fasthttp — why Go is the right choice for SaaS</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every SaaS starter has to pick a backend language. Most pick Node or Bun because that’s where the tutorials are. Laju Go picks Go, and the reason isn’t vibes — it’s a set of engineering tradeoffs that compound over the life of a product.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;one-binary-no-node_modules&quot;&gt;One binary, no &lt;code dir=&quot;auto&quot;&gt;node_modules&lt;/code&gt;&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;go build&lt;/code&gt; produces a single static binary. You ship it to a $6 VPS, &lt;code dir=&quot;auto&quot;&gt;./laju-go&lt;/code&gt;, and you’re done. There is no &lt;code dir=&quot;auto&quot;&gt;node_modules/&lt;/code&gt; to sync, no runtime to install, no version manager to fight. The Dockerfile is a multi-stage build that ends with &lt;code dir=&quot;auto&quot;&gt;COPY /laju-go /laju-go&lt;/code&gt; on a &lt;code dir=&quot;auto&quot;&gt;FROM scratch&lt;/code&gt;-style base. The deploy story is a 12 MB artifact and a systemd unit.&lt;/p&gt;
&lt;p&gt;Node ships a runtime. Go ships a binary. For a SaaS that needs to run for years, that difference shows up every time you provision a server, every time you debug a production issue at 2am, and every time you onboard a contributor who just wants to run the thing.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;goroutines-not-an-event-loop&quot;&gt;Goroutines, not an event loop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Node’s concurrency model is a single-threaded event loop with async callbacks. It works, but it forces every I/O operation through a callback chain and punishes you with callback hell or &lt;code dir=&quot;auto&quot;&gt;async&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;await&lt;/code&gt; coloring if you forget. Go’s model is goroutines — lightweight, multiplexed onto OS threads by the runtime, with no function-coloring.&lt;/p&gt;
&lt;p&gt;Laju Go uses this directly. The main process starts a background cleanup goroutine that prunes expired sessions and password-reset tokens every few minutes, while the HTTP server handles requests on the main goroutine:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Start background cleanup for expired sessions and password reset tokens&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;startBackgroundCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;querier&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// ...later, the server runs in its own goroutine&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;slog&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;server listening&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AppPort&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Listen&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AppPort&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;slog&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;server failed&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;os&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Exit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;No &lt;code dir=&quot;auto&quot;&gt;setInterval&lt;/code&gt;, no &lt;code dir=&quot;auto&quot;&gt;Promise.all&lt;/code&gt;, no thinking about whether something blocks the loop. You write sequential code and the runtime handles the scheduling.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;fasthttp-under-the-hood&quot;&gt;fasthttp under the hood&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Laju Go runs on &lt;a href=&quot;https://gofiber.io&quot;&gt;Fiber&lt;/a&gt;, which is built on &lt;a href=&quot;https://github.com/valyala/fasthttp&quot;&gt;fasthttp&lt;/a&gt; — not &lt;code dir=&quot;auto&quot;&gt;net/http&lt;/code&gt;. fasthttp is purpose-built for low allocation and high throughput. The Fiber benchmark sits around 300,000 requests per second on commodity hardware, an order of magnitude above Express and well above what Bun’s HTTP layer sustains under real workloads.&lt;/p&gt;
&lt;p&gt;The app is configured for it explicitly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fiber&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;New&lt;/span&gt;&lt;span&gt;(fiber.Config{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;AppName&lt;/span&gt;&lt;span&gt;:           &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Laju&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ErrorHandler&lt;/span&gt;&lt;span&gt;:      &lt;/span&gt;&lt;span&gt;customErrorHandler&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;StreamRequestBody&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,        &lt;/span&gt;&lt;span&gt;// stream uploads, don&apos;t buffer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ReadBufferSize&lt;/span&gt;&lt;span&gt;:    &lt;/span&gt;&lt;span&gt;64&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt;,   &lt;/span&gt;&lt;span&gt;// tuned for large uploads&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;StreamRequestBody: true&lt;/code&gt; is the difference between buffering a 2 GB TUS upload in RAM and streaming it to disk. On Node, you’d be configuring streams manually and hoping the framework respects them.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;no-gc-pauses-that-matter&quot;&gt;No GC pauses that matter&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Go’s GC is concurrent and sub-millisecond for small heaps. Laju Go’s heap is small — SQLite is the store, sessions are cached in a &lt;code dir=&quot;auto&quot;&gt;sync.RWMutex&lt;/code&gt; map, and the query layer is generated code with no reflection. There is no V8-style pause-the-world stop on request paths. The p99 latency is the database, not the runtime.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-real-tradeoff&quot;&gt;The real tradeoff&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Go is not the right answer if your team only knows TypeScript, or if you need a library that only exists on npm. It is the right answer when you want a backend that compiles in 2 seconds, deploys as one file, handles hundreds of thousands of requests per second per core, and has a concurrency model that doesn’t require a mental model of the event loop to reason about.&lt;/p&gt;
&lt;p&gt;Laju Go is a SaaS starter. The backend is the part that runs forever and costs you money per request. That’s the part worth optimizing — and Go is the optimization.&lt;/p&gt;</content:encoded><category>go</category><category>architecture</category></item><item><title>Why Inertia — not SPA + API</title><link>https://laju.dev/blog/why-inertia/</link><guid isPermaLink="true">https://laju.dev/blog/why-inertia/</guid><description>Server-driven SPA, no separate API, useForm, SSR shell via templ, 303 redirects</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The conventional SaaS architecture is two apps: a React/Vue SPA and a JSON API. You build the API, then you build the SPA, then you build the glue — routing, auth state, fetch wrappers, error handling, loading spinners, optimistic updates. Inertia.js deletes the middle layer. Laju Go is built on it, and the result is a server-driven SPA with no separate API.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-server-owns-the-page&quot;&gt;The server owns the page&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;With Inertia, a GET request returns an HTML shell on the first load and a JSON page payload on subsequent XHRs. The Go handler decides what component renders and what props it gets — exactly like a server-rendered app, except the client swaps the component without a full reload.&lt;/p&gt;
&lt;p&gt;Here’s a Laju Go handler rendering a page:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;h &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;AppHandler) &lt;/span&gt;&lt;span&gt;Dashboard&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;fiber.Ctx) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;inertiaService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Redirect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/login&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;inertiaService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;app/Dashboard&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, fiber.Map{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Render(c, &quot;app/Dashboard&quot;, props)&lt;/code&gt; tells Inertia: render the Svelte component at &lt;code dir=&quot;auto&quot;&gt;app/Dashboard&lt;/code&gt; with these props. On a normal browser hit, it returns the HTML shell with the props serialized into a JSON &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;div data-page&gt;&lt;/code&gt;. On an Inertia XHR (&lt;code dir=&quot;auto&quot;&gt;X-Inertia: true&lt;/code&gt;), it returns just the JSON. The Svelte side mounts the component. No API endpoint, no client-side router, no data-fetching hook.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;useform-replaces-the-fetch-layer&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;useForm&lt;/code&gt; replaces the fetch layer&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The other half is form submission. Inertia’s &lt;code dir=&quot;auto&quot;&gt;useForm&lt;/code&gt; handles POST/PUT/PATCH with CSRF, loading state, error bag, and redirect-following built in:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;lang&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ts&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { useForm } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@inertiajs/svelte&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;useForm&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ email: &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;, password: &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;submitForm&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;preventDefault&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/login&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;form.post(&quot;/login&quot;)&lt;/code&gt; POSTs the form fields as form-encoded data with the CSRF token, follows the response, and updates &lt;code dir=&quot;auto&quot;&gt;form.errors&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;form.processing&lt;/code&gt; automatically. The Go handler doesn’t know it’s talking to Inertia — it just parses the body, calls the service, and redirects.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;303-redirects-not-json-contracts&quot;&gt;303 redirects, not JSON contracts&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;After a successful POST, the handler returns a 303 See Other. Inertia intercepts it and does a partial reload to the new page. This is the same control flow as a traditional server-rendered app — POST, redirect, GET — except the client doesn’t blink:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;h &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;AuthHandler) &lt;/span&gt;&lt;span&gt;Login&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;fiber.Ctx) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt; models.LoginRequest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;BodyParser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Status&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fiber&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;StatusBadRequest&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(fiber.Map{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Invalid request body&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;authService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Login&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Email&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Password&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;errors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Is&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;services&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ErrInvalidCredentials&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Flash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Invalid email or password&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;inertiaService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Redirect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/login&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Flash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Failed to login. Please try again.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;inertiaService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Redirect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/login&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;store&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CreateAuthenticatedSession&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;inertiaService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Redirect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/app&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;No API response shape to design. No &lt;code dir=&quot;auto&quot;&gt;status: &quot;success&quot;&lt;/code&gt; field. No client-side code that maps API errors to form fields. The flash message is set server-side, consumed on the next render, and gone.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;ssr-shell-via-templ&quot;&gt;SSR shell via templ&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The initial HTML shell — the &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;head&gt;&lt;/code&gt;, the asset tags, the root &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;div data-page&gt;&lt;/code&gt; — is rendered by a &lt;a href=&quot;./why-templ&quot;&gt;templ&lt;/a&gt; component, not by a JS bundle. That means the first byte of HTML is real, crawlable, and fast. The Svelte app hydrates into it. You get SPA navigation after the first load and a server-rendered shell on the first request, without running a Node SSR server.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;external-redirects-work-too&quot;&gt;External redirects work too&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;For OAuth flows that leave the app, Inertia has a separate mechanism — &lt;code dir=&quot;auto&quot;&gt;Location&lt;/code&gt; returns a 409 with &lt;code dir=&quot;auto&quot;&gt;X-Inertia-Location&lt;/code&gt;, which the client turns into a &lt;code dir=&quot;auto&quot;&gt;window.location&lt;/code&gt; assignment:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;h &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;AuthHandler) &lt;/span&gt;&lt;span&gt;GoogleLogin&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;fiber.Ctx) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;generateState&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;authService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GoogleAuthURL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;inertiaService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Location&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;the-tradeoff&quot;&gt;The tradeoff&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Inertia is not for apps that need a public JSON API consumed by mobile clients or third parties. If your product is “an API that happens to have a web UI,” you need the API. Laju Go’s assumption is the opposite: the web UI &lt;em&gt;is&lt;/em&gt; the product, and the API is an implementation detail of the server. For that shape of app, Inertia removes an entire layer of code — the client-side data layer — and lets the Go handlers be the single source of truth for routing, auth, and data.&lt;/p&gt;</content:encoded><category>inertia</category><category>architecture</category><category>frontend</category></item><item><title>Why sqlc — not an ORM</title><link>https://laju.dev/blog/why-sqlc/</link><guid isPermaLink="true">https://laju.dev/blog/why-sqlc/</guid><description>Type-safe Go from SQL, compile-time safety, zero runtime reflection, AI writes SQL at peak performance</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The Go ecosystem has ORMs — GORM, ent, Pop. Laju Go uses none of them. It uses &lt;a href=&quot;https://sqlc.dev&quot;&gt;sqlc&lt;/a&gt;, a compiler that reads SQL and generates type-safe Go. The reasoning is simple: SQL is the right language for database work, and Go is the right language for application logic. An ORM forces you to write SQL in a worse language and then translates it back to SQL at runtime.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;you-write-sql-it-writes-go&quot;&gt;You write SQL, it writes Go&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The input is a &lt;code dir=&quot;auto&quot;&gt;.sql&lt;/code&gt; file with named queries. Here’s the entire password-reset query set in Laju Go:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- queries/password_reset.sql&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- name: CreatePasswordReset :exec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; password_resets (token, user_id, email, expires_at, created_at)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;VALUES&lt;/span&gt;&lt;span&gt; (?, ?, ?, ?, ?);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- name: GetPasswordReset :one&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; password_resets &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; token &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ? &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; used &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt; expires_at &lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; ?;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- name: MarkPasswordResetUsed :exec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;UPDATE&lt;/span&gt;&lt;span&gt; password_resets &lt;/span&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; used &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; token &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ?;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-- name: DeleteExpiredPasswordResets :exec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; password_resets &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; expires_at &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt; CURRENT_TIMESTAMP;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;npm run db:generate&lt;/code&gt; runs sqlc, which produces Go code in &lt;code dir=&quot;auto&quot;&gt;app/queries/&lt;/code&gt; — typed query constants, parameter structs, and methods on a &lt;code dir=&quot;auto&quot;&gt;Queries&lt;/code&gt; struct. The generated code for &lt;code dir=&quot;auto&quot;&gt;CreateUser&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;user.sql&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Code generated by sqlc. DO NOT EDIT.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;-- name: CreateUser :one&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;INSERT INTO users (email, name, password, role, created_at, updated_at)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;VALUES (?, ?, ?, ?, ?, ?)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;RETURNING id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; CreateUserParams &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Email&lt;/span&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Password&lt;/span&gt;&lt;span&gt;  sql.NullString&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Role&lt;/span&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CreatedAt&lt;/span&gt;&lt;span&gt; time.Time&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;UpdatedAt&lt;/span&gt;&lt;span&gt; time.Time&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;q &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;Queries) &lt;/span&gt;&lt;span&gt;CreateUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; context.Context, &lt;/span&gt;&lt;span&gt;arg&lt;/span&gt;&lt;span&gt; CreateUserParams) (&lt;/span&gt;&lt;/span&gt;&lt;span&gt;int64&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;row&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;q&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;QueryRowContext&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;arg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Email&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;arg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;arg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Password&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;arg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Role&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;arg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CreatedAt&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;arg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UpdatedAt&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int64&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;row&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Scan&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The service layer calls it like a normal function:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;querier&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CreateUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt;, queries.CreateUserParams{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Email&lt;/span&gt;&lt;span&gt;:     &lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt;:      &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Password&lt;/span&gt;&lt;span&gt;:  sql.NullString{&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;hash&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Valid&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Role&lt;/span&gt;&lt;span&gt;:      &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CreatedAt&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Now&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;UpdatedAt&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Now&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;No string building. No &lt;code dir=&quot;auto&quot;&gt;.Where(&quot;email = ?&quot;, email).First(&amp;#x26;user)&lt;/code&gt;. No struct tags that secretly control SQL generation. The SQL is the source of truth and the Go is derived from it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;compile-time-safety&quot;&gt;Compile-time safety&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you rename a column in the schema and forget to update a query, sqlc fails at generation time — before the app compiles. If you pass the wrong type to a parameter, the Go compiler catches it. There is no &lt;code dir=&quot;auto&quot;&gt;interface{}&lt;/code&gt; reflection at runtime, no “query returned 3 columns but struct has 4” panic in production.&lt;/p&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;app/queries/&lt;/code&gt; directory is generated and committed. The rule in &lt;code dir=&quot;auto&quot;&gt;AGENTS.md&lt;/code&gt; is explicit: &lt;strong&gt;never edit &lt;code dir=&quot;auto&quot;&gt;app/queries/&lt;/code&gt; manually&lt;/strong&gt;. You edit the &lt;code dir=&quot;auto&quot;&gt;.sql&lt;/code&gt; file, run &lt;code dir=&quot;auto&quot;&gt;db:generate&lt;/code&gt;, and the Go updates. This keeps the SQL as the single source of truth and prevents the generated code from drifting.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;zero-runtime-reflection&quot;&gt;Zero runtime reflection&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;GORM and ent use reflection and struct tags to build queries at runtime. That means allocations, interface boxing, and a non-trivial CPU cost per query. sqlc generates plain &lt;code dir=&quot;auto&quot;&gt;QueryRowContext&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;QueryContext&lt;/code&gt; calls with the SQL as a string constant. The runtime cost is the database driver and nothing else. For a high-throughput SaaS, that’s measurable.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;ai-writes-sql-at-peak-performance&quot;&gt;AI writes SQL at peak performance&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is the part that matters in 2026. LLMs are excellent at SQL and mediocre at ORM DSLs. When the data layer is plain SQL, an AI agent can write a new query — &lt;code dir=&quot;auto&quot;&gt;-- name: GetUserByEmail :one\nSELECT * FROM users WHERE email = ?&lt;/code&gt; — and sqlc generates the Go. The agent doesn’t need to know GORM’s chainable API, ent’s schema definition syntax, or which struct tag means “primary key.” It writes SQL, which is the thing it’s best at, and the compiler does the rest.&lt;/p&gt;
&lt;p&gt;The three-tier rule in Laju Go enforces this: the Query layer is the &lt;strong&gt;only&lt;/strong&gt; layer that executes SQL. Services call &lt;code dir=&quot;auto&quot;&gt;s.querier.*&lt;/code&gt;. Handlers never touch the database. This means an AI agent adding a feature writes SQL + a service method + a handler, and each layer has a clear contract. The ORM version of this task requires the agent to understand the ORM’s mental model, its migration system, and its query builder — all of which are worse than SQL for the database part.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-tradeoff&quot;&gt;The tradeoff&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;sqlc doesn’t do migrations (Laju Go uses &lt;a href=&quot;https://github.com/pressly/goose&quot;&gt;Goose&lt;/a&gt; for that). It doesn’t do relationships — there’s no &lt;code dir=&quot;auto&quot;&gt;.Preload(&quot;orders&quot;)&lt;/code&gt;. If you want eager loading, you write a JOIN in SQL, which is what you should do anyway. And it’s SQL-first, so if you don’t want to write SQL, it’s not for you. But if you’re building a SaaS and you’re willing to write SQL, sqlc gives you type safety, compile-time checking, zero runtime overhead, and an AI-friendly data layer — all at once.&lt;/p&gt;</content:encoded><category>sqlc</category><category>database</category><category>architecture</category></item><item><title>Why SQLite — not Postgres</title><link>https://laju.dev/blog/why-sqlite/</link><guid isPermaLink="true">https://laju.dev/blog/why-sqlite/</guid><description>One file, no server, WAL mode, tens of thousands of req/s, backup is a file copy</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Postgres is the default database for a SaaS starter. It’s also the default reason a starter is hard to deploy: you need a server, a port, credentials, a connection pooler, and a backup strategy that isn’t &lt;code dir=&quot;auto&quot;&gt;pg_dump&lt;/code&gt; on a cron. Laju Go uses SQLite, and for a starter that targets a single-instance VPS, the tradeoff is almost always in SQLite’s favor.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;one-file-no-server&quot;&gt;One file, no server&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;SQLite is a library, not a server process. The database is a single file on disk. There is no &lt;code dir=&quot;auto&quot;&gt;postgres&lt;/code&gt; daemon to install, no port to open, no &lt;code dir=&quot;auto&quot;&gt;DATABASE_URL&lt;/code&gt; to configure, no connection pool to tune against a remote host. Laju Go opens it with one line:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Open&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sqlite3&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DBPath&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The DSN is &lt;code dir=&quot;auto&quot;&gt;file:data.db?_foreign_keys=on&lt;/code&gt; — a path, not a network address. This means the database lives and dies with the app binary. Deploy is &lt;code dir=&quot;auto&quot;&gt;scp laju-go data.db server:&lt;/code&gt;. Backup is &lt;code dir=&quot;auto&quot;&gt;cp data.db data.db.bak&lt;/code&gt;. There is no separate failure mode for “the database is up but the app can’t reach it.”&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;wal-mode-for-concurrency&quot;&gt;WAL mode for concurrency&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The old knock on SQLite was write concurrency — a single writer lock. WAL (Write-Ahead Logging) mode fixes this. Readers and writers don’t block each other; writes append to a WAL file and checkpoint asynchronously. Laju Go enables it explicitly, along with a set of PRAGMAs tuned for a single-instance NVMe VPS:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;initDatabase&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;dbPath&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;sql.DB, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sql&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Open&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sqlite3&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;dbPath&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SetMaxOpenConns&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SetMaxIdleConns&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SetConnMaxLifetime&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Minute&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SetConnMaxIdleTime&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Second&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PRAGMA foreign_keys = ON&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PRAGMA journal_mode = WAL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PRAGMA synchronous = NORMAL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PRAGMA cache_size = -16000&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)      &lt;/span&gt;&lt;span&gt;// 16 MB cache&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PRAGMA mmap_size = 268435456&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)    &lt;/span&gt;&lt;span&gt;// 256 MB memory-mapped I/O&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PRAGMA temp_store = MEMORY&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PRAGMA busy_timeout = 5000&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)      &lt;/span&gt;&lt;span&gt;// wait 5s for locks&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;PRAGMA wal_autocheckpoint = 1000&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;synchronous = NORMAL&lt;/code&gt; trades strict durability (FULL) for a ~2x write speedup while remaining safe against power loss in WAL mode. &lt;code dir=&quot;auto&quot;&gt;mmap_size = 256MB&lt;/code&gt; lets SQLite memory-map the file so reads bypass the page cache syscall path. &lt;code dir=&quot;auto&quot;&gt;busy_timeout = 5000&lt;/code&gt; means writers wait for a lock instead of failing immediately.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;tens-of-thousands-of-requests-per-second&quot;&gt;Tens of thousands of requests per second&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;On a modern NVMe drive with WAL mode, SQLite sustains tens of thousands of writes per second and far more reads — more than enough for a SaaS starter’s first 10,000 users. The bottleneck for a typical SaaS is not the database’s raw throughput; it’s the application’s query patterns and indexing. SQLite with the right PRAGMAs handles the load that a single-instance app can generate.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;backup-is-a-file-copy&quot;&gt;Backup is a file copy&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Backing up Postgres is a production concern: &lt;code dir=&quot;auto&quot;&gt;pg_dump&lt;/code&gt;, WAL archiving, PITR, a replica. Backing up SQLite is:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sqlite3&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data.db&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;.backup data.db.bak&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# or, if the app is stopped:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data.db&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data.db.bak&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;.backup&lt;/code&gt; command uses the online backup API — it works while the app is running and produces a consistent snapshot. For a starter, this is a one-line cron job. There is no replication slot to fill up, no WAL archive to fill the disk.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;when-you-outgrow-it&quot;&gt;When you outgrow it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;SQLite is single-instance. The moment you need horizontal scale — two app servers hitting the same database — you need Postgres. Laju Go’s architecture is built for that day: the query layer is generated by &lt;a href=&quot;./why-sqlc&quot;&gt;sqlc&lt;/a&gt; from plain SQL, so swapping SQLite for Postgres is a matter of changing the driver, the schema types, and regenerating. The three-tier rule (Handler → Service → Query) means no business logic touches the database driver directly.&lt;/p&gt;
&lt;p&gt;But most SaaS starters never reach that day. Most never reach the point where a single VPS can’t handle the load. SQLite gets you to revenue without a database server, and the migration path is open when you need it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-tradeoff&quot;&gt;The tradeoff&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;SQLite is the wrong choice if you need multi-writer concurrency across nodes, if you need Postgres-specific features (JSONB operators, full-text search via &lt;code dir=&quot;auto&quot;&gt;tsvector&lt;/code&gt;, logical replication), or if you’re deploying on a network filesystem where file locks are unreliable. It is the right choice for a starter that wants to ship on one box, back up with &lt;code dir=&quot;auto&quot;&gt;cp&lt;/code&gt;, and not run a database server until the business pays for one.&lt;/p&gt;</content:encoded><category>sqlite</category><category>database</category><category>architecture</category></item><item><title>Why Svelte 5 — not React or Vue</title><link>https://laju.dev/blog/why-svelte/</link><guid isPermaLink="true">https://laju.dev/blog/why-svelte/</guid><description>Runes reactivity, compiled output, no VDOM — why Svelte 5 fits this stack</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The frontend choice for Laju Go is Svelte 5. Not React, not Vue. This isn’t a fashion call — it’s a decision driven by three things: the reactivity model, the compilation step, and how cleanly it integrates with a server-driven architecture.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;runes-not-hooks&quot;&gt;Runes, not hooks&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Svelte 5 introduced runes — &lt;code dir=&quot;auto&quot;&gt;$state&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;$derived&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;$props&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;$effect&lt;/code&gt; — as the reactivity primitives. They look like function calls but are compiler directives. The difference from React hooks is structural: runes have no rules-of-hooks constraints, no dependency arrays, no stale-closure traps.&lt;/p&gt;
&lt;p&gt;Here’s the actual profile page in Laju Go:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;lang&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ts&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; Props {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;?:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;let { &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Props&lt;/span&gt;&lt;span&gt; = $&lt;/span&gt;&lt;span&gt;props&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;profileForm&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;useForm&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;EditProfile&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; ?? &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;email: &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;email&lt;/span&gt;&lt;span&gt; ?? &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;avatar: &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;avatar&lt;/span&gt;&lt;span&gt; ?? &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;let &lt;/span&gt;&lt;span&gt;showPassword&lt;/span&gt;&lt;span&gt; = $&lt;/span&gt;&lt;span&gt;state&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;let &lt;/span&gt;&lt;span&gt;previewUrl&lt;/span&gt;&lt;span&gt; = $&lt;/span&gt;&lt;span&gt;derived&lt;/span&gt;&lt;span&gt;(user&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;avatar&lt;/span&gt;&lt;span&gt; ?? &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;$props()&lt;/code&gt; receives server data. &lt;code dir=&quot;auto&quot;&gt;$state(false)&lt;/code&gt; is local reactive state. &lt;code dir=&quot;auto&quot;&gt;$derived(...)&lt;/code&gt; is a computed value that updates when its dependencies change — no &lt;code dir=&quot;auto&quot;&gt;useMemo&lt;/code&gt;, no dependency array to get wrong. You declare what the value &lt;em&gt;is&lt;/em&gt;, not how to &lt;em&gt;recompute it on render&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The rules are short and load-bearing: use &lt;code dir=&quot;auto&quot;&gt;$derived()&lt;/code&gt; for derived state, &lt;code dir=&quot;auto&quot;&gt;$state(value ?? default)&lt;/code&gt; to init from props, and reserve &lt;code dir=&quot;auto&quot;&gt;$effect&lt;/code&gt; for actual side effects (&lt;code dir=&quot;auto&quot;&gt;document.title&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;localStorage&lt;/code&gt;). There is no &lt;code dir=&quot;auto&quot;&gt;useEffect&lt;/code&gt;-as-derived-state anti-pattern because the primitive for that exists.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;compiled-no-virtual-dom&quot;&gt;Compiled, no virtual DOM&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Svelte is a compiler. At build time it turns &lt;code dir=&quot;auto&quot;&gt;.svelte&lt;/code&gt; files into vanilla JavaScript that directly manipulates the DOM. There is no virtual DOM, no diffing, no reconciliation pass at runtime. React reconciles every render; Svelte emits code that touches only the nodes that changed.&lt;/p&gt;
&lt;p&gt;The practical consequence is bundle size and runtime cost. A Svelte 5 component ships as a few hundred bytes of JS that wires up event listeners and updates bindings. The same component in React ships the component &lt;em&gt;plus&lt;/em&gt; the React runtime &lt;em&gt;plus&lt;/em&gt; the JSX transform, and pays a reconciliation tax on every state change. For a SaaS dashboard with 30 reactive widgets, that’s the difference between a 40 KB JS bundle and a 180 KB one.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;inertia-integration&quot;&gt;Inertia integration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Laju Go uses Inertia.js to drive the SPA from the server. Svelte’s Inertia adapter gives you &lt;code dir=&quot;auto&quot;&gt;useForm&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;use:inertia&lt;/code&gt; — the two primitives that replace an entire API layer:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;lang&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ts&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { inertia, useForm } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@inertiajs/svelte&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;useForm&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{ email: &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;, password: &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;submitForm&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;Event&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;preventDefault&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/login&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;onsubmit&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;submitForm&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;form.post(&quot;/login&quot;)&lt;/code&gt; submits to the Go handler, which validates, creates a session, and returns a 303 redirect. No &lt;code dir=&quot;auto&quot;&gt;fetch&lt;/code&gt;, no &lt;code dir=&quot;auto&quot;&gt;JSON.stringify&lt;/code&gt;, no client-side state machine for loading/error/success. The server is the source of truth and Inertia is the transport.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-not-react&quot;&gt;Why not React&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;React is the default, but the default is expensive: a larger runtime, a mental model built around renders and effects, and an ecosystem that assumes you’re building a client-side app with its own routing and data layer. Laju Go’s architecture is server-driven — the Go handlers own routing, auth, and data. React’s client-side machinery is overhead you pay for and don’t use.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-not-vue&quot;&gt;Why not Vue&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Vue 3’s composition API is good and Vue is also compiled. But Svelte 5’s runes are a cleaner reactivity story than Vue’s &lt;code dir=&quot;auto&quot;&gt;ref&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;reactive&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;computed&lt;/code&gt; split, and Svelte’s output is smaller. For a starter that prioritizes a small, fast, server-driven frontend, Svelte 5 is the tighter fit.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-tradeoff&quot;&gt;The tradeoff&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Svelte 5 is newer than React and has a smaller ecosystem. If you need a niche component that only exists as an npm package for React, you’ll write it yourself. For a SaaS boilerplate where the frontend is forms, tables, and dashboards driven by a Go backend, that’s a trade worth making. You get a smaller bundle, a simpler reactivity model, and a compiler that catches mistakes before they reach the browser.&lt;/p&gt;</content:encoded><category>svelte</category><category>frontend</category><category>architecture</category></item><item><title>Why templ — not HTML templates</title><link>https://laju.dev/blog/why-templ/</link><guid isPermaLink="true">https://laju.dev/blog/why-templ/</guid><description>Type-safe HTML in Go, compile-time checking, no string interpolation, SVG handling</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Go’s standard &lt;code dir=&quot;auto&quot;&gt;html/template&lt;/code&gt; package is fine for a blog. For a SaaS landing page with SVG logos, conditional classes, component composition, and a design system, it becomes a liability: string interpolation, runtime errors, no type checking on the data you pass in. Laju Go uses &lt;a href=&quot;https://templ.ph&quot;&gt;templ&lt;/a&gt; for its server-rendered HTML — the landing page, the Inertia shell — and the difference is structural.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;type-safe-html-in-go&quot;&gt;Type-safe HTML in Go&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;templ is a typed template language that compiles to Go. You write &lt;code dir=&quot;auto&quot;&gt;.templ&lt;/code&gt; files; &lt;code dir=&quot;auto&quot;&gt;templ generate&lt;/code&gt; produces &lt;code dir=&quot;auto&quot;&gt;*_templ.go&lt;/code&gt; files that render to an &lt;code dir=&quot;auto&quot;&gt;io.Writer&lt;/code&gt;. The parameters are typed Go function arguments, not &lt;code dir=&quot;auto&quot;&gt;interface{}&lt;/code&gt; passed through a map.&lt;/p&gt;
&lt;p&gt;Here’s the signature of Laju Go’s landing page:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;templates/index.templ&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;templ&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LandingPage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;isDev&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;viteURL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;mainCSS&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;productLinks&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; FooterLinks{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;FLabel&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Features&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;FURL&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;#features&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;FLabel&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;How it works&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;FURL&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;#how-it-works&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;!&lt;/span&gt;&lt;span&gt;doctype&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lang&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;en&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;dark&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;charset&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;UTF-8&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt; } &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Laju&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Go&lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;title&lt;/code&gt; is a &lt;code dir=&quot;auto&quot;&gt;string&lt;/code&gt;. &lt;code dir=&quot;auto&quot;&gt;isDev&lt;/code&gt; is a &lt;code dir=&quot;auto&quot;&gt;bool&lt;/code&gt;. If you pass the wrong type, the Go compiler tells you at build time. There is no &lt;code dir=&quot;auto&quot;&gt;template.Execute(w, data)&lt;/code&gt; that panics at runtime because a field was missing or the wrong type.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;compile-time-checking&quot;&gt;Compile-time checking&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;templ generate&lt;/code&gt; runs before &lt;code dir=&quot;auto&quot;&gt;go build&lt;/code&gt;. If a template references an undefined variable, has a syntax error, or calls a component with the wrong arguments, generation fails. The error is at build time, not at the first request that hits the route. This is the same guarantee sqlc gives you for SQL — the bad code never reaches production.&lt;/p&gt;
&lt;p&gt;The generated file is &lt;code dir=&quot;auto&quot;&gt;index_templ.go&lt;/code&gt;, and the rule in &lt;code dir=&quot;auto&quot;&gt;AGENTS.md&lt;/code&gt; is explicit: &lt;strong&gt;edit only &lt;code dir=&quot;auto&quot;&gt;.templ&lt;/code&gt; files, never &lt;code dir=&quot;auto&quot;&gt;*_templ.go&lt;/code&gt;&lt;/strong&gt;. The generated file is overwritten on every &lt;code dir=&quot;auto&quot;&gt;templ generate&lt;/code&gt;. This keeps the source of truth in the &lt;code dir=&quot;auto&quot;&gt;.templ&lt;/code&gt; file and prevents hand-edits from being silently destroyed.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;no-string-interpolation&quot;&gt;No string interpolation&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;html/template&lt;/code&gt; escapes by default, but you’re still building strings. templ renders directly to a &lt;code dir=&quot;auto&quot;&gt;Buffer&lt;/code&gt; / &lt;code dir=&quot;auto&quot;&gt;Writer&lt;/code&gt; with proper context-aware escaping built into the compiler. There is no &lt;code dir=&quot;auto&quot;&gt;fmt.Sprintf(&quot;&amp;#x3C;div class=&apos;%s&apos;&gt;&quot;, class)&lt;/code&gt; — that’s a string, not HTML, and it’s how XSS happens. In templ:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;px-6 py-3 rounded-lg bg-brand-400 &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extraClass&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Name&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;{ }&lt;/code&gt; expressions are type-checked and escaped by context. &lt;code dir=&quot;auto&quot;&gt;{ user.Name }&lt;/code&gt; in an HTML text node is HTML-escaped. The same expression in an attribute context is attribute-escaped. You don’t think about it, and you can’t get it wrong.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;svg-and-component-composition&quot;&gt;SVG and component composition&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The landing page has inline SVG logos, gradient definitions, and path data. In &lt;code dir=&quot;auto&quot;&gt;html/template&lt;/code&gt;, inline SVG is a wall of raw strings or &lt;code dir=&quot;auto&quot;&gt;template.HTML&lt;/code&gt; escapes that bypass safety. In templ, SVG is just markup — you write it directly, the compiler parses it, and it renders as-is:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;svg&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;width&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;28&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;28&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;viewBox&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;0 0 32 32&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;none&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;xmlns&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;http://www.w3.org/2000/svg&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;defs&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;linearGradient&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;logoGrad&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;x1&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;y1&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;x2&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;100%&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;y2&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;100%&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;stop&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;offset&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;stop&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;#22d3ee&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;stop&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;offset&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;100%&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;stop&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;#a855f7&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;linearGradient&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;defs&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;rect&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;width&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;32&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;height&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;32&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rx&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;url(#logoGrad)&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;d&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;M19 7L10 17h5l-1 8 9-10h-5l1-8Z&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;white&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;svg&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Components are Go functions. &lt;code dir=&quot;auto&quot;&gt;@FeatureCardLarge(&quot;auth&quot;, &quot;Authentication&quot;, &quot;...&quot;, &quot;brand&quot;)&lt;/code&gt; is a call to another templ component, type-checked at compile time. You compose pages from components the same way you compose Svelte components — except the output is server-rendered HTML with zero client-side JS.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-templ-fits-in-laju-go&quot;&gt;Where templ fits in Laju Go&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;templ renders two things: the public landing page (standalone HTML, no Inertia) and the Inertia root shell (the &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;head&gt;&lt;/code&gt;, asset tags, and &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;div data-page&gt;&lt;/code&gt; that Inertia hydrates into). The Svelte app handles everything inside the authenticated app. This split is deliberate — the marketing page should be fast, crawlable HTML with no JS bundle, and templ makes that type-safe. The app UI is interactive and uses Svelte. Each tool does what it’s best at.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-tradeoff&quot;&gt;The tradeoff&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;templ requires a build step (&lt;code dir=&quot;auto&quot;&gt;templ generate&lt;/code&gt;) and a CGO-free Go toolchain. The syntax is not Go — it’s templ’s own DSL — so there’s a small learning curve, and editor support is good but not universal. If you’re rendering a single simple page, &lt;code dir=&quot;auto&quot;&gt;html/template&lt;/code&gt; is less machinery. For a SaaS with a real landing page, a design system, and SVG assets, templ’s compile-time safety and component model are worth the build step. You get type-checked HTML, no string interpolation, and errors at build time instead of in production.&lt;/p&gt;</content:encoded><category>templ</category><category>frontend</category><category>architecture</category></item></channel></rss>