- doc/deployment.md: run familiar as a remote MCP server — secure defaults,
bearer vs OAuth modes, the full env-var reference, one-time login / token-store
provisioning, the published images, docker compose, and TLS reverse-proxy
blocks for nginx (proxy_buffering off + long read timeout), Caddy, and Traefik.
- doc/clients/: one page per client with exact working steps — Claude Code,
VS Code (.vscode/mcp.json with a masked token input, and the workspace
.mcp.json header gotcha), Mistral Le Chat, Claude Desktop + Cowork (mcp-remote
bridge, per-OS config paths), and Claude web (oauth mode + external IdP, with
"use Desktop + bearer" as the simpler alternative). Plus a clients/ index.
- README: a "Connect a client" matrix (client -> auth -> guide) and a compact
"Deploy with Docker" section linking to doc/deployment.md; oauth mode noted.
- doc/README.md index: rows for Deployment and Client setup.
- doc/getting-started.md: pointer from the HTTPS quickstart to the full docs.
- CLAUDE.md: FAMILIAR_AUTH_MODE / FAMILIAR_OAUTH_* / FAMILIAR_ALLOW_INSECURE_BIND
and /healthz in the transport note; the split ci.yml / docker.yml workflows.
Deployment docs live under the existing doc/ tree (the repo uses doc/, not a
separate docs/) to keep a single documentation directory.
New .gitea/workflows/docker.yml (alongside the untouched ci.yml wheel publish).
Runs lint + test, then builds the image and smoke-tests it — non-root uid,
/healthz 200, MCP 401 without a token and 200 with a valid bearer. On a merge to
main/dev it pushes to git.hiddenden.cafe/hiddenden/familiar (main -> :latest +
:main, dev -> :dev, plus :sha-<12>) via the existing REGISTRY_TOKEN, then does a
scoped cleanup. The two workflows are independent and don't interfere.
- docker/Dockerfile: multi-stage uv wheel build; runtime runs `familiar serve`
as a non-root user (uid 1000) on port 8080, /data volume for the encrypted
token store, and a HEALTHCHECK against /healthz. No token is baked in — serve
refuses to start without FAMILIAR_HTTP_TOKEN, so it's supplied at run time.
- docker-compose.yml: joins an external `proxy` network (Traefik labels for TLS,
nginx/Caddy documented as alternatives), read-only rootfs, tmpfs /tmp,
no-new-privileges, cap_drop ALL, non-root, /healthz healthcheck. Pulls the
published FAMILIAR_IMAGE with a local build as fallback; writes default off.
- .env.example: every FAMILIAR_* knob incl. the bearer token, auth mode, the
OAuth resource-server settings, and the insecure-bind escape hatch.
- .dockerignore: keep local state (data/, *.enc, key) and dev files out.
Extend the HTTP transport (from the remote-MCP work) so it's production-ready.
- /healthz: an unauthenticated GET liveness route that returns only
{status, server, version} — no account data, no token state. For Docker and
reverse-proxy probes; on stdio the route is never served.
- Fail-closed bind: refuse to start without authentication, and make the
message distinguish a loopback bind from a public (0.0.0.0) one. The only
override is the explicit FAMILIAR_ALLOW_INSECURE_BIND=true escape hatch, which
permits an unauthenticated bind (with a loud warning).
- FAMILIAR_AUTH_MODE=oauth: run as an OAuth 2.1 resource server. A JWTVerifier
validates external-issuer JWTs (JWKS from FAMILIAR_OAUTH_JWKS_URI, else
discovered from FAMILIAR_OAUTH_ISSUER's OIDC metadata) against issuer +
audience, wrapped in a RemoteAuthProvider that serves
/.well-known/oauth-protected-resource so OAuth-only clients (Claude web) can
discover the authorization server. familiar is a resource server only — no
authorization server, no DCR. bearer mode is unchanged and stays the default.
Tests cover healthz (200, no leak), fail-closed vs escape hatch, and oauth
(valid JWT 200; missing/expired/wrong-audience 401 + WWW-Authenticate; the
protected-resource metadata), plus the new config readers.