Overview
Laju Go is a single-entry Go application. One binary, one process, one main(). There is no separate API server — Inertia.js makes the Svelte 5 frontend a server-rendered SPA that talks to the same Fiber process that serves HTML.
Entry point
Section titled “Entry point”Everything boots from cmd/laju-go/main.go. That file owns the entire dependency graph: it opens SQLite, runs Goose migrations, builds the querier, wires services into handlers, registers routes, and starts Fiber.
// cmd/laju-go/main.go — abbreviateddb, err := initDatabase(cfg.DBPath)if err := runMigrations(db, "./migrations"); err != nil { /* ... */ }
querier := queries.NewQuerier(db)sessionCache := cache.NewSessionCache()sessionStore := session.New(querier, sessionCache, cfg.SessionTTL)
authService := services.NewAuthService(querier, services.AuthServiceConfig{ /* ... */ })userService := services.NewUserService(querier)inertiaService := services.NewInertiaService(assetService, sessionStore)
routeHandlers := routes.Handlers{ Public: handlers.NewPublicHandler(authService, userService, inertiaService, assetService), Auth: handlers.NewAuthHandler(authService, sessionStore, inertiaService), App: handlers.NewAppHandler(userService, sessionStore, inertiaService), Upload: uploadHandler,}
routes.SetupRoutes(app, routeHandlers, sessionStore, userService, mailerService, csrfMiddleware)There is no service locator, no plugin registry, no reflection. Dependencies are constructed in main() and passed down by hand. If a handler needs something, it receives it in its constructor.
Request flow
Section titled “Request flow”Browser (Svelte 5 + Inertia.js) │ ▼Fiber app ── global middleware (recover, compress, Inertia, CORS, CSRF) │ ▼routes/web.go ── route + per-group middleware (AuthRequired, Guest, rate limit) │ ▼app/handlers/ ── parse request, call service, return Inertia Render / Redirect / JSON │ ▼app/services/ ── business logic, auth flows, calls s.querier.* or s.cache.* │ ▼app/queries/ ── THE ONLY layer that executes SQL (generated by sqlc) │ ▼SQLite (CGO, mattn/go-sqlite3)A GET that renders a page ends at h.inertiaService.Render(c, "app/Dashboard", fiber.Map{...}) — Inertia returns JSON for XHR visits and full HTML for the first load. A POST/PUT that mutates state ends at h.inertiaService.Redirect(c, "/path"), a 303 See Other that Inertia turns into a follow-up GET.
Layers
Section titled “Layers”| Layer | Path | Responsibility |
|---|---|---|
| Routes | routes/web.go |
URL → handler mapping, middleware grouping. No logic. |
| Handlers | app/handlers/ |
Parse request (BodyParser, params, session), call one service, shape the response. No business logic, no SQL. |
| Services | app/services/ |
Business logic, auth flows, external APIs. Reads via s.querier.* or s.cache.*. Never opens a DB connection. |
| Queries | app/queries/ |
Type-safe SQL, generated by sqlc from queries/*.sql. The only layer that touches the database. Never edited by hand. |
| Models | app/models/ |
Pure data structs — domain entities and DTOs. No logic, no DB access, must not import app/queries. |
| Cache | app/cache/ |
In-memory session cache (sync.RWMutex + map). Called via the Service layer, never from a Handler. |
| Session | app/session/ |
DB-backed sessions with an in-memory cache front. store.Get(c), store.Flash(c, ...), store.CreateAuthenticatedSession(...). |
| Middlewares | app/middlewares/ |
AuthRequired, Guest, AdminRequired, CSRFMiddleware, rate limiters. |
| Frontend | frontend/src/ |
Svelte 5 + Inertia.js. Built by Vite into dist/, consumed by Go via dist/.vite/manifest.json. |
Build order
Section titled “Build order”The Go binary reads Vite’s asset manifest, so the frontend must build first:
vite build → dist/.vite/manifest.json → go buildnpm run build:all (or make build) runs them in the right order. Reversing it produces a binary that references stale or missing assets.
What this buys you
Section titled “What this buys you”- One deploy artifact. A single Go binary plus
dist/andmigrations/. No Node runtime in production. - Type safety end to end. sqlc generates Go from SQL; templ generates Go from templates; Svelte 5 is typed. The only untyped boundary is
fiber.Mapprops passed to Inertia. - No hidden layers. Every dependency is visible in a constructor. Every SQL statement lives in
queries/*.sql. Every route is registered inroutes/web.go.
The single rule that holds this together — Handler → Service → Query, no skipping — is covered in Three-tier rule.