Sessions

Cookie-based sessions via req.session.

Enabling sessions

bxsapp.use( boxExpressSession() )

Once registered, every request gets a session — a new one is created and a session cookie set if the request didn't already carry one.

Reading and writing session data

bxsapp.get( "/visit-count", ( req, res ) => {
    req.session.views = ( req.session.views ?: 0 ) + 1
    res.json( { views: req.session.views, sessionID: req.sessionID } )
} )

req.session is a plain struct — read and write it directly. It's persisted server-side and keyed by req.sessionID, which is also readable directly for logging or debugging.

Ending a session

bxsapp.get( "/logout", ( req, res ) => {
    req.destroySession()
    res.json( { loggedOut: true } )
} )

req.destroySession() clears the stored session data and expires the session cookie on the client (Max-Age=0) — the next request starts a fresh session.

Regenerating on login

bxsapp.post( "/login", ( req, res ) => {
    // ...verify credentials...
    req.regenerateSession()
    req.session.user = user
    res.redirect( "/" )
} )

Call req.regenerateSession() when a user logs in (or gains privileges). It moves the session's data to a new id, deletes the old one, and sends the new cookie. Without it, a session id an attacker managed to plant in the victim's browser before login would become an authenticated session (session fixation).

Skipping unnecessary store writes (resave, saveUninitialized)

By default, every request through this middleware writes to the store — even one that never reads or touches req.session at all. Free against the default in-memory store, a real cost against anything out-of-process (a JDBCStore-backed boxExpressCacheStore(), Redis, etc). Two options, mirroring Express's own express-session options of the same names, turn that off:

bxsapp.use( boxExpressSession( {
    store: myStore,
    resave: false,             // don't re-save an existing session that wasn't modified
    saveUninitialized: false   // don't save (or cookie) a new session that wasn't modified
} ) )
  • saveUninitialized: false — a freshly created session that's never modified isn't saved, and isn't given a Set-Cookie either. This is the one that matters most in practice: an anonymous request that only ever reads req.session — including a vulnerability scanner throwing a burst of unrelated 404s at a public site — shouldn't mint and persist a throwaway session for each one.
  • resave: false — an existing session that was loaded but never modified isn't re-written to the store. The cookie is still refreshed either way; only the store write is skipped.
  • A session that is modified, new or existing, is always saved regardless of either option.

Both default to true (the historical always-save behavior), so upgrading doesn't change anything unless you opt in. Recommended false for anything backed by a real datastore.

Durable sessions (boxExpressCacheStore)

The default session store is an in-memory ConcurrentHashMap on the Session instance — fine for one process, gone on restart, and not shared across a cluster. It holds at most maxSessions sessions (default 100000); past that, new sessions aren't stored — existing ones keep working — and a warning is logged, so a flood of cookieless requests can't exhaust memory. With the default saveUninitialized: true every new visitor counts toward that cap, so set it to false if you can. Expired entries are swept at most every 30 seconds. boxExpressCacheStore() is a ready-made store backed by BoxLang's own cache() service instead:

bxsapp.use( boxExpressSession( { store: boxExpressCacheStore( "sessions" ) } ) )

boxExpressSession( { cache: "sessions" } ) is shorthand for the same thing.

The named cache ("sessions" here) should already be registered in boxlang.json — this doesn't create one, it just talks to it (if it's missing, the app keeps serving — see Falling back without durable storage). Point that cache's objectStore at "JDBCStore" and session data lands in a real SQL table instead of memory, surviving a restart and shared across every process pointed at the same database:

json"caches": {
  "default": { "provider": "BoxCacheProvider" },
  "sessions": {
    "provider": "BoxCacheProvider",
    "properties": {
      "objectStore": "JDBCStore",
      "datasource": "sessionDB",
      "table": "boxlang_sessions",
      "autoCreate": false
    }
  }
}

See Configuration for the full datasource block. Two separate things worth knowing, found by actually running it against a real database rather than trusting the docs:

  • Keep "default" in the caches block alongside your own entry — overriding caches replaces it wholesale, and BoxLang's own query engine depends on a "default" cache existing somewhere in it.
  • autoCreate: true is currently unreliable, and declaring "default" does not fix it. It can fail at BoxLang startup with Cache [default] does not exist, because JDBCStore's own auto-create check runs a query internally, and cache creation order isn't guaranteed to reach "default" first — this reproduced the same way whether "default" was declared or not, and regardless of where it sat in the JSON. The two bullets above are unrelated fixes for unrelated problems. Safest path: create the table yourself once (a migration, or a one-time script) and leave autoCreate: false, as in the example above — that sidesteps the internal query entirely.

Falling back without durable storage

boxExpressCacheStore() (and rate limiting's cache option) degrade instead of failing requests:

  • The named cache isn't registered — sessions live in this process's memory, with a warning logged at startup.
  • The cache is registered but in-memory (BoxLang's default ConcurrentStore, which isDistributed() reports as not shared) — it's used as-is, with a warning that entries aren't shared across instances or kept across restarts.
  • A call to a durable cache fails at runtime (the database goes away) — that call uses per-process memory, logged at most once every 30 seconds, and the next call tries the cache again, so it recovers by itself. Entries written during the outage stay local; reads check the cache first, then local memory.

getMode() reports "cache" or "local", and isDurable() whether the cache is backed by a durable, shared store. Two options fail fast in production instead of degrading:

OptionEffect
fallback: falseA missing cache throws at startup and runtime failures propagate — the behavior before 0.2.17, when boxExpressCacheStore( "missing" ) threw on first use
requireDurable: trueRefuses to start unless the cache is durable

The session cookie is named connect.sid, matching Express's own default — familiar if you're coming from Node. It's set with HttpOnly by default, the same as res.cookie().