Help us shape the future of Go! Take the 2026 Go Developer Survey

tug

package module
v0.28.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 43 Imported by: 0

README

tug

A web framework for Go apps whose frontend is Inertia.js: React pages rendered with props straight from Go handlers, with no API in between, and the whole app shipped as one binary.

The name: a tugboat is small, and moves ships many times its size.

Status: early. v0.28.0 is the latest release: the framework, its CLI, background jobs, on a schedule in any time zone too, pushed in the app's own transactions, and so many at once or a second across the instances, server-side rendering, tests of an app's pages, uploads kept on the app's disk or in S3, limits on routes and on guessing passwords, counted across the app's instances, the client's address behind its proxies, signed links, what tug and the app say in the request's language, lists in pages, the app's own commands, a cache and locks across the instances, mail with copies, files and a link to unsubscribe, downloads, streams and server-sent events, the app's own values encrypted, events broadcast to the pages open on every instance, what a user may do with a thing, headers that say what a browser may do with a page, a Content-Security-Policy that runs its own scripts alone, the database's tables made and changed by files of SQL, each run once, and starters in React, Vue or Svelte, one with accounts, from registering to two-factor logins, passkeys, a profile photo, API tokens, admins, and notifications in the app and by mail, in SQLite, Postgres or MySQL. The guide covers all of it, and docs/roadmap.md has what's next.

go install github.com/cuonggt/tug/cmd/tug@latest
tug new blog                                        # a new app, in ./blog; tug new -auth blog for accounts
cd blog
tug dev                                             # http://localhost:8080, rebuilt and reloaded as it changes
type PostsIndexProps struct {
	Posts []Post                   `json:"posts"`
	Stats inertia.DeferProp[Stats] `json:"stats"` // fetched after the page shows
}

var PostsIndex = tug.Page[PostsIndexProps]("Posts/Index") // resources/js/pages/Posts/Index.tsx

func index(c *tug.Ctx) error {
	return PostsIndex.Render(c, PostsIndexProps{
		Posts: store.List(),
		Stats: inertia.Defer(store.Count),
	})
}

type PostInput struct {
	Title string `json:"title" validate:"required,max=80"`
	Body  string `json:"body" validate:"required"`
}

func create(c *tug.Ctx) error {
	var in PostInput
	if err := c.BindValid(&in); err != nil {
		return err // back to the form with its errors; a field being filled in is checked here too
	}
	post := store.Add(in)
	c.Flash("success", "Post created") // usePage().flash on the next page
	return c.RedirectRoute("posts.show", post.ID)
}

examples/inertia is the whole app: React pages, a deferred prop, forms that check each field as it's left, flash messages, posts that show in every browser open as they're made, the Vite dev server with hot reload, and the frontend embedded in the binary for production. Handlers that aren't pages look like this:

func main() {
	app := tug.New() // ADDR or PORT, and APP_DEBUG, from the environment
	app.Use(middleware.RequestID(), middleware.Logger(), middleware.Recover(), middleware.CSRF())

	app.Get("/posts/{id}", showPost).Name("posts.show")

	if err := app.Run(); err != nil { // graceful shutdown on SIGINT and SIGTERM
		log.Fatal(err)
	}
}

func showPost(c *tug.Ctx) error {
	var in struct {
		ID int64 `path:"id"`
	}
	if err := c.Bind(&in); err != nil {
		return err // /posts/abc is a 404
	}
	post, ok := posts[in.ID]
	if !ok {
		return tug.NewHTTPError(http.StatusNotFound, "post not found")
	}
	return c.JSON(http.StatusOK, post)
}

examples/api is a complete JSON API.

What's here

  • The CLI, tug: tug new makes an app, ready to run, with a .env and a fresh APP_KEY, and a frontend of React, or with -vue or -svelte, of Vue or Svelte. tug dev runs it with Vite: Go is rebuilt and restarted as it changes, and the browser reloaded. tug gen writes the TypeScript of each page's props and of the named routes, with a typed route(), so the frontend is checked against the Go. tug build makes one static binary with the frontend in it, and the app comes with a Dockerfile for a distroless image.
  • Inertia pages (package inertia), to the whole v3 protocol: a first visit gets HTML with the page object, later visits get JSON. tug.Page[Props] ties a component to the props it takes, and props nest at any depth.
    • Loading: Lazy, Optional, Always and Defer props, worked out concurrently. Defer(...).Rescue() turns a failure into Inertia's rescue slot rather than a failed page.
    • Merging: Merge props, prepended or deep-merged, and matched on a key. Scroll pages a list for <InfiniteScroll>.
    • Pagination: tug.Paginate is the page of a list ?page asks for, from the app's own count and query, with where it sits and a pager's links, under the keys Laravel's paginators write; SimplePaginate without a count, and CursorPaginate by cursor. Each feeds Scroll too.
    • Once: Once props stay on the client until they expire.
    • Partial reloads reach nested props by path.
    • Redirects: a browser running an old build reloads. A 302 after a PUT, PATCH or DELETE becomes a 303, and a redirect to a #fragment keeps it.
    • Error pages: errors show as a page (Config.ErrorPage) with their own status.
    • Empty lists: nil slices go out as [], never null.
  • Server-side rendering (package ssr), when an app wants it, as tug new -ssr makes one: a first visit's page comes with its HTML, for search engines and pages that show before their scripts run, rendered by Inertia's own SSR in Node, which the app runs beside it, from the SSR build it embeds. Without Node, pages render in the browser, as they would anyway.
  • Forms: c.BindValid binds and checks a request by validate tags (package validate, go-playground/validator's rules, and the app's own, with validate.Rule) and checks of the handler's own. A form that doesn't validate goes back with its errors in the errors prop, under an error bag when the form names one; an API client gets a 422. A message names a field as a person reads it, "first name is required", by its label tag or its key made into words. Precognition, a form checking each field as it's left, is answered without running the rest of the handler.
  • Languages (package lang): what tug says to a person, a form's errors, a 429, an error page's status, and what the app says with c.T, in the request's language: the app's choice, or else the browser's Accept-Language, from a JSON file for each language, keyed by the English, as Laravel's lang/vi.json is, with plurals by each language's rules. tug lang vi writes the file, with every text the app says.
  • Encryption (package crypt): the app's own values, as a token for another service kept in a column, sealed with AES-256-GCM, under a key for their purpose alone, derived from APP_KEY, bound to their row if the app likes, and moved to a new key after a rotation, as tug's own are. tug key makes a key.
  • Sessions (package session): in an encrypted cookie, keyed by APP_KEY, with flash data. c.Flash reaches the next page shown, after a redirect or not, and only that one.
  • Files (package storage): uploads, from an Inertia form with its progress, kept in a directory the app serves, or in S3 or any service that speaks its API, Cloudflare R2 and MinIO among them, with S3's signatures on the standard library. validate checks an upload's size and what it is, from its first bytes rather than its name, and a page gets a link to it, public or signed until it expires. What's served can't run as the app: its type is its bytes', and anything but an image is a download.
  • Accounts: tug new -auth makes an app where people register and verify their email, log in, with a code from an authenticator app too once they turn that on, or with a passkey and no password at all, reset a forgotten password by email, change their profile and photo, password and appearance in settings, and make API tokens, for a script or another service to call the app's API with, hear of each change to their account that could hand it to someone else, at once in the app, by its bell, and by mail, the old email too, and where admins, whom a command makes, have a page of the jobs that failed, with the users in SQLite, or with -postgres or -mysql, in Postgres or MySQL, and a frontend of Tailwind and shadcn's components, as Laravel's starter kits have, in React, Vue or Svelte. Its handlers are the app's own code, on package auth, which has the parts where a slip is a security hole: argon2id password hashes, logins that end when the password changes, signed tokens for reset and verification links, two-factor codes and recovery codes kept encrypted, passkeys, WebAuthn's checks on the standard library, asking for the password again, API tokens kept as their hashes, throttles on guessing, whose counts every instance of the app shares, and abilities, what a user may do with a thing, whose no is a 403 that says why.
  • Migrations (package migrate): the database's tables, made and changed by files of SQL, named for when they were made, which tug migrate new writes, each run once, in order, as the app starts, and by its binary's migrate command, which lists them and undoes the last. What ran is kept with a hash of its SQL, so a file changed after it ran is caught, and instances starting at once take turns. The auth starter's tables are its migrations, in SQLite, Postgres or MySQL.
  • Mail (package mail): through an SMTP server, or in development written out where tug dev shows it, links and all, with copies, a Bcc no one sees, replies to another address, files, and a link to unsubscribe in one click, as Gmail and Yahoo ask of mail sent in bulk. mailtest.Outbox keeps what an app sends, for its tests, which read it in order, or by who it went to.
  • Background jobs (package queue): work a request starts and doesn't wait for, such as a mail, kept by a store such as a table in the app's database, so a failure or a restart doesn't lose it. A job that fails runs again after a wait that grows, and the workers run beside the server with app.Go, finishing what they have as the app stops. Jobs run on a schedule too, with Every or a cron expression, on UTC's clock or a time zone's, through its daylight saving as cron goes, once across all instances. A unique kind keeps one job waiting for each value, however often it's pushed, at the latest push's time if it likes, and runs them one at a time if it likes. A kind runs so many at once at most, or starts so many a second, counted on all the instances, and says, with OnFail, when a job has failed for good. A job pushed in the app's own transaction is kept with what else it writes, or not at all. The auth starter sends its mail this way, with its jobs in its database, where on Postgres or MySQL the claims of several instances skip each other's jobs rather than wait for them, and its binary lists the jobs that failed for good, and runs them again: ./blog jobs.
  • Cache (package cache): what's slow to work out, kept for a while where every instance of the app finds it, such as a table in its database, as the auth starter has, with cache.Remember, which works a value out once however many ask for it at once, and locks that hold across the instances, for an import or a command that mustn't run twice at once.
  • Broadcasting (package broadcast): events on channels, as posts or users.42, to the pages that follow them on every instance of the app, through c.Events, carried by its database, as the auth starter has it: Postgres's NOTIFY, or a table in SQLite and MySQL, and in the transaction that makes the change, or not at all. A post made in one browser shows in the others, and the auth starter's page that asks to verify the email moves on once the link is followed on the phone.
  • Tests (package tugtest): Inertia's client, for Go's tests of an app's pages, which need no browser or frontend build. Its visits keep the cookies the app sets and follow its redirects, upload files as the browser does, and a page's props read into the struct its tug.Page declares: tugtest.Props(r, Dashboard).User. The starters' tests use it.
  • Vite (package vite): tags from the dev server while it runs, with the React refresh preamble, and from the build's manifest otherwise, with CSS and preloads, the scripts with the page's nonce. The built files are served, and cached for a year.
  • Routing on net/http's ServeMux: method routes, groups with their own middleware, and named routes with app.URL("posts.show", 42). Paths match exactly, so / is only the home page, and {name...} takes everything under a path. A trailing slash redirects to the route without it, and a wrong method is a 405 with Allow. Links that leave the app, as in mail, are whole, from APP_URL, never the request's Host, and app.SignedURL makes ones only the app could have, until they expire, for a route tug.Signed wraps. tug.Limit puts a limit on a route, by address or user: a 429, with Retry-After, past it.
  • Handlers return errors. tug.NewHTTPError(404, "post not found") picks the status. Any other error is a 500 whose details stay in the log, or show in the response with APP_DEBUG=true. A panic is a 500 with its stack in the log. Errors are JSON for a client that asks for JSON.
  • Files, downloads and streams: c.File sends one of the app's files, with ranges, c.Download a file to save, its name however it's written, Vietnamese, quotes and all, c.StreamDownload a CSV made a row at a time, and c.Events server-sent events, for a page to follow work as it goes, ended as the app shuts down.
  • c.Bind fills a struct from path values, the query, and a JSON or form body, files included, by struct tags. A value that doesn't parse is a 400 that names the field, "age must be a whole number"; a bad path value is a 404.
  • Middleware is func(http.Handler) http.Handler: RequestID, Logger (through slog), Recover, CSRF, which is Go's http.CrossOriginProtection, so there are no tokens, CORS, for an API other sites' pages call, TrustProxies, which reads the client's address past the app's load balancers, from the end of X-Forwarded-For, for c.IP(), Headers, which says what a browser may do with a response, as be framed by another site, and holds it to HTTPS, and CSP, a Content-Security-Policy that runs only the scripts that carry a nonce made for the response, which the root template gives Vite's tags and its own, and logs what browsers report it blocked.
  • app.Run listens on ADDR or PORT, and on SIGTERM stops taking connections and lets the requests in flight finish. Given one of the app's commands, as ./blog jobs, which app.Command adds, it runs that in place of serving, in the app as main made it.

tug needs Go 1.26. Its dependencies are go-playground/validator, for package validate, and golang.org/x/crypto, for argon2id in package auth; the rest is the standard library.

Development

go test -race ./...
go test -run '^$' -bench . -benchmem .    # tug next to ServeMux alone
ADDR=127.0.0.1:8080 go run ./examples/api

The Inertia example, from examples/inertia:

npm install
npm run dev                               # the Vite dev server
ADDR=127.0.0.1:8080 go run .              # in another terminal
npm run build && npx playwright test      # end to end, in Chrome

Documentation

Overview

Package tug is a web framework for Go apps whose frontend is Inertia.js.

This is the HTTP core the rest builds on. Routing is net/http's ServeMux, handlers return errors, and middleware has net/http's own shape, func(http.Handler) http.Handler, so anything written for net/http plugs in:

app := tug.New()
app.Use(middleware.RequestID(), middleware.Logger(), middleware.Recover())
app.Get("/posts/{id}", showPost).Name("posts.show")
if err := app.Run(); err != nil {
	log.Fatal(err)
}

A handler gets a *Ctx for its request and returns an error. The app's ErrorHandler turns that error into the response, so a handler says what went wrong, as NewHTTPError(http.StatusNotFound), and never writes an error page itself.

The rest is in packages of their own, which don't import this one: inertia renders pages, session keeps sessions in a cookie, validate checks structs, vite puts a Vite build into pages, auth has the parts of accounts where a slip is a security hole, mail sends mail, and middleware has RequestID, Logger, Recover and CSRF. The guide is in the repository's docs directory: https://github.com/cuonggt/tug/tree/main/docs.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultErrorHandler

func DefaultErrorHandler(c *Ctx, err error)

DefaultErrorHandler answers an error from a handler. validate.Errors go back to the form they came from, as Inertia expects, or to an API client as a 422 (see BindValid). An *HTTPError chooses the status and message. So does an error that says its status with a StatusCode method, as package auth's no does, with its own Error as the message under 500, and the status's text from 500 up, as its Error may not be the client's. Anything else is a 500 that keeps its details in the log, unless Config.Debug is on. Server errors are logged through slog.Default(), with the stack for a panic.

The body is JSON, {"message": "..."}, when the request's Accept header asks for JSON first, and plain text otherwise.

func Generating added in v0.12.0

func Generating() bool

Generating reports whether tug gen started the app, to learn its pages and routes: Run writes their TypeScript then, and returns without serving. main runs up to Run either way, and what only serving needs, such as a database, it can leave alone while Generating says so, so that tug gen, and tug new and tug build, which run it, need nothing else running.

Types

type App

type App struct {
	*Router
	// contains filtered or unexported fields
}

App is a web application: its routes, middleware and settings. It is an http.Handler, so an http.Server set up by hand can serve it too.

func New

func New(config ...Config) *App

New returns an App. Without a Config it reads one from the environment (ConfigFromEnv); a Config passed in is used as it is. It panics on a Config.URL that isn't the app's address, as a mistyped APP_URL should stop the app as it starts rather than make links that go nowhere.

func (*App) AbsoluteURL added in v0.14.0

func (a *App) AbsoluteURL(name string, params ...any) (string, error)

AbsoluteURL builds the whole link to the route named name, as URL builds its path, from Config.URL, the app's own address: AbsoluteURL("posts.show", 42) is "https://example.com/posts/42". It's for a link that leaves the app, as in mail; within the app, the path is enough. Without Config.URL it's an error.

func (*App) Command added in v0.17.0

func (a *App) Command(name, summary string, run func(ctx context.Context, args []string) error)

Command adds a command to the app's binary, for what it does besides serving: a fix to its data, an import, a user made an admin. Run, given the command's name as its first argument, runs it in place of serving, with the arguments after the name and a context canceled on SIGINT or SIGTERM, and returns its error: ./blog users:admin ann@example.com runs users:admin with ann@example.com. ./blog help lists the app's commands, with their summaries, and a name that isn't one is an error that says so.

app.Command("users:admin", "make a user an admin: users:admin <email>", func(ctx context.Context, args []string) error {
	if len(args) != 1 {
		return errors.New("users:admin takes the user's email")
	}
	return users.MakeAdmin(ctx, args[0])
})

A command runs in the app main made, with its routes, for the links it makes, its queue, for the jobs it pushes, and its database, but neither the server nor what Go runs: a job it pushes runs on the instances that serve. Its arguments are its own, to read with package flag or by hand. An app without commands serves whatever its arguments, and under tug gen, Run writes the types, whatever they are.

Command panics on a name that's taken, help, which lists them, or one that isn't a word of letters, digits and :-_, and once the app is serving.

func (*App) Go added in v0.3.0

func (a *App) Go(fn func(ctx context.Context) error)

Go runs fn beside the server, for work that isn't a request, such as a job queue's workers: Run and Serve start it as they start serving, with a context that's done as the app shuts down, and wait for it to return. An error it returns before then shuts the app down, and Serve returns it: an app whose jobs have stopped shouldn't carry on as though they hadn't. ServeHTTP, as tests call it, starts nothing, and nor does tug gen.

Go panics once the app is serving.

func (*App) Locale added in v0.15.0

func (a *App) Locale(r *http.Request) string

Locale returns the language of r: the one Config.Locale chooses, or else the best of Config.Lang's for the browser's Accept-Language, or else the app's default, as "vi" or "pt-BR". It's English without Config.Lang. For a handler, Ctx.Locale is the same; this is for code that has the request alone, as a props function shared with every page.

func (*App) Run

func (a *App) Run() error

Run serves on Config.Addr until SIGINT or SIGTERM, then shuts down gracefully: it stops taking connections, waits up to Config.ShutdownTimeout for the requests in flight to finish, and waits for what Go runs to return.

Run is also where tug gen learns about the app: started by it, with TUG_GEN set to a file, Run writes the TypeScript for the app's pages and named routes there, and returns without serving. And for an app with commands, given one, Run runs it in place of serving: see Command.

func (*App) Serve

func (a *App) Serve(ctx context.Context, ln net.Listener) error

Serve serves on ln until ctx is done, then shuts down as Run does. It closes ln.

func (*App) ServeHTTP

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP answers a request. The first request fixes the routes and middleware: adding either after it panics.

func (*App) SignedURL added in v0.14.0

func (a *App) SignedURL(name string, expires time.Time, params ...any) (string, error)

SignedURL builds the whole link to the route named name, as AbsoluteURL does, signed with the app's key, Config.Keys, until expires: a link that only the app can have made, for a route that Signed wraps, such as an invitation's, mailed to whoever is invited.

The signature is over the link's path and its expiry, which the query carries as expires and signature. A link works until it expires, as often as it's followed: what must happen once, as an invitation accepted, is the app's to record.

func (*App) URL

func (a *App) URL(name string, params ...any) (string, error)

URL builds the path of the route named name, filling its wildcards with params in order: URL("posts.show", 42) is "/posts/42". Values are escaped, except for the slashes in a {name...} wildcard's value.

type BindError

type BindError struct {
	Field  string // the key the value came under: a form or query key, JSON path or path wildcard
	Reason string // what the value has to be, as "must be a whole number"
	Err    error  // the parse error underneath
	// contains filtered or unexported fields
}

BindError is a request value that doesn't fit the field it was bound to, such as "abc" for an int. Its message is for the person who sent the value: "age must be a whole number".

func (*BindError) Error

func (e *BindError) Error() string

func (*BindError) Unwrap

func (e *BindError) Unwrap() error

type Config

type Config struct {
	// Addr is where Run listens. Default ":8080".
	Addr string

	// Debug puts error details and panic stacks into 500 responses. It is
	// for development: in production they belong in the log only.
	Debug bool

	// ErrorHandler answers the errors handlers return, and the 404s and
	// 405s of requests no route takes. Default DefaultErrorHandler.
	ErrorHandler func(c *Ctx, err error)

	// BodyLimit is the largest request body Bind reads, in bytes; a larger
	// one is a 413. Default 32 MiB.
	BodyLimit int64

	// ShutdownTimeout is how long Run waits for requests in flight after
	// SIGINT or SIGTERM. Default 10 seconds.
	ShutdownTimeout time.Duration

	// Inertia renders the app's pages, for Ctx.Inertia and Page.Render.
	// With it set, every request also goes through its Middleware, inside
	// the App's own middleware.
	Inertia *inertia.Inertia

	// ErrorPage is the Inertia page component that errors are shown with,
	// such as "Error", with the props status and message: for a 404, a
	// 403, or a 500 in production. API clients still get JSON, and with
	// Debug on a 500 still shows its details. Without it, errors are plain
	// text, which Inertia's client shows in a dialog.
	ErrorPage string

	// Session keeps each visitor's session, for Ctx.Session. It's what
	// carries flash data and validation errors from a request to the page
	// after it. With it set, every request goes through its Middleware,
	// inside the App's own middleware.
	Session *session.Store

	// URL is the app's own address, a scheme and a host, as
	// https://example.com: what the links AbsoluteURL and SignedURL make
	// begin with, for links that leave the app, as in mail. It's never the
	// request's Host, which can name any site. New panics on one that isn't
	// an address.
	URL string

	// Keys sign the links SignedURL makes, and check them: the first signs,
	// and each of them checks, so the links made with a key being rotated
	// out still work until it's dropped. session.KeysFromEnv reads them from
	// APP_KEY and APP_PREVIOUS_KEYS.
	Keys [][]byte

	// Lang is the app's languages, which lang.Load reads: what tug says to
	// a person, a form's errors, a 429 or an error page's status, is said
	// in the request's language, as Ctx.Locale picks it, and so is what the
	// app says with Ctx.T. Without it, everything is in English.
	Lang *lang.Catalog

	// Locale is the language the app has chosen for a request, as a user's
	// choice, kept in their session or their account: one of Lang's, as
	// lang.Catalog.Find finds it, or "" to leave it to the browser's
	// Accept-Language. For a handler, the ErrorHandler and the pages it
	// renders, it runs inside the session's middleware, so it can read the
	// session; in the App's own middleware, there's none yet.
	Locale func(r *http.Request) string
}

Config is how an App behaves. Every field has a default, so the zero value works.

func ConfigFromEnv

func ConfigFromEnv() Config

ConfigFromEnv reads the settings a deployment sets: ADDR, or PORT as platforms such as Cloud Run and Fly.io set it, APP_DEBUG, and APP_URL.

type Ctx

type Ctx struct {
	// contains filtered or unexported fields
}

Ctx is a request and its response, as a handler sees them. It belongs to its request: don't keep it after the handler returns.

func (*Ctx) AbsoluteURL added in v0.14.0

func (c *Ctx) AbsoluteURL(name string, params ...any) (string, error)

AbsoluteURL builds the whole link to a named route, from the app's address, as App.AbsoluteURL does.

func (*Ctx) Bind

func (c *Ctx) Bind(dst any) error

Bind fills the struct dst points to from the request. Each field's tags say where its value comes from:

type UpdatePost struct {
	ID    int64  `path:"id"`     // the {id} in "/posts/{id}"
	Draft bool   `query:"draft"` // ?draft=1
	Title string `json:"title"`  // the body
}

The body is read by its Content-Type. JSON goes through encoding/json and its json tags. A form, urlencoded or multipart, fills a field by its form tag, or else its json tag's name, or else its name; a key that repeats, or ends in "[]", fills a slice. Files go to fields of type *multipart.FileHeader or []*multipart.FileHeader. An empty form value leaves its field alone, as an empty input means no value rather than zero.

Inertia's <Form> sends a form as JSON, with every value a string: "on" from a checkbox, "42" from a number input, "2026-09-25" from a date input, and "" from one left empty. So a JSON string where a bool, a number or a time.Time goes is read as a form's value is, and "" leaves its field alone; a JSON true or 42 binds as it is.

Query values are bound after the body, and path values last, so a body can't overwrite the ID in the URL.

A value that doesn't parse is a 400, or a 404 when it's a path value, since /posts/abc is a page that isn't there. The error wraps a *BindError naming the field. The fields after it are still bound, so that checking them, as BindValid does, finds what's really wrong with them rather than what's missing. A body over Config.BodyLimit is a 413, and a body Bind can't read is a 415. The messages are in the request's language (see Ctx.Locale), and name a field by its label tag, or else by its key made into words.

func (*Ctx) BindValid

func (c *Ctx) BindValid(dst any, checks ...func(validate.Errors)) error

BindValid binds the request into dst, as Bind does, then checks dst against its validate tags (see package validate) and against checks, which can add errors of their own, such as a title that's taken:

var in PostInput
err := c.BindValid(&in, func(errs validate.Errors) {
	if posts.TitleTaken(in.Title) {
		errs.Add("title", "title is taken")
	}
})
if err != nil {
	return err
}

When something's wrong it returns validate.Errors, which the ErrorHandler answers: a form goes back where it came from, with the errors, and an API client gets a 422. A value Bind can't parse is one of the errors: "age must be a whole number". The messages are in the request's language (see Ctx.Locale); the checks' are theirs to say, with Ctx.T.

A Precognition request, sent by a form checking its fields as they're filled in, is answered here with a 204 or a 422, and BindValid returns an error, so that the handler stops before it changes anything: return the error as it is.

func (*Ctx) Blob

func (c *Ctx) Blob(code int, contentType string, b []byte) error

Blob writes b as the body, with the given content type.

func (*Ctx) Choice added in v0.15.0

func (c *Ctx) Choice(text string, n int, args ...any) string

Choice says text for a count in the request's language, as lang.Words.Choice does: c.Choice(":count post deleted|:count posts deleted", n).

func (*Ctx) ClearHistory

func (c *Ctx) ClearHistory()

ClearHistory has the next page shown tell the client to clear the history it has encrypted, as signing out should. See inertia.Config.EncryptHistory.

func (*Ctx) Context

func (c *Ctx) Context() context.Context

Context returns the request's context, which is canceled when the client goes away.

func (*Ctx) Download added in v0.20.0

func (c *Ctx) Download(name string, content io.Reader) error

Download sends content as a file to save, as name: an invoice made as a PDF, bytes.NewReader(pdf), or a file a disk's Open reads, under the name it was uploaded with. The browser saves it rather than show it, in Content-Disposition's words, which say the name however it's written: in Vietnamese, with quotes, or with a line break that can't end the header. What's before the name's last slash or backslash is dropped, as the browser saves a name, not a path.

Its type is its name's extension's, or else what its first bytes say. Content that seeks, as a file or a bytes.Reader does, goes through http.ServeContent, with ranges, and, when it has a Stat, as a file does, its time for If-Modified-Since. Other content goes as it's read, as Stream's body does. Download reads content to its end, and leaves it open: closing it is the caller's.

func (*Ctx) Events added in v0.20.0

func (c *Ctx) Events(fn func(ctx context.Context, send func(Event) error) error) error

Events sends a stream of server-sent events, which the browser's EventSource reads, as fn sends them: an import's progress, say, or a change a page should show, which it reloads its props for with Inertia's router.reload. The stream lasts as long as fn does: fn's context is done when the client goes away, and when the app shuts down, as Serve's Shutdown waits for the requests in flight, which a stream would hold on to. fn returns then; send fails once it's done.

app.Get("/imports/{id}/progress", func(c *tug.Ctx) error {
	return c.Events(func(ctx context.Context, send func(tug.Event) error) error {
		for p := range imports.progress(ctx, c.Param("id")) {
			if err := send(tug.Event{Name: "progress", Data: p}); err != nil {
				return err
			}
		}
		return nil
	})
})

An EventSource connects again when a stream ends, the app's shutdown's among them, after a few seconds, with the last event's ID in Last-Event-ID, for the app to go on from: a page that has what it waited for closes it. A quiet stream sends a comment every 20 seconds, which keeps the proxies in front of the app from closing it.

fn's error is Events', for the ErrorHandler, which logs it, as the response has started, unless the client went away or the app is shutting down, which is no failure. An event whose Name or ID has a line break, or whose Data JSON can't hold, is send's error, and isn't sent.

func (*Ctx) File added in v0.20.0

func (c *Ctx) File(path string) error

File sends the file at path, one of the app's own, such as a report it made, shown as its type says, HTML as a page: http.ServeContent sends it, with ranges, so a download that broke off goes on where it stopped, and If-Modified-Since. A file that isn't there, or a directory, is a 404 for the ErrorHandler.

path is the app's, never a request's, which could name any file: a file a request names is FileFS's, from a directory, as os.DirFS or an os.Root has it, which keeps it inside. A file someone uploaded goes by its disk's route, which sends it as what its bytes are, or by Download, to be saved.

func (*Ctx) FileFS added in v0.20.0

func (c *Ctx) FileFS(fsys fs.FS, name string) error

FileFS sends the file name in fsys, as File sends one: a file embedded in the app, or one of a directory a request names a file of, as FileFS(os.DirFS("docs"), c.Param("name")), where a name that would leave the directory, as "../app.db", isn't there.

func (*Ctx) Flash

func (c *Ctx) Flash(key string, value any)

Flash adds flash data for the next page shown, whether this request renders it or redirects to it: a message such as "Post created". The client reads it as usePage().flash, shows it once, and doesn't keep it in history. Surviving a redirect takes Config.Session.

func (*Ctx) HTML

func (c *Ctx) HTML(code int, html string) error

HTML writes html as a page, exactly as given: anything from a user in it must already be escaped.

func (*Ctx) IP added in v0.14.0

func (c *Ctx) IP() string

IP returns the address the request came from, without its port, as "203.0.113.9": a key for Limit that counts by address. Behind a proxy it's the proxy's, unless middleware.TrustProxies names the proxy, and reads the client's past it.

func (*Ctx) Inertia

func (c *Ctx) Inertia(component string, props any) error

Inertia renders an Inertia page component with props: an HTML page for a first visit, and the page object as JSON for a visit from Inertia's client. props is a struct or a map; see inertia.Inertia.Render. The app needs Config.Inertia.

func (*Ctx) JSON

func (c *Ctx) JSON(code int, v any) error

JSON writes v as JSON.

func (*Ctx) Locale added in v0.15.0

func (c *Ctx) Locale() string

Locale returns the request's language, as App.Locale picks it: the one the app chose for it with Config.Locale, or else the browser's, of the app's languages, or else the app's default.

func (*Ctx) Location

func (c *Ctx) Location(url string) error

Location sends the client to url with a full page load, which is how an Inertia app leaves for another site: see inertia.Location.

func (*Ctx) NoContent

func (c *Ctx) NoContent(code int) error

NoContent writes a status and no body, such as 204.

func (*Ctx) Param

func (c *Ctx) Param(name string) string

Param returns the value of the path wildcard name: Param("id") is "42" for /posts/42 on a "/posts/{id}" route.

func (*Ctx) PreserveFragment

func (c *Ctx) PreserveFragment()

PreserveFragment has the next page shown tell the client to keep the #fragment of the visit that led to it, across the redirect: a comment sent from /posts/1#comments comes back to the comments.

func (*Ctx) Query

func (c *Ctx) Query(name string) string

Query returns the first value of the query parameter name.

func (*Ctx) Redirect

func (c *Ctx) Redirect(to string) error

Redirect sends the client to another URL: with 302 Found after a GET or HEAD, and 303 See Other after any other method. A 303 makes browsers, and Inertia, follow a PUT, PATCH or DELETE with a GET, where a 302 may repeat the method.

func (*Ctx) RedirectBack added in v0.2.0

func (c *Ctx) RedirectBack() error

RedirectBack redirects to the page the request came from, by its Referer, when that's a page of this app, and to "/" otherwise: a Referer can name any site. It's for a form that more than one page has, which goes back to whichever it was sent from.

func (*Ctx) RedirectRoute

func (c *Ctx) RedirectRoute(name string, params ...any) error

RedirectRoute redirects to the named route, filling its wildcards as App.URL does.

func (*Ctx) Request

func (c *Ctx) Request() *http.Request

Request returns the request.

func (*Ctx) Response

func (c *Ctx) Response() http.ResponseWriter

Response returns the response writer.

func (*Ctx) Session

func (c *Ctx) Session() *session.Session

Session returns the request's session, or nil when the app has no Config.Session.

func (*Ctx) SignedURL added in v0.14.0

func (c *Ctx) SignedURL(name string, expires time.Time, params ...any) (string, error)

SignedURL builds a signed link to a named route, as App.SignedURL does.

func (*Ctx) Stream added in v0.20.0

func (c *Ctx) Stream(contentType string, write func(w io.Writer) error) error

Stream sends a body as write makes it, of the type contentType, such as "text/csv", for a body too long to hold, made as it's read: every post as CSV, a row at a time, from rows as a query returns them. Each write goes to the client as it's made, flushed, so a client that reads as it comes, as a fetch of lines does, gets each one; a writer of many small writes wraps w in a bufio.Writer, as csv.Writer does itself.

write's error before its first write is Stream's, for the ErrorHandler, as any handler's is. Once the body has started, there's no error page to show: the error goes to the log, and the connection is cut, with http.ErrAbortHandler, so the client sees the body end before its time, where a browser would save half a file as the whole.

func (*Ctx) StreamDownload added in v0.20.0

func (c *Ctx) StreamDownload(name, contentType string, write func(w io.Writer) error) error

StreamDownload sends a body as write makes it, as Stream does, as a file to save, as name, which Download writes as it writes its own:

return c.StreamDownload("posts.csv", "text/csv; charset=utf-8", func(w io.Writer) error {
	out := csv.NewWriter(w)
	...
	out.Flush()
	return out.Error()
})

func (*Ctx) String

func (c *Ctx) String(code int, s string) error

String writes s as plain text.

func (*Ctx) T added in v0.15.0

func (c *Ctx) T(text string, args ...any) string

T says text in the request's language, as lang.Words.T does, from Config.Lang: c.T("Welcome back, :name", "name", user.Name), for a flash message. A text the language has no words for says itself, in English.

func (*Ctx) URL

func (c *Ctx) URL(name string, params ...any) (string, error)

URL builds the path of a named route, as App.URL does.

func (*Ctx) Validate

func (c *Ctx) Validate(v any, checks ...func(validate.Errors)) error

Validate checks v as BindValid checks what it binds, for values that don't come straight from the request. It answers Precognition requests the same way.

func (*Ctx) Written

func (c *Ctx) Written() bool

Written reports whether the response has started. After that its status and headers are on their way and can't change.

type CursorPaginated added in v0.16.0

type CursorPaginated[T any] struct {
	Data        []T     `json:"data"`
	Path        string  `json:"path"`
	PerPage     int     `json:"per_page"`
	NextCursor  *string `json:"next_cursor"`
	NextPageURL *string `json:"next_page_url"`
	// contains filtered or unexported fields
}

CursorPaginated is a page of a list by cursor, as CursorPaginate makes it, under the keys Laravel's cursor paginator writes: the cursor of the page after it, and its link, null on the last.

func CursorPaginate added in v0.16.0

func CursorPaginate[T, C any](c *Ctx, perPage int, fetch func(after *C, limit int) ([]T, error), cursor func(item T) C, opts ...PageOption) (CursorPaginated[T], error)

CursorPaginate is one page of a list by cursor, for a list whose items come and go as it's read, as a feed's: fetch gets the items after the one at the cursor, nil for the first page, limit of them, one more than a page, to know whether there's a next; cursor is where an item sits in the query's order, the app's own value, such as its time and its ID:

posts, err := tug.CursorPaginate(c, 20,
	func(after *PostCursor, limit int) ([]Post, error) { return store.PostsAfter(after, limit) },
	func(p Post) PostCursor { return PostCursor{At: p.CreatedAt, ID: p.ID} })

The link to the next page carries the last item's cursor, as JSON, in base64url, under ?cursor. It isn't signed, as Laravel's aren't: it's only where to start, and one made up starts somewhere else within what the query lets the page see. One that doesn't decode is the first page. Cursors go forward: a page before one runs the query the other way, which is the app's to write.

func (CursorPaginated[T]) Paging added in v0.16.0

func (p CursorPaginated[T]) Paging() inertia.Paging

Paging is where the page sits, for inertia.Scroll: the cursor it was asked for with, and the next page's.

type ErrorPageProps

type ErrorPageProps struct {
	Status  int    `json:"status"`
	Message string `json:"message"`
}

ErrorPageProps are the props of Config.ErrorPage.

type Event added in v0.20.0

type Event struct {
	// Name is the event's type, which a page listens for with
	// addEventListener, as "progress". Without one, it's a message, which
	// onmessage gets.
	Name string

	// ID is the event's, which the browser sends back when it connects
	// again, in Last-Event-ID, for the app to go on from after it. Optional.
	ID string

	// Data is what the event carries, as JSON, which the page reads with
	// JSON.parse: a value encoding/json can encode, or nil for none.
	Data any
}

Event is one of the server-sent events Ctx.Events sends.

type HTTPError

type HTTPError struct {
	Code    int
	Message string
	Err     error
}

HTTPError is an error with a status. Return one from a handler to choose the response: NewHTTPError(http.StatusNotFound) is a 404.

Message is shown to the client, so it says what went wrong in their terms, and in their language, with Ctx.T; without one, the status's text is shown, in the request's language. Err is the cause, for the log; only Config.Debug shows it, in a server error's response.

func NewHTTPError

func NewHTTPError(code int, message ...string) *HTTPError

NewHTTPError returns an HTTPError with the given status. Without a message it says the status's text, such as "Not Found".

func (*HTTPError) Error

func (e *HTTPError) Error() string

func (*HTTPError) Unwrap

func (e *HTTPError) Unwrap() error

type HandlerFunc

type HandlerFunc func(c *Ctx) error

HandlerFunc handles a request. A returned error is answered by the app's ErrorHandler: return an *HTTPError to choose the status.

func Limit added in v0.13.0

func Limit(l Limiter, key func(c *Ctx) string, h HandlerFunc) HandlerFunc

Limit wraps h so that each request counts a try with l, by the key that key makes of it: the address it came from, Ctx.IP, which behind a proxy takes middleware.TrustProxies, or the user's ID. A request over the limit doesn't reach h: it's a 429 for the app's ErrorHandler, with Retry-After in seconds, so an Inertia visit gets the error page, and an API's client JSON. An error from l is as though h returned it.

searches := &auth.Throttle{Name: "searches", Max: 30, Window: time.Minute}
app.Get("/search", tug.Limit(searches, (*tug.Ctx).IP, search))

func Signed added in v0.14.0

func Signed(h HandlerFunc) HandlerFunc

Signed wraps h so that only a link SignedURL made reaches it, before it expires. Any other request is a 403 for the app's ErrorHandler, which says whether the link has expired or isn't one the app made: one that was changed, or has anything in its query besides expires and signature, which Bind would give the handler.

app.Get("/invitations/{id}", tug.Signed(acceptInvitation)).Name("invitations.accept")

func WrapHandler

func WrapHandler(h http.Handler) HandlerFunc

WrapHandler turns an http.Handler into a HandlerFunc, to put something written for net/http, such as http.FileServer, on a route.

type Limiter added in v0.13.0

type Limiter interface {
	Try(ctx context.Context, key string) (time.Duration, error)
}

A Limiter counts a try for a key, and says how long a key that has had its tries waits, or 0: Limit's one call, which *auth.Throttle has.

type Middleware

type Middleware = func(http.Handler) http.Handler

Middleware wraps a handler. It is net/http's own shape, so middleware written for net/http works as it is.

type PageLink struct {
	URL    *string `json:"url"`
	Label  string  `json:"label"`
	Active bool    `json:"active"` // it's the page shown
}

PageLink is a pager's link to a page, by its number, or its "...", which has no URL, for the pages it leaves out.

type PageOf

type PageOf[P any] struct {
	// contains filtered or unexported fields
}

PageOf is an Inertia page component, and the props it takes. Page declares one.

func Page

func Page[P any](component string) PageOf[P]

Page declares an Inertia page component, and the props it takes:

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: posts, Stats: inertia.Defer(countWords)})
}

Declared this way, a page can only be rendered with the props it takes, and tug gen writes their TypeScript, for the page component to take them too. A component declared twice with different props panics.

func (PageOf[P]) Component

func (p PageOf[P]) Component() string

Component returns the page's component name, as "Posts/Index".

func (PageOf[P]) Render

func (p PageOf[P]) Render(c *Ctx, props P) error

Render renders the page with props, as Ctx.Inertia does.

type PageOption added in v0.16.0

type PageOption func(*pageOptions)

A PageOption changes how Paginate, SimplePaginate and CursorPaginate read the page asked for, and link the others.

func PageName added in v0.16.0

func PageName(name string) PageOption

PageName names the query parameter that picks the page, "page" by default, and "cursor" by cursor: another is for a second list on one page, as "comments_page", whose links leave the first list's page as it is.

type Paginated added in v0.16.0

type Paginated[T any] struct {
	CurrentPage  int        `json:"current_page"`
	Data         []T        `json:"data"`
	FirstPageURL string     `json:"first_page_url"`
	From         *int       `json:"from"` // the first item's place in the list, from 1, or null for a page with none
	LastPage     int        `json:"last_page"`
	LastPageURL  string     `json:"last_page_url"`
	Links        []PageLink `json:"links"` // the pages around this one, for a pager
	NextPageURL  *string    `json:"next_page_url"`
	Path         string     `json:"path"`
	PerPage      int        `json:"per_page"`
	PrevPageURL  *string    `json:"prev_page_url"`
	To           *int       `json:"to"` // the last item's place, or null
	Total        int        `json:"total"`
	// contains filtered or unexported fields
}

Paginated is a page of a list in numbered pages, as Paginate makes it: its items, and where it sits among the pages, under the keys Laravel's paginators write it with as JSON, so that a pager written for theirs reads it. The links are paths, with the query the list was shown with.

func Paginate added in v0.16.0

func Paginate[T any](c *Ctx, perPage int, count func() (int, error), fetch func(limit, offset int) ([]T, error), opts ...PageOption) (Paginated[T], error)

Paginate is one page of a list in numbered pages of perPage items: the one ?page asks for. count says how many items the list has, and fetch gets a page of them, by the limit and offset it's given, as SQL's LIMIT and OFFSET take them: the query is the app's, so tug has no SQL.

posts, err := tug.Paginate(c, 20, store.CountPosts, store.Posts)

A page that isn't a number, or is under 1, is the first, as Laravel has it, and a page past the last is empty, not a 404, as a list can shrink between two visits: count runs first, so fetch isn't asked for a page that has no items. The page size is the app's alone: a client that could pick it would pick how much the database reads.

func (Paginated[T]) Paging added in v0.16.0

func (p Paginated[T]) Paging() inertia.Paging

Paging is where the page sits, for inertia.Scroll, so that one query feeds numbered pages and <InfiniteScroll> alike:

inertia.Scroll(func() ([]Post, inertia.Paging, error) {
	posts, err := tug.Paginate(c, 20, store.CountPosts, store.Posts)
	return posts.Data, posts.Paging(), err
})

type PanicError

type PanicError struct {
	Value any
	Stack []byte
}

PanicError is a handler's panic, recovered by the app and passed to its ErrorHandler like any other error, with the stack for the log.

func (*PanicError) Error

func (e *PanicError) Error() string

func (*PanicError) Unwrap

func (e *PanicError) Unwrap() error

Unwrap returns the panic's value when that was an error.

type Props

type Props = inertia.Props

Props are a page's props, for a page without a struct of its own.

type Route

type Route struct {
	// contains filtered or unexported fields
}

Route is a route added to a Router. Name it to build its URL.

func (*Route) Name

func (rt *Route) Name(name string) *Route

Name names the route, for App.URL and Ctx.RedirectRoute. A name belongs to one route: naming a second route "posts.show" panics.

func (*Route) String

func (rt *Route) String() string

String describes the route by its method and path, "GET /posts/{id}".

type Router

type Router struct {
	// contains filtered or unexported fields
}

Router adds routes. The App is the root Router, and Group makes one for a path prefix, with middleware of its own.

A path matches exactly: "/posts" is /posts and nothing under it, and "/" is only the home page. End a path with a {name...} wildcard to take everything under it. Beyond that, paths are net/http ServeMux patterns, with its wildcards and its rule that the more specific pattern wins.

func (*Router) Any

func (r *Router) Any(path string, h HandlerFunc, mw ...Middleware) *Route

Any adds a route for requests to path under any method.

func (*Router) Delete

func (r *Router) Delete(path string, h HandlerFunc, mw ...Middleware) *Route

Delete adds a route for DELETE requests to path.

func (*Router) Get

func (r *Router) Get(path string, h HandlerFunc, mw ...Middleware) *Route

Get adds a route for GET requests to path, which also takes HEAD.

func (*Router) Group

func (r *Router) Group(prefix string, mw ...Middleware) *Router

Group returns a Router for routes under prefix, such as "/admin", that run mw after the middleware of r. An empty prefix groups routes for the middleware alone.

func (*Router) Handle

func (r *Router) Handle(method, path string, h HandlerFunc, mw ...Middleware) *Route

Handle adds a route for method requests to path; an empty method takes them all. mw wraps this route alone, inside its groups' middleware.

Handle panics when path is not a valid pattern, or takes the same requests as a route already added, as ServeMux does: both are mistakes to fix before the app can start.

func (*Router) Options

func (r *Router) Options(path string, h HandlerFunc, mw ...Middleware) *Route

Options adds a route for OPTIONS requests to path.

func (*Router) Patch

func (r *Router) Patch(path string, h HandlerFunc, mw ...Middleware) *Route

Patch adds a route for PATCH requests to path.

func (*Router) Post

func (r *Router) Post(path string, h HandlerFunc, mw ...Middleware) *Route

Post adds a route for POST requests to path.

func (*Router) Put

func (r *Router) Put(path string, h HandlerFunc, mw ...Middleware) *Route

Put adds a route for PUT requests to path.

func (*Router) Use

func (r *Router) Use(mw ...Middleware)

Use adds middleware, which runs in the order added. On the App it wraps every request, including those no route takes, which suits logging and recovery. On a group it wraps the group's routes, inside the middleware of the groups around it, including routes added before the Use.

type SimplePaginated added in v0.16.0

type SimplePaginated[T any] struct {
	CurrentPage    int     `json:"current_page"`
	CurrentPageURL string  `json:"current_page_url"`
	Data           []T     `json:"data"`
	FirstPageURL   string  `json:"first_page_url"`
	From           *int    `json:"from"`
	NextPageURL    *string `json:"next_page_url"`
	Path           string  `json:"path"`
	PerPage        int     `json:"per_page"`
	PrevPageURL    *string `json:"prev_page_url"`
	To             *int    `json:"to"`
	// contains filtered or unexported fields
}

SimplePaginated is a page of a list in numbered pages without a count, as SimplePaginate makes it, under the keys Laravel's simple paginator writes: it knows whether there's a next page, and not how many there are.

func SimplePaginate added in v0.16.0

func SimplePaginate[T any](c *Ctx, perPage int, fetch func(limit, offset int) ([]T, error), opts ...PageOption) (SimplePaginated[T], error)

SimplePaginate is one page of a list in numbered pages, as Paginate is, for a list too long to count at each visit: fetch is asked for one item more than a page, to know whether there's a page after it.

func (SimplePaginated[T]) Paging added in v0.16.0

func (p SimplePaginated[T]) Paging() inertia.Paging

Paging is where the page sits, for inertia.Scroll.

Directories

Path Synopsis
Package auth has the parts of logging users in where a slip is a security hole, for an app that keeps its users where it likes:
Package auth has the parts of logging users in where a slip is a security hole, for an app that keeps its users where it likes:
passkeytest
Package passkeytest is a passkey authenticator in software, for tests of an app's passkeys.
Package passkeytest is a passkey authenticator in software, for tests of an app's passkeys.
throttletest
Package throttletest is for testing with auth.Throttle: TestStore checks that an auth.ThrottleStore keeps the promises a Throttle depends on.
Package throttletest is for testing with auth.Throttle: TestStore checks that an auth.ThrottleStore keeps the promises a Throttle depends on.
Package broadcast carries events between an app's instances, to the pages open on any of them: a post one instance saves, the pages on the others hear of, through a route of tug's Ctx.Events.
Package broadcast carries events between an app's instances, to the pages open on any of them: a post one instance saves, the pages on the others hear of, through a route of tug's Ctx.Events.
broadcasttest
Package broadcasttest is for testing with package broadcast: Memory is a Store in memory, and TestStore checks that a Store keeps the promises a Hub depends on.
Package broadcasttest is for testing with package broadcast: Memory is a Store in memory, and TestStore checks that a Store keeps the promises a Hub depends on.
Package cache keeps what's slow to work out, such as a dashboard's counts or what another service answered, for a while, in a store every instance of the app shares, such as a table in its database, and has locks for what mustn't run twice at once, on any of them.
Package cache keeps what's slow to work out, such as a dashboard's counts or what another service answered, for a while, in a store every instance of the app shares, such as a table in its database, and has locks for what mustn't run twice at once, on any of them.
cachetest
Package cachetest is for testing with package cache: TestStore checks that a cache.Store keeps the promises a cache.Cache depends on.
Package cachetest is for testing with package cache: TestStore checks that a cache.Store keeps the promises a cache.Cache depends on.
cmd
tug command
Command tug makes and runs tug apps:
Command tug makes and runs tug apps:
Package crypt encrypts the app's own values for keeping, such as a token for another service, which a user connected, kept in a column: a copy of the database, as a leaked backup, then has the token sealed, and nothing to open it with.
Package crypt encrypts the app's own values for keeping, such as a token for another service, which a user connected, kept in a column: a copy of the database, as a leaked backup, then has the token sealed, and nothing to open it with.
examples
api command
Command api is a small JSON API on tug: posts kept in memory, behind named routes in a group, with the usual middleware, errors that come back as JSON to a client that asks for it, and a limit on how fast a client writes, by its address, behind the proxies TRUSTED_PROXIES names.
Command api is a small JSON API on tug: posts kept in memory, behind named routes in a group, with the usual middleware, errors that come back as JSON to a client that asks for it, and a limit on how fast a client writes, by its address, behind the proxies TRUSTED_PROXIES names.
inertia command
Command inertia is tug's example of an Inertia app: posts, shown by React pages that get their props from Go handlers, and written with forms that check themselves as they're filled in.
Command inertia is tug's example of an Inertia app: posts, shown by React pages that get their props from Go handlers, and written with forms that check themselves as they're filled in.
Package inertia is the server side of the Inertia.js v3 protocol, for net/http.
Package inertia is the server side of the Inertia.js v3 protocol, for net/http.
internal
filetype
Package filetype tells what a file is by its first bytes, as http.DetectContentType does, not by its name or by what a browser says it is, which are the user's to say.
Package filetype tells what a file is by its first bytes, as http.DetectContentType does, not by its name or by what a browser says it is, which are the user's to say.
label
Package label names a field as a person reads it in a message: by its label tag, or else by the key a client sends it under, made into words.
Package label names a field as a person reads it in a message: by its label tag, or else by the key a client sends it under, made into words.
nonce
Package nonce carries a response's Content-Security-Policy nonce in its request's context: package middleware makes it, and package inertia hands it to the root template, and neither imports the other.
Package nonce carries a response's Content-Security-Policy nonce in its request's context: package middleware makes it, and package inertia hands it to the root template, and neither imports the other.
rw
Package rw wraps an http.ResponseWriter to remember what went through it: the status, and so whether the response has started, which an error handler has to know before it writes an error page, and the size, for the log.
Package rw wraps an http.ResponseWriter to remember what went through it: the status, and so whether the response has started, which an error handler has to know before it writes an error page, and the size, for the log.
seal
Package seal encrypts values with AES-256-GCM, under a key for one use alone, derived by HKDF from each of the app's keys: the first seals, and each of them opens, so the app's key can be rotated.
Package seal encrypts values with AES-256-GCM, under a key for one use alone, derived by HKDF from each of the app's keys: the first seals, and each of them opens, so the app's key can be rotated.
typegen
Package typegen writes the TypeScript that mirrors a tug app's Go: the props each page takes, the props every page shares, and the app's named routes.
Package typegen writes the TypeScript that mirrors a tug app's Go: the props each page takes, the props every page shares, and the app's named routes.
Package lang says an app's texts in its languages: what tug says to a person, as a form's errors, and what the app does, as its flash messages and mail.
Package lang says an app's texts in its languages: what tug says to a person, as a form's errors, and what the app does, as its flash messages and mail.
Package mail sends email: through an SMTP server, or, while developing, out to the terminal, where a link in it can be clicked.
Package mail sends email: through an SMTP server, or, while developing, out to the terminal, where a link in it can be clicked.
mailtest
Package mailtest is for testing with package mail: Outbox is a Mailer that keeps what it's sent, for a test to read.
Package mailtest is for testing with package mail: Outbox is a Mailer that keeps what it's sent, for a test to read.
Package middleware is net/http middleware for tug apps.
Package middleware is net/http middleware for tug apps.
Package migrate runs an app's migrations: files of SQL in its migrations/ directory, each a change to its database's tables, run once, in the order of their names, which say when they were made:
Package migrate runs an app's migrations: files of SQL in its migrations/ directory, each a change to its database's tables, run once, in the order of their names, which say when they were made:
migratetest
Package migratetest has the promises a migrate.Store keeps, as tests, which each Store's own tests run on its database.
Package migratetest has the promises a migrate.Store keeps, as tests, which each Store's own tests run on its database.
Package queue runs jobs in the background: work a request starts and doesn't wait for, such as sending a mail.
Package queue runs jobs in the background: work a request starts and doesn't wait for, such as sending a mail.
queuetest
Package queuetest is for testing with package queue: Memory is a Store for tests, and TestStore checks that a Store keeps the promises the queue depends on.
Package queuetest is for testing with package queue: Memory is a Store for tests, and TestStore checks that a Store keeps the promises the queue depends on.
Package session keeps a visitor's session in an encrypted cookie, with flash data: values one request leaves for the next, such as a message after a redirect or the validation errors a form is sent back with.
Package session keeps a visitor's session in an encrypted cookie, with flash data: values one request leaves for the next, such as a message after a redirect or the validation errors a form is sent back with.
Package ssr renders Inertia pages on the server, for their first visits, so a page's HTML is in the response before its scripts run: for search engines, link previews, and pages that show sooner.
Package ssr renders Inertia pages on the server, for their first visits, so a page's HTML is in the response before its scripts run: for search engines, link previews, and pages that show sooner.
Package storage keeps files, such as what people upload: in a directory on the app's own disk, or in S3, or any service that speaks S3's API, such as Cloudflare R2 or MinIO.
Package storage keeps files, such as what people upload: in a directory on the app's own disk, or in S3, or any service that speaks S3's API, such as Cloudflare R2 or MinIO.
Package tugtest tests an app's pages as Inertia's client uses them, without a browser or a frontend build.
Package tugtest tests an app's pages as Inertia's client uses them, without a browser or a frontend build.
Package validate checks a struct against the rules in its validate tags, and says what's wrong in words for the person who filled in the form:
Package validate checks a struct against the rules in its validate tags, and says what's wrong in words for the person who filled in the form:
Package vite puts a frontend built with Vite into pages that Go renders: the tags that load it, from the dev server while that runs and from the build's manifest otherwise, and a handler that serves the built files.
Package vite puts a frontend built with Vite into pages that Go renders: the tags that load it, from the dev server while that runs and from the build's manifest otherwise, and a handler that serves the built files.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL