Stateless, encrypted, type-safe session cookies for Go's
net/httpstack.
seshcookie enables you to associate session-state with HTTP requests while keeping your server stateless. Session data travels with each request inside a single AES-GCM encrypted cookie, so restarts, blue/green deploys, or load-balanced replicas do not require sticky routing or a cache tier. The package is inspired by Beaker and mirrors the authoritative go doc github.com/bpowers/seshcookie/v3 description: cookies are authenticated/encrypted with a key derived via Argon2id every time NewHandler/NewMiddleware is constructed. Each request gets a strongly-typed protobuf message via context.Context; mutate it, call SetSession, and seshcookie handles encryption, authentication, expiry, and change detection for you.
If you need to centrally revoke sessions, store large payloads, or share state with non-HTTP clients, a server-side store may be a better fit.
SetSession.http.Handler or a middleware constructor.go get github.com/bpowers/seshcookie/v3
Create a .proto file:
syntax = "proto3";
package myapp;
option go_package = "myapp/pb";
message UserSession {
string username = 1;
int32 visit_count = 2;
repeated string roles = 3;
}
Generate Go code:
protoc --go_out=. --go_opt=paths=source_relative session.proto
Wrap your top-level handler (or router) with seshcookie. Provide a high-entropy key that is shared by every replica of your service.
key := os.Getenv("SESHCOOKIE_KEY") // base64 string holding 32 random bytes
handler, err := seshcookie.NewHandler[*pb.UserSession](
&VisitedHandler{},
key,
&seshcookie.Config{
HTTPOnly: true,
Secure: true,
MaxAge: 24 * time.Hour,
},
)
if err != nil {
log.Fatalf("NewHandler: %v", err)
}
log.Fatal(http.ListenAndServe(":8080", handler))
Prefer middleware-style wiring when you already have a router (e.g., http.ServeMux, chi, gorilla/mux):
mw, err := seshcookie.NewMiddleware[*pb.UserSession](key, nil)
if err != nil {
log.Fatal(err)
}
router := http.NewServeMux()
router.HandleFunc("/", appHandler)
log.Fatal(http.ListenAndServe(":8080", mw(router)))
Within any wrapped handler, call the helpers on the request context. The session is lazily created on first access and only written back when SetSession (or ClearSession) is invoked.
session, err := seshcookie.GetSession[*pb.UserSession](req.Context())
if err != nil {
http.Error(rw, "session unavailable", http.StatusInternalServerError)
return
}
session.VisitCount++
if err := seshcookie.SetSession(req.Context(), session); err != nil {
http.Error(rw, "could not save session", http.StatusInternalServerError)
return
}
if shouldLogout(req) {
_ = seshcookie.ClearSession[*pb.UserSession](req.Context()) // drops cookie at end of request
http.Redirect(rw, req, "/login", http.StatusSeeOther)
return
}
go doc)go doc github.com/bpowers/seshcookie/v3 is the source of truth for exported API semantics. The key entry points are:
GetSession[T proto.Message](ctx context.Context) (T, error) retrieves the typed protobuf message from context, auto-creating a zero instance (never nil) if no cookie is present. It returns ErrNoSession if the context was not seeded by seshcookie.SetSession[T proto.Message](ctx context.Context, session T) error marks the session as changed so the cookie is rewritten at the end of the request.ClearSession[T proto.Message](ctx context.Context) error deletes the session and instructs the response writer to expire the cookie.NewHandler[T proto.Message](handler http.Handler, key string, cfg *Config) (*Handler[T], error) and NewMiddleware[T proto.Message](key string, cfg *Config) (func(http.Handler) http.Handler, error) wrap an existing http.Handler/router. They derive an AES key from key using Argon2id and store configuration in a Handler[T] that you can pass directly to http.ListenAndServe.DefaultConfig exposes the defaults used when cfg is nil (cookie name session, path /, HTTPOnly: true, Secure: true, MaxAge: 24 * time.Hour).Sessions live in request context until you call SetSession or ClearSession, so read-only requests avoid cookie writes and preserve the original issued_at timestamp.
CookieName (default "session"): cookie name.CookiePath (default /): path scope.HTTPOnly (default true): prevents JavaScript access.Secure (default true): only send over HTTPS; disable only for local development.MaxAge (default 24 * time.Hour): server-side TTL based on issuance time.crypto/rand (32+ bytes), store it outside source control, and keep it consistent across replicas so cookies remain decryptable everywhere.Secure and HTTPOnly enabled, and terminate TLS before requests hit seshcookie. Toggle Secure off only for local HTTP development.MaxAge that matches your authentication policy, and rotate the key when you need to invalidate all sessions at once.SetSession only when data actually changes; combine with domain logic (e.g., bump visit counts, persist auth claims) to avoid needless cookie churn.ClearSession on logout/revocation flows and pair seshcookie with CSRF protection for state-changing requests.MaxAge determines validity, so clients cannot prolong sessions.You still need standard web security measures (TLS, CSRF tokens, input validation) around your application logic.
SessionEnvelope carrying the payload and issued_at metadata.issued_at + MaxAge before exposing the session to your handler.SetSession or ClearSession, allowing long-lived sessions with stable issuance timestamps.Version 3.0 updates the module path to comply with Go's semantic import versioning requirements:
Migration steps:
github.com/bpowers/seshcookie to github.com/bpowers/seshcookie/v3.go mod tidy to update your dependencies.That's it! The API remains the same as v2.x.
Version 2.0/3.0 is a breaking change from v1.x. Key differences:
| v1.x | v2.x/v3.x |
|---|---|
Session map[string]interface{} |
Strongly-typed protobuf messages |
GetSession(ctx) Session |
GetSession[T](ctx) (T, error) |
| Direct map modification | Explicit SetSession(ctx, session) |
NewHandler(h, key, cfg) *Handler |
NewHandler[T](h, key, cfg) (*Handler[T], error) |
| No expiry enforcement | Server-side expiry via MaxAge |
| GOB encoding | Protobuf encoding |
Migration steps:
github.com/bpowers/seshcookie/v3.protoc.GetSession[T], SetSession, and ClearSession.NewHandler and session operations.A complete authentication example is available in the example/ directory, demonstrating:
- Login/logout flows
- Protobuf session messages
- Role-based access control
- Proper error handling
seshcookie is offered under the MIT license; see LICENSE for details.
$ claude mcp add seshcookie \
-- python -m otcore.mcp_server <graph>