Skip to content

Authentication and live sessions

The live layer authenticates a browser page: the cookie proves which browser, the page id says which page. It knows nothing about your users: NewSession takes no identity, and the live cookie is not your login cookie and does not expire with it. Connecting the two is application code.

The page render is the one moment when your ordinary auth middleware and the live session are in the same place. Put the identity on the session there, under an unexported key type so nothing else can collide with it:

type currentUserKey struct{}
func (a *app) page(w http.ResponseWriter, r *http.Request) {
user, ok := a.auth.UserFrom(r) // your middleware, your session store
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
sess := a.live.NewSession(w, r) // before anything is written to w
sess.Set(currentUserKey{}, user.ID)
// ... render the page with Config{Live: true, Page: sess.ID()}
}

A handler re-derives the user from the session and, for anything that matters, re-checks that the login is still good. The live cookie outlives your auth cookie, so “this session belonged to Ada at render time” is not “Ada is still logged in now”:

var errUnauthenticated = errors.New("no signed-in user for this session")
func (a *app) user(c *live.Ctx) (UserID, error) {
v, ok := c.Session.Get(currentUserKey{})
if !ok {
return "", errUnauthenticated
}
uid := v.(UserID)
// Ctx.Request is the action's own POST: your auth cookie rides on it,
// so the ordinary check works here too. This is what makes logout stick.
if !a.auth.StillValid(c.Request, uid) {
return "", errUnauthenticated
}
return uid, nil
}
live.On(srv, "delete-item", func(c *live.Ctx, d components.ItemListRemoveDetail) error {
uid, err := a.user(c)
if err != nil {
return err // logged server-side, a bare 500 to the page
}
return a.items.Delete(uid, d.ID) // scoped to the user, not to the detail
})

The StillValid check costs one session-store read per action. If an action is hot enough for that to matter, it is almost certainly not one that needs per-action revocation. Read-only filters and cursors can skip it and rely on the render-time binding alone.

Re-checking on each action already means a logged-out user’s next click fails. What it does not do is stop the stream: patches pushed by background work would keep flowing to a screen someone walked away from. If that matters (shared machines, sensitive dashboards), keep an index of the user’s live sessions and end them at logout:

type presence struct {
mu sync.Mutex
byUser map[UserID]map[*live.Session]struct{}
}
func (p *presence) add(uid UserID, s *live.Session) {
p.mu.Lock()
defer p.mu.Unlock()
if p.byUser[uid] == nil {
p.byUser[uid] = map[*live.Session]struct{}{}
}
p.byUser[uid][s] = struct{}{}
}
// At render, alongside the identity binding:
sess.Set(currentUserKey{}, user.ID)
p.add(user.ID, sess)
// At logout:
func (p *presence) logout(uid UserID) {
p.mu.Lock()
sessions := p.byUser[uid]
delete(p.byUser, uid)
p.mu.Unlock()
for s := range sessions {
s.Send(live.Reload()) // the reload lands on /login via your middleware
s.Close()
}
}

The Reload before Close is what turns “the stream went quiet” into “the page navigated to the login screen”: the reloaded request has no valid auth cookie, so your ordinary middleware redirects it. Closed sessions remove themselves from the server; entries in the index are reaped with the map delete at logout, and a session that expires on its own is just a dead pointer that logout closes harmlessly.

  • Do not put the user id in the page or the detail. The session already knows; anything the page sends can be edited in a console.
  • Do not treat the live cookie as a login. It authenticates a browser to its own sessions and nothing else; two different users on one browser share it by design, which is precisely why identity is bound per-session at render.
  • Do not gate at NewSession alone. That checks login at render time; actions arrive minutes or hours later. The render-time check decides what to render; the per-action check decides what may happen.