early · v0.96.0

Full-stack Go that doesn't get in the way of you or your agents.

GoFastr is a full-stack Go framework. Declare your domain in Go and get server-rendered screens, REST endpoints, MCP tools, migrations, and typed queries. It stays plain Go on disk that you own.

main.go
30 lines
package main

import (
	"context"
	"net/http"

	"github.com/DonaldMurillo/gofastr/core/handler"
	"github.com/DonaldMurillo/gofastr/core/render"
	"github.com/DonaldMurillo/gofastr/core/router"
)

type Pong struct {
	Status string `json:"status"`
}

func main() {
	r := router.New()

	// A server-rendered page.
	r.Get("/", render.HTMLHandler(func(req *http.Request) render.HTML {
		return render.Tag("h1", nil, render.Text("Hello from core."))
	}))

	// A typed JSON route: the adapter binds input and serializes output.
	r.Get("/api/ping", handler.HandlerAdapter(func(ctx context.Context, _ struct{}) (Pong, error) {
		return Pong{Status: "ok"}, nil
	}))

	http.ListenAndServe(":8080", r)
}
main.go
26 lines
package main

import (
	"database/sql"
	"log"

	"github.com/DonaldMurillo/gofastr/core/schema"
	"github.com/DonaldMurillo/gofastr/framework"
	_ "github.com/DonaldMurillo/gofastr/sqlite/stdlib"
)

func main() {
	db, _ := sql.Open("sqlite3", "app.db")
	app := framework.NewApp(framework.WithDB(db), framework.WithMCP()) // WithMCP serves the tools at /mcp

	// CRUD is auto-on when a DB is set (CRUD *bool: nil = auto).
	app.Entity("posts", framework.EntityConfig{
		Exposure: &framework.ExposureConfig{
			Public: true, // anonymous read AND write; omit it and CRUD requires a session (secure by default)
			MCP:    true, // emit posts_list/get/create/update/delete MCP tools
		},
		Fields: []schema.Field{{Name: "title", Type: schema.String, Required: true}},
	})

	log.Fatal(app.Start(":8080")) // GET/POST /posts, /openapi.json, MCP: all live
}
main.go
67 lines
package main

import (
	"context"
	"database/sql"
	"log"

	"github.com/DonaldMurillo/gofastr/battery/auth"
	"github.com/DonaldMurillo/gofastr/core-ui/app"
	"github.com/DonaldMurillo/gofastr/core-ui/html"
	"github.com/DonaldMurillo/gofastr/core/render"
	"github.com/DonaldMurillo/gofastr/core/schema"
	"github.com/DonaldMurillo/gofastr/framework"
	"github.com/DonaldMurillo/gofastr/framework/uihost"
	_ "github.com/DonaldMurillo/gofastr/sqlite/stdlib"
)

// A screen is plain Go: Render returns server-rendered HTML.
type HomeScreen struct{}

func (s *HomeScreen) ScreenTitle(ctx context.Context) string { return "Notes" }
func (s *HomeScreen) Render() render.HTML {
	return html.Heading(html.HeadingConfig{Level: 1}, render.Text("My notes"))
}

func main() {
	db, _ := sql.Open("sqlite3", "notes.db")

	// Server-rendered screens. Each also serves an auto llm.md.
	ui := app.NewApp("Notes")
	ui.Register("/", &HomeScreen{}, nil)

	// SEO for those pages.
	host := uihost.New(ui,
		uihost.WithDescription("A tiny notes app."),
		uihost.WithOpenGraph(uihost.OG{Title: "Notes", Type: "website"}),
		uihost.WithSitemap(uihost.SitemapConfig{BaseURL: "https://notes.example"}),
	)

	// MCP for agents.
	fwApp := framework.NewUIHostApp(host,
		framework.WithDB(db),
		framework.WithAPIPrefix("/api"),
		framework.WithMCP(),
	)

	// Scope.OwnerField scopes rows per user: anonymous → 401, cross-user → 404.
	fwApp.Entity("notes", framework.EntityConfig{
		Scope:    &framework.ScopeConfig{OwnerField: "user_id"},
		Exposure: &framework.ExposureConfig{MCP: true},
		Fields:   []schema.Field{{Name: "title", Type: schema.String, Required: true}},
	})

	// Login + sessions.
	authMgr := auth.New(auth.AuthConfig{
		DevMode:      true, // dev only: mints a per-process JWT secret; set JWTSecret in prod
		UserStore:    auth.NewEntityUserStore(db, "auth_users"),
		SessionStore: auth.NewEntitySessionStore(db, "auth_sessions"),
	})
	authMgr.Use(auth.NewCorePlugin())
	if err := authMgr.Init(fwApp); err != nil {
		log.Fatal(err)
	}
	fwApp.Use(auth.SessionMiddleware(authMgr))

	log.Fatal(fwApp.Start(":8080"))
}

Numbers you can check.

Each value is measured by this running binary or enforced by a test in the repo. Nothing here is an adjective.

of client JavaScript, gzipped
16.2 KB
The core runtime, measured from this running binary. Feature modules load on demand; a size-budget test fails the build if any of them grows.
docs embedded in every binary
122
gofastr docs reads them offline. This site serves the same files under /docs and /llms.txt; agents query them over MCP.
MCP tools per entity
5
list, get, create, update, delete. Each dispatches through the app router, so the caller's login and permissions apply.
databases
2
SQLite and Postgres. No MySQL, no Mongo.
binary to deploy
1
go build emits it. No Node, no platform, no telemetry.
npm packages
0
There is no package.json in the repo. The client runtime is checked-in JS the binary serves.

Server-rendered screens, not just an API.

Below is the shape of a server-rendered screen: a data table with status badges and a create button, built from framework/ui components and served as plain HTML.

Most Go frameworks stop at the API. GoFastr renders the pages too: on the server, in Go, with no React or Vue on the client.

  • Every page is full HTML on first load: fast and readable by crawlers and agents.
  • A small JS runtime hydrates that HTML in place with no re-render. Cross-page nav swaps content client-side with a route cache; you never write the router.
  • In-page changes such as sort, paginate, or add a row are island calls. The server returns new HTML and the runtime swaps one part.
  • You write screens in Go, composed from framework/ui components.

Customers

meridian.local/customers

Customers
NamePlanMRRStatus
Acme Corppro$1,240active
Globexenterprise$8,900active
Initechfree$0churned
Umbrellapro$2,150active