tug v0.45.0

Go + Inertia.js v3

React pages straight from Go handlers, with no API in between.

tug is a web framework for Go apps whose frontend is Inertia.js. Handlers render React, Vue or Svelte pages with typed props, and the whole app ships as one binary.

From nothing to one binary
$ go install github.com/cuonggt/tug/cmd/tug@latest
$ tug new blog          # -auth for accounts
$ cd blog
$ tug dev
tug  │ serving http://localhost:8080, built in 268ms
^C
$ tug build
tug build: blog, 9.6 MB, frontend included

How it fits together

A Go struct is the page's props, and its types.

A handler renders a page with a struct, which the page's component gets as its props. tug gen writes the struct's TypeScript, so the component is checked against the Go, and tug dev writes it again as the Go changes.

main.go
type PostsIndexProps struct {
	Posts []Post                   `json:"posts"`
	Stats inertia.DeferProp[Stats] `json:"stats"`
}

var PostsIndex = tug.Page[PostsIndexProps]("Posts/Index")

func index(c *tug.Ctx) error {
	return PostsIndex.Render(c, PostsIndexProps{
		Posts: store.List(),
		Stats: inertia.Defer(store.Count), // after the page shows
	})
}
resources/js/tug/pages.ts
// Code generated by tug gen from the Go types. DO NOT EDIT.

export interface PostsIndexProps {
  posts: Post[]
  stats?: Stats
}
resources/js/pages/Posts/Index.tsx
import { Deferred, Link } from '@inertiajs/react'
import type { PageProps } from '../../tug/pages'
import { route } from '../../tug/routes'

type Props = PageProps<'Posts/Index'>

export default function Index({ posts, stats }: Props) {
  return (
    <>
      <Deferred data="stats" fallback={<p>Counting…</p>}>
        {stats && <p>{stats.posts} posts</p>}
      </Deferred>
      {posts.map((post) => (
        <Link
          key={post.id}
          href={route('posts.show', { id: post.id })}
        >
          {post.title}
        </Link>
      ))}
    </>
  )
}

What's in it

The parts an app needs, already working with Inertia.

Sessions, flash messages, validation errors and CSRF work with Inertia without setup, on the standard library underneath.

  • Inertia v3, whole

    Deferred, merged, once and optional props, partial reloads by path, infinite scroll, and a page's title and description from Go.

  • Types from Go

    tug gen writes the TypeScript of each page's props, the named routes, what each form sends and the flash data's keys.

  • Forms that check themselves

    c.BindValid binds a request and checks it by its struct tags. A form that doesn't validate goes back with its errors, and each field is checked as it's left.

  • Accounts, from one flag

    tug new -auth: registering, verified email, two-factor codes, passkeys, API tokens and notifications, in SQLite, Postgres or MySQL.

  • Background jobs

    Jobs that run again when they fail, on a schedule in any time zone, once however often they're pushed, and in the app's own transaction.

  • One binary

    tug build makes one static binary with the frontend in it, sent gzipped by the app itself, and the app comes with a Dockerfile.

  • Rendered on the server, if you like

    tug new -ssr renders a first visit's page with Inertia's own SSR, in Node beside the app. Without Node, pages render in the browser.

  • Tests without a browser

    Package tugtest is Inertia's client for Go's tests: visits, forms, uploads and logins, with a page's props read into its Go struct.

  • Events on every instance

    Package broadcast carries events on channels through the app's database, to the pages open on every instance, which reload what changed.

Why Go

One small binary, on every core, that starts in milliseconds.

Go makes a web app one static binary, which uses every core in one process, starts in milliseconds, and runs in tens of MB of memory. tug keeps all of it for an Inertia app: one static binary, its frontend's build inside it, served by Go's own HTTP server.

  • Performance

    Tens of thousands of requests a second on a laptop, in one process that uses every core, in tens of MB of RAM.

  • Startup

    In milliseconds, which keeps the cost down on a platform that scales to zero.

  • Deploying

    One file to copy, with no runtime or dependencies to install, built for Linux or ARM from a Mac.

  • Development

    A small language that compiles fast, with an HTTP server in its standard library that's good enough for production.

How fast

60,061 visits a second, in 19.9 MB of memory.

One Inertia page, a post and its comments, served by an app as tug new makes one, with its session, CSRF and Content-Security-Policy, in one process that uses every core.

  • 60,061 visits a second

    Inertia's client asking for the page, answered as JSON, 99 in 100 within 4.38 ms.

  • 56,146 first visits a second

    A browser asking for the page, answered with its HTML, 99 in 100 within 5.09 ms.

  • 19.9 MB of memory under load

    6.4 MB once it has started, idle.

  • 15 ms to start

    From starting the binary to its first answer to the page.

  • 9.6 MB to deploy

    One static binary, with nothing to install beside it.

  • 75 ns added to a request

    What tug's App adds to ServeMux, Go's own router, in one process.

Measured on Apple M1 Max, 10 cores, macOS 27.0.1. Every number, and how they were measured

The guide

From a new app to a deployed one.

18 parts, in the order an app needs them. Each package's own documentation, on pkg.go.dev, has the rest of the detail.

  1. Getting started

    install tug, make an app, run it, and add a page and a form.

  2. Routing and handlers

    the App, routes, whole and signed links, groups, middleware, security headers among them, the route that answered, the client's address behind a proxy, Ctx, files, downloads, streams and events, binding requests, and errors.

  3. Pages

    Inertia pages and their props, the root template and its nonce, the props worked out later, shared props, redirects, downloads and events on a page, error pages, Vite, and Inertia's DevTools.

  4. Server-side rendering

    pages rendered on the server for a first visit, by Node beside the app, and package ssr.

  5. Forms and sessions

    validation, rules of the app's own, forms that check each field as it's left, flash messages, sessions, in their cookies or kept on the server, and CSRF.

  6. Languages

    what tug and the app say, in the request's language, from a file for each, package lang, and tug lang.

  7. Files

    uploads, checked by their size and what they are, kept on the app's disk or in S3 with package storage, and links to them, public or signed.

  8. Accounts

    the auth starter, in SQLite, Postgres or MySQL, its API tokens and notifications, and packages auth and mail, with copies, files and a link to unsubscribe in one click.

  9. Migrations

    the database's tables, made and changed by files of SQL, each run once, in order, as the app starts, and by its migrate command, with tug migrate new for the next, and package migrate.

  10. Authorization

    what a user may do with a thing, package auth's abilities, a no as a 403 that says why, and what a page's user may do in its props.

  11. Encryption

    the app's key, what tug encrypts and signs with it, package crypt, for the app's own values, and rotating the key.

  12. Background jobs

    package queue, for work that outlasts the request, and runs again when it fails, runs on a schedule, in a time zone too, waits once however often it's asked for, or so many at once or a second, on every instance, and says when it has failed for good, and how each run went.

  13. Cache

    package cache, for what's slow to work out, kept where every instance of the app finds it, and locks for what mustn't run twice at once.

  14. Broadcasting

    package broadcast, events on channels, carried by the app's database to the pages open on every instance, which reload what changed.

  15. TypeScript

    the types tug gen writes from the Go.

  16. Testing

    package tugtest, Inertia's client for Go's tests of an app's pages, forms, uploads and logins, mailtest, for its mail, and its events.

  17. Deployment

    one binary, the Dockerfile, the environment, HTTPS served by the app itself, with certificates from Let's Encrypt, the headers that say what a browser may do with the app's pages, a Content-Security-Policy among them, the logs, and metrics.

  18. The CLI

    tug new, tug dev, tug gen, tug routes, tug lang, tug migrate, tug build and tug key, in full.

A tugboat is small, and moves ships many times its size.

Make your first app
tug