Session & Instance
In Doors, a session and an instance are different lifetimes:
- a session is the shared scope for one browser session
- an instance is one live page in that session
Most app code does not need to control them directly. Doors manages them for you.
These utilities are for the cases where you want to:
- cap how long the current Doors session may live
- force-end a session or just one instance
- attach background work to the session lifetime
- attach runtime IDs to logs or traces
For session and instance storage, see Storage & Auth.
Use
These functions need a Doors context, such as the ctx you get in:
- event handlers
- hook handlers
doors.Go(...)- other Doors runtime callbacks
Use the current Doors ctx, not context.Background().
Lifecycle
By default, Doors manages session and instance lifetime automatically.
At a high level:
- a request without a live Doors session cookie gets a new session cookie on that response right away, but the session itself is created lazily, when something first uses it
- rendering a page uses the session; requests fully handled by
app.Usemiddleware ordoors.UseResourcedo not create one - a session is renewed on later requests and uses a timer-based lifetime that starts at creation
- a page gets its own live instance
The internal Doors session cookie is HttpOnly, Secure, scoped to /, and named from ServerSessionCookiePrefix + WithID. By default, the prefix is empty and the app ID defaults to doors, so the cookie is named doors.
For plain HTTP development, ServerSessionCookieNoSecure can omit Secure.
If you want browser-enforced cookie prefix rules, set ServerSessionCookiePrefix explicitly, for example to __Host- or __Secure-.
Instances also have their own lifecycle rules:
- a new instance must get its first client connection within
InstanceConnectTimeout - after that, inactive instances are cleaned up by
InstanceTTL - if the session reaches
SessionInstanceLimit, older instances can be suspended
That suspension is why an older page may come back by reloading when the user returns to it.
Expire
doors.SessionExpire(ctx, d) sets the maximum remaining lifetime of the current Doors session.
doors.SessionExpire(ctx, 24*time.Hour)
This affects the whole session, not just the current page.
Use it when your application has its own session lifetime and you do not want the internal Doors session to outlive it.
One common case is login:
doors.SessionExpire(ctx, sessionDuration)
Important detail: this is a cap, not a keepalive. The session can still end earlier because of the normal session TTL rules. The earlier limit wins.
It also does not affect only one tab. It sets the limit for the whole current Doors session.
If you call doors.SessionExpire(ctx, 0), Doors removes that forced expiration cap and goes back to the normal session TTL behavior.
End
doors.SessionEnd(ctx) force-ends the whole current Doors session.
doors.SessionEnd(ctx)
That ends all live instances in the session, not just the page that called it.
Use it when you intentionally want a full teardown, for example:
- a hard logout that should close every open page immediately
- a security event that invalidates the whole session
- a forced account or tenant switch where all live pages must stop
With shared reactive session state, this is not the normal auth update path. For normal login, logout, and deauth flows, it is usually better to update session-scoped state and let pages react.
Context
doors.SessionContext(ctx) returns a context that is canceled when the current Doors session ends.
sessionCtx := doors.SessionContext(ctx)
Use it for goroutines or external work that should live for the whole browser session and stop on SessionEnd, session expiration, or session cleanup.
It is broader than the current instance or dynamic owner context. It is suitable for session-scoped helpers, Door methods, and Source or Beam reads, subscriptions, and updates; a subscription made with it lives until the session ends. doors.Reload, the package-level doors.Deferred* functions, and instance-scoped helpers panic on it.
For work that should stay scoped to the current dynamic owner, keep using the current ctx, or use doors.DetachedContext(ctx) from a goroutine of your own, see Detached Context.
For work that should outlive the current dynamic owner but stay bounded by the current page instance, use doors.InstanceContext(ctx).
Both are detached contexts: wait on the channels of their operations only in goroutines of your own, see Detached Context.
Detached Context
doors.DetachedContext(ctx) returns a context whose operations are delivered on their own. It keeps the values and the owner of ctx, so it is canceled when the content you called it from goes away, but its operations do not join the handler's updates, its batch, or its indicator.
dctx := doors.DetachedContext(ctx)
go func() {
// Door operations and doors.Call with dctx
}()
Operations made with a detached context are not part of the current handler's work: not its batch, its indicator, or
OnSettle. Wait on their channels only in goroutines of your own, never during rendering, in a handler, or in a subscription: that can block the page.
Whether a context is detached decides what the completion channels of Door operations and doors.Call deliver.
Every channel first sends one value once the call is scheduled: nil, or an error when it failed before that point — the render failed, the action could not be built, or the call was canceled or superseded by a newer operation (context.Canceled). If the Door is not mounted, the channel closes without a value.
What comes after that value depends on the context:
| Context | After the scheduled value |
|---|---|
| Detached | A second value: nil once the update is on the page or the browser has answered, an error, or context.Canceled when the call was canceled after it was scheduled. A destination armed with Into is written before it. Then the channel closes. |
| Any other | The channel closes. No result is reported, and a destination armed with Into stays unwritten; the closed channel is the only signal, there is no error. |
A channel that closes without a further value means the operation was accepted, but its outcome is not reported: the Door is not mounted yet, or the context is not detached.
Detached contexts:
doors.DetachedContext(ctx)doors.InstanceContext(ctx)- the
ctxofdoors.Go(...) - the
ctxpassed todoors.OnReadyanddoors.OnBatchcallbacks - any context derived from one of these with the
contextpackage, such ascontext.WithTimeout
Not detached:
- handler
ctx— event, form, and hook handlers (also when used later from a goroutine) - render
ctx - beam subscription
ctx - the
ctxpassed todoors.OnSettlecallbacks: it keeps their operations in place with the handler's updates, so do not block in them - the
ctxpassed to theopsofdoors.OnSettleanddoors.OnBatch, even when the call itself was made with a detached context
If you need the result, wait for the second value in a goroutine of your own, on a detached context:
go func(ctx context.Context) {
var now time.Time
ch := doors.Call(ctx, doors.ActionTime{}.Into(&now))
<-ch // scheduled
if err, ok := <-ch; ok && err == nil {
// use now
}
}(doors.DetachedContext(ctx))
Use doors.Go(...) for the same wait when the goroutine should follow the lifetime of a rendered subtree, and doors.InstanceContext(ctx) when it should outlive the content it started from but stop with the page.
Instance End
doors.InstanceEnd(ctx) ends only the current live page instance.
doors.InstanceEnd(ctx)
Use it when the current page should stop, but the rest of the session should stay active.
For example:
- a one-off detached page should go away after finishing its work
- the current tab should be discarded, but other tabs should stay live
This ends the current live instance immediately. It does not end the session and does not touch sibling instances.
IDs
doors.SessionID(ctx) returns the ID of the current Doors session.
sessionID := doors.SessionID(ctx)
All instances in the same session share that value.
doors.InstanceID(ctx) returns the ID of the current live page instance.
instanceID := doors.InstanceID(ctx)
That value is specific to one live page.
These IDs are useful for:
- logging
- tracing
- grouping related events
- debugging multi-tab behavior
They are Doors runtime IDs, not user IDs or authentication tokens.
Rules
- Use
SessionExpireto cap the session lifetime. - Use
SessionExpire(ctx, 0)to remove an explicit session-expiration cap. - Use
SessionContextfor work that should stop when the whole session ends. - Use
DetachedContextfor goroutines of your own that keep the current dynamic owner; it is the context on which completion channels deliver results. - Use
InstanceContextfor goroutines that should outlive the current dynamic owner but stop with the current instance. - Only detached contexts get the result of a Door operation or
doors.Call; see Detached Context. - Use
SessionEndonly when you really want to end the whole Doors session. - Use
InstanceEndwhen only the current live page should stop. - Use
SessionIDandInstanceIDfor diagnostics, not as business identifiers.