How to run and debug session-manager directly on your machine (binary on the host, backing services in Docker). This is the fast inner-loop workflow: you can set breakpoints and restart in seconds.
This is not the k3d/Helm deployment (
make start) documented in the README. Use that when you need to test the containerized service end-to-end; use this when you're developing and debugging the Go code.
- Go (see
go.modfor the version) - Docker (with Compose v2 —
docker compose, notdocker-compose) buf— for gRPC calls (grpcurldoes not work here; see gRPC)- Optional, for step debugging:
dlv(go install github.com/go-delve/delve/cmd/dlv@latest)
make dev-deps # start Postgres, Valkey, Dex (waits until healthy)
make migrate # build + apply DB migrations
make run # build + run the api-server in the foregroundmake run blocks and streams logs. Open a second terminal for requests. Stop it
with Ctrl-C; stop the dependencies with make dev-deps-down.
Run make help to list all the dev targets.
| Component | Where | Address | Credentials |
|---|---|---|---|
| api-server | host (binary) | REST :8080, gRPC :9091, status :8888 |
— |
| Postgres | Docker Compose | localhost:5432 |
postgres / secret, db session_manager |
| Valkey | Docker Compose | localhost:6379 |
password secret |
| Dex (IdP) | Docker Compose | localhost:5556 |
user admin@example.com / password; client my-client / secret |
Config comes from ./config.yaml (search order: /etc/session-manager/, then
$HOME/.session-manager/, then ./). It already points at these localhost
addresses with the credentials above. The compose stack lives in
dev/docker-compose.yaml; the Dex config is in
dev/dex/config.yaml.
The binary has four subcommands:
| Subcommand | Purpose |
|---|---|
api-server |
Runs the REST + gRPC servers (this is what you run) |
migrate |
Applies DB migrations (embedded, via goose) |
housekeeper |
Periodic session cleanup + token refresh |
version |
Prints build info |
curl localhost:8888/probe/liveness # process is alive
curl localhost:8888/probe/readiness # {"status":"up"} => DB + Valkey reachable
curl localhost:8888/versionNote the /probe/ prefix — there is no /healthz.
grpcurl cannot introspect these services — the protos use proto edition 2024,
which grpcurl's runtime rejects (EDITION_2024 not yet supported). Use
buf curl, which works over server reflection with no local proto files:
# List every method
buf curl --protocol grpc --http2-prior-knowledge http://localhost:9091 --list-methods
# Register a tenant's trust mapping (REQUIRED before /sm/auth works for it)
buf curl --protocol grpc --http2-prior-knowledge \
-d '{"tenant_id":"demo","oidc":{"issuer":"http://localhost:5556/dex","client_id":"my-client","audiences":["my-client"]}}' \
http://localhost:9091/kms.api.cmk.sessionmanager.trustmapping.v1.Service/ApplyTrustMapping
# Read it back
buf curl --protocol grpc --http2-prior-knowledge -d '{"tenant_id":"demo"}' \
http://localhost:9091/kms.api.cmk.sessionmanager.session.v1.Service/GetTrust
# Validate a session (session_id comes from the login flow below)
buf curl --protocol grpc --http2-prior-knowledge -d '{"session_id":"<id>","tenant_id":"demo"}' \
http://localhost:9091/kms.api.cmk.sessionmanager.session.v1.Service/GetSession/sm/auth requires a trust mapping for the tenant (create one with
ApplyTrustMapping above) — otherwise it returns 404. Once seeded:
curl -i "http://localhost:8080/sm/auth?tenant_id=demo&request_uri=http://localhost:8080/"This returns a 302 to the IdP (Dex) authorization endpoint with a PKCE
challenge, plus the login-CSRF cookie. In a browser you log in at Dex and get
redirected back to /sm/callback, which exchanges the code for tokens and sets
the SESSION-<tenant> and CSRF-<tenant> cookies.
With dev-deps up and a trust mapping seeded for demo (pointing at
http://localhost:5556/dex), open this in a browser and log in as
admin@example.com / password:
http://localhost:8080/sm/auth?tenant_id=demo&request_uri=http://localhost:8080/
You'll land back on http://localhost:8080/ with session cookies set. Feed the
SESSION-demo cookie value to gRPC GetSession to confirm it validates.
Dex is served over plain http://, which two settings in config.yaml enable
for local dev (both default to the hardened behavior in production):
sessionManager.allowHttpScheme: trueand the gRPC session service'sallowHttpScheme: true— permithttp://OIDC issuers (production: https only).sessionManager.loginCSRFCookieTemplate.name: "LoginCSRF"— the default__Host-LoginCSRFname requires the cookie'sSecureattribute, which browsers reject over http. Leavingnameunset restores the hardened default.
Because the binary runs on the host, you can attach a debugger with breakpoints.
All approaches below use Delve
(go install github.com/go-delve/delve/cmd/dlv@latest); make sure your Delve
version supports your installed Go toolchain.
No editor integration required:
make dev-deps
dlv debug ./cmd/session-manager -- api-server
# (dlv) break internal/session/manager.go:117
# (dlv) continue
# ... fire a request from another terminal, then: next / step / print <var>Swap api-server for migrate or housekeeper to debug the other subcommands.
Editor-specific config files (.vscode/, .dir-locals.el, etc.) are not
committed — set this up locally in whatever editor you use. Delve speaks the
Debug Adapter Protocol (DAP), which every mainstream editor debug client
(VS Code, Neovim nvim-dap, Emacs dape/dap-mode, JetBrains GoLand's native
debugger) can drive. Point your editor's Go/Delve debug configuration at:
| Setting | Value |
|---|---|
| debugger / adapter | dlv dap (Delve in DAP mode) |
| request | launch |
| mode | debug (build from source) |
| program | ./cmd/session-manager (the main package) |
| args | ["api-server"] (or ["migrate"] / ["housekeeper"]) |
| working directory | the repository root (so the binary finds config.yaml) |
To attach to an already-running make run process instead of launching a
new one, use request: attach, mode: local, and the PID from
pgrep -f 'session-manager api-server'.
Working examples for two popular editors follow. These files are gitignored — copy them into your local checkout as-is.
Requires the Go extension
(which bundles Delve). ${workspaceFolder} resolves to the repo root.
Requires dape. The eval form registers
the launch/attach configs when you open a Go file (Emacs prompts once to mark it
safe); the project root is resolved dynamically, and session-manager-attach
auto-discovers the running PID via pgrep.
;;; Directory Local Variables -*- no-byte-compile: t; -*-
((go-ts-mode
. ((eval
. (let ((root (when-let ((d (locate-dominating-file
(or (buffer-file-name) default-directory)
"go.mod")))
(expand-file-name d))))
(when (and root (require 'dape nil t))
(dolist (cfg
`((session-manager-api-server
modes (go-mode go-ts-mode) ensure dape-ensure-command
command "dlv"
command-args ("dap" "--listen" "127.0.0.1::autoport")
command-cwd ,root port :autoport
:type "debug" :request "launch" :mode "debug"
:program ,(expand-file-name "cmd/session-manager" root)
:cwd ,root :args ["api-server"])
(session-manager-migrate
modes (go-mode go-ts-mode) ensure dape-ensure-command
command "dlv"
command-args ("dap" "--listen" "127.0.0.1::autoport")
command-cwd ,root port :autoport
:type "debug" :request "launch" :mode "debug"
:program ,(expand-file-name "cmd/session-manager" root)
:cwd ,root :args ["migrate"])
(session-manager-housekeeper
modes (go-mode go-ts-mode) ensure dape-ensure-command
command "dlv"
command-args ("dap" "--listen" "127.0.0.1::autoport")
command-cwd ,root port :autoport
:type "debug" :request "launch" :mode "debug"
:program ,(expand-file-name "cmd/session-manager" root)
:cwd ,root :args ["housekeeper"])
(session-manager-attach
modes (go-mode go-ts-mode) ensure dape-ensure-command
command "dlv"
command-args ("dap" "--listen" "127.0.0.1::autoport")
command-cwd ,root port :autoport
:type "debug" :request "attach" :mode "local"
:processId
,(lambda ()
(let ((pids (split-string
(shell-command-to-string
"pgrep -f 'session-manager api-server'")
nil t)))
(cond ((= (length pids) 1) (string-to-number (car pids)))
(pids (string-to-number
(completing-read "Attach to PID: " pids nil t)))
(t (read-number "PID to attach: ")))))
:cwd ,root)))
(setf (alist-get (car cfg) dape-configs) (cdr cfg))))))
;; If you use lsp-mode + golangci-lint, mirror the integration build tag:
(go-ts-mode-build-tags . ("integration"))
(lsp-go-env . ((GOFLAGS . "-tags=integration"))))))- Neovim —
leoluz/nvim-dap-go(or a rawnvim-dapadapter runningdlv dap) with the program/args/cwd from the table above. - JetBrains GoLand — a Go Build run/debug configuration with Run kind:
Package, the
cmd/session-managerpackage path, and program argumentsapi-server; GoLand ships its own Delve integration.
Logs are JSON. Pipe through jq to read them, and bump the level in
config.yaml (logger.level: debug) for detail:
make run 2>&1 | jq .
make run 2>&1 | jq 'select(.level=="ERROR")'Each request carries a requestId you can grep on.
# Postgres
PGPASSWORD=secret psql -h localhost -U postgres -d session_manager -c '\dt'
PGPASSWORD=secret psql -h localhost -U postgres -d session_manager -c 'SELECT * FROM trust;'
# Valkey (sessions + OIDC state, prefixed session-manager:)
redis-cli -h localhost -a secret --scan --pattern 'session-manager:*'# stop the server: Ctrl-C in its terminal (or: pkill -f "session-manager api-server")
make dev-deps-down # stop and remove the containers
{ "version": "0.2.0", "configurations": [ { "name": "api-server", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceFolder}/cmd/session-manager", "cwd": "${workspaceFolder}", "args": ["api-server"] }, { "name": "migrate", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceFolder}/cmd/session-manager", "cwd": "${workspaceFolder}", "args": ["migrate"] }, { "name": "housekeeper", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceFolder}/cmd/session-manager", "cwd": "${workspaceFolder}", "args": ["housekeeper"] }, { "name": "attach (running api-server)", "type": "go", "request": "attach", "mode": "local", "processId": "session-manager" } ] }