# How Hister Stores Session Data and Manages Secret Keys

> Discover how Hister stores session data and manages secret keys using a custom database backed store and cryptographically signed cookies. Learn more about this secure session management solution.

- Repository: [Adam Tauber/hister](https://github.com/asciimoo/hister)
- Tags: internals
- Published: 2026-09-01

---

**Hister uses a custom database-backed session store that persists gob-encoded session state in a `web_sessions` table while using a configurable secret key to cryptographically sign client-side cookies that reference the database entries.**

Hister is an open-source search engine application written in Go that implements a custom session management system. Unlike traditional cookie-based stores that serialize session data into the browser, Hister maintains session state server-side in a relational database, using signed cookies only as opaque references to database records. This architecture provides enhanced security by ensuring that sensitive session data never leaves the server.

## Secret Key Configuration and Initialization

Hister derives its cryptographic signing capabilities from a **configurable secret key** that is injected at runtime through environment variables or configuration files.

### Environment-Based Secret Key

The secret key is exposed through the `Config.SecretKey()` method in [`config/config.go`](https://github.com/asciimoo/hister/blob/main/config/config.go), which typically reads from an environment variable such as `HISTER_SECRET_KEY`. This key is passed as a byte slice to the session store initialization and is used to sign and encrypt session cookies, preventing client-side tampering.

### Store Initialization in server.go

During application startup in [`server/server.go`](https://github.com/asciimoo/hister/blob/main/server/server.go), the database-backed session store is instantiated with the secret key, base URL, and maximum session age:

```go
// server/server.go (line 145)
sessionStore = newSessionStore(cfg.SecretKey(), cfg.BaseURL(""), sessionMaxAge)

```

This `newSessionStore` function creates a `databaseSessionStore` struct that implements the gorilla/sessions `Store` interface while replacing the default cookie storage with database persistence.

## Database-Backed Session Architecture

Hister's session storage separates the session identifier from the session payload, using cryptographic hashing for lookups while keeping the actual session values in the database.

### Session Token Generation and Hashing

For every new session, Hister generates a **32-byte random token** (`sessionTokenBytes = 32`) that serves as the client-side cookie value. However, the database lookup key is not the raw token but rather its **SHA-256 hash** (`sessionTokenHash`). This design ensures that even if the database is compromised, session tokens cannot be directly correlated with active cookies because only the hashes are stored.

### Web Sessions Database Schema

Session data is persisted in the `web_sessions` table via model functions including `model.GetWebSession`, `model.UpdateWebSession`, and `model.DeleteWebSession`. Each record contains:

*   `token_hash` – The SHA-256 hash of the session token used for database lookups
*   `data` – The gob-encoded `session.Values` containing the actual session state
*   `expires_at` – The absolute timestamp when the session should be invalidated

This schema allows Hister to store complex Go data structures in the database while keeping client-side cookies small and opaque.

## Session Lifecycle Management

The `databaseSessionStore` in [`server/session.go`](https://github.com/asciimoo/hister/blob/main/server/session.go) implements standard session operations including creation, retrieval, and persistence, with all state changes reflected in the database rather than the cookie.

### Creating New Sessions

The `New(r, name)` method initializes a fresh `sessions.Session` with default options and marks it as new. When `Save` is subsequently called, if `session.ID` is empty, the store generates a new random token, hashes it, and creates a database record:

```go
// server/session.go – Save()
if session.ID == "" {
    token := make([]byte, sessionTokenBytes) // 32-byte random token
    // ... encode Values into data
    model.CreateWebSession(sessionTokenHash(token), data, now.Add(s.maxAge))
    session.ID = token
}

```

### Loading Existing Sessions

The `Get(r, name)` method inspects incoming requests for existing session cookies. If a cookie is present, the store validates the token by looking up the hashed value in the database:

```go
// server/session.go – Get()
cookie, err := r.Cookie(name)
if err == nil {
    record, err := model.GetWebSession(sessionTokenHash(cookie.Value))
    if err == nil {
        decodeSessionValues(record.Data, &session.Values)
        session.ID = cookie.Value
        session.IsNew = false
    }
}

```

If the database lookup fails (invalid or expired token), a new session is created transparently.

### Persisting Session Data

When `Save` is called, the store checks whether the session is new or existing. For existing sessions, it updates the database row with the current gob-encoded values. The cookie written to the client contains only the raw token, making it impossible for users to inspect or modify their session data.

## Security Features and Token Rotation

Hister implements several security mechanisms to prevent session fixation and ensure clean authentication boundaries.

### Session Token Rotation

The `Rotate` function deletes the old database row and marks the session as new, forcing the next `Save` operation to generate a fresh token. This is typically invoked after authentication events to prevent session fixation attacks, ensuring that the authenticated session ID differs from the pre-authentication session ID.

### Authentication and Logout Handling

After successful authentication, the `authenticateToken` and `authenticateUser` functions store a proof token in `session.Values`. The `resetSessionValuesAfterAuthentication` helper preserves only the CSRF token while clearing other values to prevent privilege escalation from stale session data.

For logout operations, setting `session.Options.MaxAge < 0` triggers deletion logic in `Save` that removes the database row and clears the client cookie:

```go
// server/session.go – Save()
if session.Options.MaxAge < 0 && session.ID != "" {
    model.DeleteWebSession(sessionTokenHash(session.ID))
    http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
}

```

## Implementation Files and Key Functions

Understanding Hister's session management requires familiarity with these specific source files:

*   **[`server/session.go`](https://github.com/asciimoo/hister/blob/main/server/session.go)** – Contains the complete `databaseSessionStore` implementation, including `New`, `Get`, `Save`, and `Rotate` methods
*   **[`server/server.go`](https://github.com/asciimoo/hister/blob/main/server/server.go)** – Handles store initialization with the secret key and integrates sessions into the HTTP request lifecycle
*   **[`config/config.go`](https://github.com/asciimoo/hister/blob/main/config/config.go)** – Provides `Config.SecretKey()` for secure key management
*   **[`model/websession.go`](https://github.com/asciimoo/hister/blob/main/model/websession.go)** – Defines the database schema and CRUD operations for the `web_sessions` table

## Summary

*   Hister stores session data in a **database table** (`web_sessions`) rather than in client-side cookies, using gob encoding for the `session.Values` payload.
*   The **secret key** (configured via `HISTER_SECRET_KEY` and accessed through `cfg.SecretKey()`) is used solely to sign cookies, not to encrypt session data, which remains server-side.
*   Session identifiers are **32-byte random tokens** transmitted to clients, but the database uses **SHA-256 hashes** of these tokens for lookups, protecting against database compromise.
*   The session lifecycle is managed through `databaseSessionStore` methods in [`server/session.go`](https://github.com/asciimoo/hister/blob/main/server/session.go), with explicit support for **token rotation** during authentication to prevent fixation attacks.
*   Logout is implemented by setting `MaxAge` to a negative value, which triggers database row deletion and cookie clearing in the `Save` method.

## Frequently Asked Questions

### Where does Hister store session data?

Hister stores session data server-side in a relational database table called `web_sessions`. The actual session values are gob-encoded and stored in a `data` column, while a SHA-256 hash of the session token serves as the lookup key. This differs from default gorilla/sessions implementations that store serialized data directly in the cookie.

### How does Hister generate and manage session tokens?

Hister generates cryptographically secure 32-byte random tokens for each session. The raw token is sent to the client as a signed cookie, but only the SHA-256 hash (`sessionTokenHash`) is stored in the database. Through the `Rotate` method in [`server/session.go`](https://github.com/asciimoo/hister/blob/main/server/session.go), Hister can invalidate old tokens and generate new ones, which is automatically triggered after successful authentication to prevent session fixation.

### What happens to session data when a user logs out?

When a logout occurs, Hister sets `session.Options.MaxAge` to a negative value. The next call to `Save` detects this condition and invokes `model.DeleteWebSession` to remove the database row using the hashed token, then sends an empty cookie to the client. This ensures complete server-side session invalidation rather than relying solely on cookie expiration.

### How is the secret key configured in Hister?

The secret key is configured through the `Config` struct in [`config/config.go`](https://github.com/asciimoo/hister/blob/main/config/config.go), typically reading from an environment variable such as `HISTER_SECRET_KEY`. This key is passed to `newSessionStore` during server initialization in [`server/server.go`](https://github.com/asciimoo/hister/blob/main/server/server.go) and is used to sign session cookies cryptographically. The key itself is never stored in the database and remains in memory only during application runtime.