Storage & Auth
In Doors, storage is a small key-value layer attached to session or instance lifetime.
That pattern is especially useful for authentication. The same idea also works for shared settings like theme or locale.
Start
You usually access storage in one of three places:
r.SessionStore()in a model handlerdoors.SessionStore(ctx)when you already have a Doorsctxdoors.InstanceStore(ctx)for page-instance-local storage
The store API is:
Init(key, func() any)creates the value once and returns it, or returns the existing value if it is already thereLoad(key)gets the current value for a keySave(key, value)replaces the stored value and returns the previous oneRemove(key)deletes the value and returns what was stored before
For reactive session state, the usual pattern is:
Initin the model handlerLoadlater in event or hook handlers
Save and Remove are more useful when you want to replace or clear the stored object itself.
Session
Session storage is shared by all live instances in the same Doors session.
Use it for state that should be shared across pages or tabs in that browser session, such as:
- authentication
- current user data
- theme
- locale
If several pages subscribe to the same source from session storage, they can all react to the same update.
Instance
Instance storage belongs to one live page instance.
Use it for page-local state that should survive rerenders in that page but should not sync across other pages or tabs.
If the state should affect the whole logged-in browser session, use session storage instead.
Model
The model handler is the usual place to bootstrap shared session state from cookies or headers.
For auth, a good pattern is:
- read the session cookie
- validate it against your real server-side session storage
- create or reuse one session-scoped source
- place that source on your
App
type authKey struct{}
type App struct {
auth doors.Source[bool]
}
doors.UseModel(router, func(r doors.RequestModel, _ doors.Source[Path]) doors.Response {
auth := r.SessionStore().Init(authKey{}, func() any {
c, err := r.GetCookie("session")
if err != nil {
return doors.NewSource(false)
}
_, ok := driver.Sessions.Get(c.Value)
return doors.NewSource(ok)
}).(doors.Source[bool])
return doors.ResponseComp(App{
auth: auth,
})
})
Init returns the existing value if it was already created earlier in the same session.
That means later requests do not create a new auth source. They get the same shared one back.
Do not treat the cookie by itself as proof of authentication. Keep the real session in your own database, Redis, or other server storage, usually by session ID from the cookie.
Render
After that, render from your App fields instead of reaching back into storage again:
elem (a App) Main() {
~(doors.Sub(a.auth, elem(ok bool) {
~(if !ok {
<p>Please log in</p>
} else {
<p>Dashboard</p>
})
}))
}
Because a.auth is a session-scoped source, every page that subscribes to it can react when it changes.
Update
On login or logout, update the real session storage, update the cookie, and update the same shared source.
auth := doors.SessionStore(ctx).Load(authKey{}).(doors.Source[bool])
On login:
func login(ctx context.Context, r doors.RequestForm[LoginData]) bool {
auth := doors.SessionStore(ctx).Load(authKey{}).(doors.Source[bool])
sessionDuration := 24 * time.Hour
session := driver.Sessions.Add(r.Data().Login, sessionDuration)
r.SetCookie(&http.Cookie{
Name: "session",
Value: session.Token,
Path: "/",
HttpOnly: true,
})
doors.SessionExpire(ctx, sessionDuration)
auth.Update(ctx, true)
return true
}
If your auth session has a fixed lifetime, it usually makes sense to call doors.SessionExpire(ctx, sessionDuration) on login too.
That keeps the internal Doors session from outliving the cookie or backend session and helps ensure an already-open instance does not keep handling requests after auth should be gone.
On logout or deauth:
func logout(ctx context.Context, r doors.RequestEvent[doors.PointerEvent]) bool {
auth := doors.SessionStore(ctx).Load(authKey{}).(doors.Source[bool])
if c, err := r.GetCookie("session"); err == nil {
driver.Sessions.Remove(c.Value)
}
r.SetCookie(&http.Cookie{
Name: "session",
Path: "/",
MaxAge: -1,
HttpOnly: true,
})
auth.Update(ctx, false)
return true
}
This is the main benefit of shared reactive session state in Doors: open pages can react to auth changes immediately, without reload actions and without ending the whole Doors session.
Rules
- If the UI should react, store a
Sourcein the store. - Put auth in session storage, not instance storage.
- Initialize shared state in the model handler, then keep it on your
App. - Validate the cookie against your real session storage, not against the cookie alone.
- If auth has a fixed lifetime, usually cap the Doors session to that same lifetime on login.
- Use
doors.SessionEnd(ctx)only when you intentionally want to force-close the whole Doors session.