Examples

board

The tutorial's app, tested end to end on SQLite and PostgreSQL

Browse on GitHub
$ git clone https://github.com/angvp/tango
$ cd tango/examples/board
$ go run .

Board

The bulletin board built in the tutorial, at its final (part 10) state: JSON API with bearer tokens and rate limits, server-rendered pages with accounts and CSRF-safe forms, the HTML admin, a live front page over WebSockets, a session-pruning job, structured logs, and graceful shutdown.

The code follows the tutorial text; when one changes, change the other.

Admin changelist filters

The HTML admin shows the boolean and foreign key filters from the admin guide's changelist filters section: Accounts filter by Active, Posts by their author, and Comments by their post. TestAdminChangelistFilters in admin_filters_test.go exercises them over HTTP on whichever database TANGO_TEST_DSN selects.

Run locally

echo "BOARD_JWT_SECRET=$(openssl rand -hex 32)" >> .env
go run . -migrate
go run . -tango-admin-create=admin
go run .
go run ./shell -c 'Models()'
go test ./...

TANGO_DB_DSN picks the database (sqlite://app.db by default, or postgres://…) and TANGO_ADDR the listen address (:8000). See part 10 for the full configuration table.

The Dockerfile is the one from part 10. In this repository the module resolves github.com/angvp/tango through a local replace => ../.., so build the image from a copy of the project that depends on a published tanGO version.

examples/board/main.go
package main

import (
	"context"
	"database/sql"
	"fmt"
	"log/slog"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/angvp/tango"
	"github.com/angvp/tango/admin"
	"github.com/angvp/tango/db"

	"board/migrations"
	"board/project"

	_ "github.com/jackc/pgx/v5/stdlib"
	_ "modernc.org/sqlite"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() error {
	// One structured JSON logger for the whole app. slog.SetDefault makes
	// it the logger for tanGO's middleware and for slog calls in your code.
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	slog.SetDefault(logger)

	if err := tango.LoadEnvFile(".env"); err != nil {
		return err
	}

	dsn, err := tango.LoadDBConfigFromEnv()
	if err != nil {
		return err
	}

	sqlDB, err := sql.Open(dsn.Driver, dsn.Source)
	if err != nil {
		return err
	}
	defer sqlDB.Close()

	store := db.NewStore(sqlDB, dsn.Dialect)
	config := project.Config(store)

	if handled, err := admin.HandleCLI(context.Background(), store, os.Args[1:], os.Stdin, os.Stdout, os.Stderr); handled || err != nil {
		return err
	}

	handled, err := tango.DispatchFlags(config, sqlDB, dsn.Dialect, migrations.Migrations)
	if handled || err != nil {
		return err
	}

	// Ctrl-C or SIGTERM (what `docker stop` and most process managers send)
	// cancels ctx, and ServeContext shuts down gracefully.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	logger.Info("listening", "addr", config.Addr)
	err = tango.ServeContext(ctx, config, sqlDB, dsn.Dialect,
		tango.WithLogger(logger),
		tango.WithShutdownTimeout(10*time.Second),
	)
	logger.Info("stopped", "clean", err == nil)
	return err
}