Forms and Auth
Forms in Vango should work in two modes:
interactive live mode
plain HTTP fallback mode
Server validation is authoritative in both.
Route forms with setup.RouteForm
This is the preferred pattern for page-owned form posts.
1type SignupInput struct {
2 Email string `form:"email" validate:"required,email"`
3 Password string `form:"password" validate:"required,min=12"`
4}
5
6func Action(ctx vango.ActionCtx, in SignupInput) (*vango.FormResult, error) {
7 if err := createAccount(ctx.StdContext(), in); err != nil {
8 return nil, err
9 }
10 return vango.RedirectTo("/welcome"), nil
11}
12
13func SignupScreen() vango.Component {
14 return vango.Setup(vango.NoProps{}, func(s vango.SetupCtx[vango.NoProps]) vango.RenderFn {
15 form := setup.RouteForm(&s, SignupInput{})
16
17 return func() *vango.VNode {
18 return form.View(
19 form.Field("email", Input(Type("email"))),
20 form.Field("password", Input(Type("password"))),
21 Button(Type("submit"), Disabled(form.IsSubmitting()), Text("Create account")),
22 )
23 }
24 })
25}
Rules:
use it when the form belongs to the current route
form.View(...) renders the actual <form> and injects CSRF automatically
parse errors and struct-tag validation rerender with raw values preserved
use vango.Invalid(...) for business-rule validation after decoding succeeds
Manual form pattern
Manual signals plus setup.Action are the escape hatch for tiny bespoke forms.
Use this only when typed form helpers are not buying you much.
Typed forms with setup.Form
Use setup.Form for session-driven forms, dialogs, and custom submit transports.
1type ContactFormData struct {
2 Name string `form:"name" validate:"required,min=2,max=100"`
3 Email string `form:"email" validate:"required,email"`
4 Message string `form:"message" validate:"required,max=1000"`
5}
6
7form := setup.Form(&s, ContactFormData{})
Common helpers:
Field(...)
Values()
Validate()
Errors()
FieldErrors(...)
SetError(...)
Reset()
Array(...)
ArrayKeyed(...)
Use setup.Form when the transport is not a route-bound page Action.
Dynamic form arrays
Use ArrayKeyed for reorderable or filterable arrays so identity stays stable.
Use plain Array only when order is static and identity does not matter.
Low-level parsing
Low-level form parsing APIs are escape hatches for custom HTTP handlers, APIs that intentionally accept form bodies, or unusual upload flows.
They are not the default path for normal page forms.
Auth model
Vango auth has two layers:
HTTP layer validates the request
session layer carries the auth projection during live interaction
The standard pattern is:
HTTP middleware authenticates the request
request context carries the user or principal
OnSessionStart and OnSessionResume rehydrate auth state for the live session
component code reads auth through the auth package
The auth bridge
Middleware authenticates the HTTP request. Session hooks project that into the live session.
Read auth in app code with helpers such as:
auth.Get[T](ctx)
auth.Require[T](ctx)
Route auth middleware
Use pkg/authmw for route-level protection.
Common helpers:
authmw.RequireAuth
authmw.RequireRole(...)
authmw.RequirePermission(...)
authmw.RequireAny(...)
authmw.RequireAll(...)
Auth freshness
Long-lived sessions need freshness checks.
use periodic auth checks in session config
use ctx.RevalidateAuth() before sensitive mutations
fail closed when auth becomes invalid
Do not store raw provider credentials or tokens in persisted app state.
Uploads
Uploads happen over HTTP, not over the live event channel.
Recommended pattern:
mount a dedicated upload endpoint
enforce CSRF, size limits, and type limits
keep temp uploads private
upload first, claim later
Toasts
Use pkg/toast for live feedback:
toast.Success(...)
toast.Error(...)
toast.Warning(...)
toast.Info(...)
toast.WithTitle(...)
Toast events are emitted through ctx.Emit("vango:toast", ...), so the UI that renders them is up to your app.
Next, read Client Boundaries before introducing browser-owned behavior.