Skip to content

Philosophy

Every choice in Laju Go is a bet against complexity. The goal is a SaaS you can understand entirely in an afternoon, deploy to a single VPS, and scale further than you’ll likely need — without reaching for Kubernetes, a separate API service, or an ORM.

Go compiles to a single static binary. No runtime to install on the server, no node_modules in production, no JIT warmup. You scp one file and run it.

Fiber sits on top of fasthttp, not the standard net/http, which makes it one of the fastest Go web frameworks available. For a SaaS starter, that is headroom you will never have to worry about.

The trade-off: CGO. SQLite uses go-sqlite3, which is CGO-based, so cross-compilation needs a C toolchain. Laju Go solves this with a make build-linux target that uses Zig as the cross-compiler — one command, no manual CC wiring.

Terminal window
make build-linux # GOOS=linux GOARCH=amd64, CGO via Zig

Postgres is excellent. It is also a second process to run, monitor, back up, and connect to. For the vast majority of SaaS starters — and honestly, for a surprising number of production workloads — SQLite is enough.

Laju Go runs SQLite in WAL mode with tuned pragmas for concurrency:

cmd/laju-go/main.go — initDatabase
db.SetMaxOpenConns(1) // SQLite writes are serialized
db.Exec("PRAGMA journal_mode=WAL") // concurrent readers + one writer
db.Exec("PRAGMA busy_timeout=5000")
db.Exec("PRAGMA foreign_keys=ON")
db.Exec("PRAGMA synchronous=NORMAL")

What you get:

  • One file to back up (data/app.db). Copy it. Done.
  • Zero ops — no database server to run, no connection pool to tune against a remote host.
  • Embedded — the database travels with the binary. Deployments are trivial.

When you outgrow SQLite — and you may never — migrating to Postgres is a matter of swapping the sqlc engine and the connection string. The query layer stays the same because sqlc abstracts it.

ORMs trade type safety for convenience, and you pay for it at runtime: N+1 queries you didn’t see, lazy-load surprises, generated SQL you can’t read. sqlc inverts the trade.

You write plain SQL — the thing you already know, the thing that maps 1:1 to what the database actually does:

queries/user.sql
-- name: GetUserByEmail :one
SELECT id, email, name, password, avatar, role, google_id, email_verified, created_at, updated_at
FROM users
WHERE email = ?;

sqlc generates fully typed Go:

// Generated by sqlc. DO NOT EDIT.
func (q *Querier) GetUserByEmail(ctx context.Context, email string) (User, error) { ... }

The benefits:

  • Compile-time safety. If a query references a column that doesn’t exist, sqlc generate fails. If you rename a struct field, the compiler catches every call site.
  • No runtime reflection. The generated code is plain database/sql calls — fast and debuggable.
  • The SQL is the source of truth. You can read every query your app runs, in SQL, in queries/. No hidden query generation.

The trade-off: you write SQL instead of Go method chains. If you find that trade painful, this is not your stack.

The conventional SPA architecture is two apps: a frontend (React/Vue/Svelte) and a separate REST/GraphQL API. You build the API, you build the client, you keep their types in sync, you handle auth twice, you version both. It doubles the surface area.

Inertia.js collapses them. The Go server renders Svelte components directly — the same way it would render an HTML template — but the result is a SPA. There is no separate API. The server is the API.

app/handlers/app.go
func (h *AppHandler) Dashboard(c *fiber.Ctx) error {
sess, _ := h.store.Get(c)
user := sessionUser(sess)
return h.inertiaService.Render(c, "app/Dashboard", fiber.Map{
"user": user,
})
}

On the frontend, useForm + <form> handles CRUD without fetch boilerplate:

<script>
import { useForm } from "@inertiajs/svelte";
const form = useForm({ name: "", email: "" });
function submit() { form.post("/register"); }
</script>
<form onsubmit={submit}>
<input name="name" bind:value={form.fields.name} />
<input name="email" bind:value={form.fields.email} />
<button>Register</button>
</form>

What you get:

  • One codebase, one deploy. The server owns routing, auth, and data — the frontend owns rendering.
  • No client-side routing to maintain. Inertia handles it.
  • No double auth. Sessions and CSRF live on the server; Inertia carries them automatically.
  • SPA UX. No full page reloads, partial reloads supported, optimistic UI via useForm.

The trade-off: your frontend and backend are coupled. If you need a public mobile API, you’ll build it separately. For a SaaS web app, the coupling is the feature.

Go’s html/template is stringly-typed — a typo in a template name or a missing field fails at runtime. templ fixes that by compiling templates to Go code.

You write .templ files with a clean syntax:

templ Dashboard(user *models.UserResponse) {
<div class="dashboard">
<h1>Welcome, { user.Name }</h1>
</div>
}

templ generate produces Go functions that are fully typed — pass the wrong type and the compiler stops you. No more runtime template errors.

The trade-off: it is a code generation step (templ generate), and Air does not watch .templ files by default, so you regenerate manually after edits. A small price for compile-time HTML safety.

Go Fiber

Fast HTTP, single binary, no runtime to deploy. The foundation everything else builds on.

SQLite

One file, zero ops, WAL-tuned for concurrency. Embedded in the binary’s deployment.

sqlc

Type-safe Go from plain SQL. The database layer is readable SQL, not magic method chains.

Inertia.js

SPA without a separate API. The server renders Svelte; one codebase, one deploy.

templ

Type-safe HTML that compiles to Go. No runtime template errors.

Svelte 5

Runes-based reactivity that is simple to reason about. $derived over $effect.

Each piece was chosen to eliminate a category of complexity, not to add a feature. Together they form a stack where the fastest path is also the correct path — and where an AI agent following AGENTS.md produces code that fits the architecture by construction.

  • Not a microservices framework. It is a monolith. That is the point.
  • Not serverless-first. It is a long-running binary. You can containerize it, but the model is a process, not a function.
  • Not ORM-friendly. If you want GORM, you will fight this stack. sqlc is the data layer.
  • Not a headless API. Inertia couples the frontend to the server. If you need a decoupled public API, you build it alongside, not instead.

If those trade-offs match your problem, Laju Go gets you to a deployed, authenticated, uploading SaaS faster than assembling the pieces yourself. If they don’t, that’s useful to know now, too.