The framework layer, on top of core
framework and framework/ui sit on top of the primitives. Declare an entity and get the database, API, and tools; compose screens from components that already match your theme.
Entities
Declare an entity in Go — fields, types, relations. One call gives you the table, REST endpoints, validation, an OpenAPI entry, and MCP tools.
app.Entity("posts", framework.EntityConfig{ Exposure: &framework.ExposureConfig{Public: true}, Fields: []schema.Field{ {Name: "title", Type: schema.String, Required: true}, {Name: "body", Type: schema.Text}, {Name: "status", Type: schema.Enum, Values: []string{"draft", "published"}, Default: "draft"}, },})
Auth
Login, sessions, OAuth, magic-link, 2FA, password reset. Each is a plugin you add to an auth manager; the middleware puts the signed-in user on the request.
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) // without a JWT secret (or DevMode) Init fails closed}fwApp.Use(auth.SessionMiddleware(authMgr))
Access control
Roles and permissions, plus per-user owner scoping. It fails closed: a request with no matching policy is denied, not allowed.
app.Entity("notes", framework.EntityConfig{ Scope: &framework.ScopeConfig{OwnerField: "user_id"}, // every read and write is scoped to the signed-in user Exposure: &framework.ExposureConfig{ Access: framework.AccessControl{ Read: "notes:read", Create: "notes:write", Update: "notes:write", Delete: "notes:admin", }, },})
Migrations
Versioned, ordered, reversible schema changes. In dev the framework can auto-migrate from the declared entities; in production you run the ordered migrations.
// Derive the schema from the declared entities and apply it.if err := framework.AutoMigrate(db, app.Registry); err != nil { log.Fatal(err)}
Components
framework/ui composes intent, not tags: PageHeader, DataTable, Form, Card, charts. Each ships its own CSS through the theme, so a page inherits your look for free.
ui.PageHeader(ui.PageHeaderConfig{ Eyebrow: "Settings", Title: "Workspace settings", Subtitle: "Tune defaults for everyone on this workspace.", Actions: ui.Button(ui.ButtonConfig{Label: "Save changes", Variant: ui.ButtonPrimary}),})
Theming
One typed theme drives every color, space, and font as CSS variables. Change a token and every component updates — you never edit a component's CSS to reskin an app.
t := theme.Default(theme.Overrides{ Primary: "oklch(0.82 0.155 78)", // amber accent Surface: "oklch(0.17 0.006 75)", RadiusMd: 6,})site.WithTheme(t)