* feat(extensions): add hindsight-extensions registry and unbundle Supabase
Extensions were only ever bundle-able or nothing: shipping one meant putting
it in `hindsight_api.extensions.builtin`, where it becomes maintainer-owned
forever, lands in every image, and — because `extensions/__init__.py` eagerly
re-exported every implementation — drags its dependencies into core's import
graph. That pipe is why a third-party IdP's JWT client was a direct dependency
of every Hindsight install.
Add `hindsight-extensions/` as the registry for extensions distributed
separately from the server. Its README is the contract: slots and how config
env vars map onto them, how to write an extension, the package layout and
naming (`hindsight-extensions/<name>/` -> `hindsight-ext-<name>` ->
`hindsight_ext_<name>`), and Docker packaging.
Move `SupabaseTenantExtension` there as the first entry, published as
`hindsight-ext-supabase-tenant`. All 54 of its tests move with it, plus two new
ones asserting the documented `hindsight_ext_supabase_tenant:...` env value
actually resolves through `load_extension`.
Two decisions worth their comments:
- The extension does NOT declare `hindsight-api-slim` as a runtime dependency.
The server is the host process that imports it, not something it installs;
declaring it would let `pip install` of an extension silently move the server
version underneath a running deployment. It is a dev extra, resolved from the
local checkout via `tool.uv.sources` (dev-only metadata, verified absent from
the built wheel).
- The Docker example installs with `uv pip install --python
/app/api/.venv/bin/python`, matching docker-compose/custom-models: the image's
venv was created by `uv sync` and ships no `pip`, so a bare `pip install`
lands in user site-packages and is invisible to the server.
Core changes:
- `extensions/__init__.py` and `builtin/__init__.py` export interfaces only.
Nothing needed the concrete re-exports — the loader imports by path — and
dropping them is what lets an extension have optional dependencies at all.
- `builtin/supabase_tenant.py` stays for one minor release as a module whose
`__getattr__` raises the migration instructions. `load_extension` wraps a
missing *attribute*, not a failed import, so the ImportError propagates with
its message intact instead of surfacing as "class not found".
- Drop the direct `PyJWT[crypto]` dependency: no core module imports `jwt` any
more. Note this does not shrink the install — `mcp` pulls pyjwt transitively
and `cryptography` is already pinned directly — so the win here is ownership
and import graph, not bytes.
Locks are not checked in for extensions: `tool.uv.sources` pins the whole
api-slim tree, so every core dependency bump would leave them stale. CI runs
`uv sync --extra dev` and retriggers on `core` changes, since these tests run
against the server's interfaces.
Docs point at the registry rather than restating it, and the Deploying section's
Docker recipe was replaced — it named an image (`vectorize/hindsight-api`) and a
PYTHONPATH volume-mount pattern that no longer exist.
Also includes two one-line generated-file syncs in skills/hindsight-docs
(quickstart, installation) that were already stale on main; regenerating the
docs skill picks them up.
* refactor(extensions): ship extensions by image, drop the compat shim
Follow-up on review. Three changes to how an extension is distributed:
- Delete `builtin/supabase_tenant.py`. An install pinned to the old path now
fails at startup with ModuleNotFoundError rather than a guided message. The
docs carry the migration instead.
- Extensions are not published to PyPI. There is no wheel, no version and no
release step: the unit of distribution is an image built on top of Hindsight
that installs the extension's dependencies and copies the package onto
PYTHONPATH. That drops the whole "declare hindsight-api-slim only as a dev
extra" problem — nothing resolves dependencies against a running server any
more.
- The pyproject is now test-harness only (`package = false`, no build backend,
no distribution metadata), and says so in a comment so nobody re-adds
packaging to it.
Docs say 0.9.3, not 0.10.
Since the Dockerfile is now the distribution mechanism rather than an example,
CI builds it — its final `import` step is the only thing proving the extension
is reachable from the interpreter the server actually runs. It builds against
`:latest-slim` via a HINDSIGHT_IMAGE build arg to keep the pull cheap.
Verified against the real image, not just locally:
docker build -f hindsight-extensions/supabase-tenant/Dockerfile \
--build-arg HINDSIGHT_IMAGE=ghcr.io/vectorize-io/hindsight:latest-slim ...
-> load_extension('TENANT', TenantExtension) inside the container returns
SupabaseTenantExtension with its config resolved from the env vars.
Worth noting from that build: `uv pip install 'PyJWT[crypto]' httpx` reports
"Checked 2 packages" — both are already in the base image transitively. The
line stays because the extension should pin what it imports rather than rely on
the server's transitive tree, but it costs nothing today.
56 extension tests pass; the 3 remaining core tests (which assert no
implementation is re-exported and no core module imports jwt) pass.
* fix(tests): import ApiKeyTenantExtension from its module, not the package
Dropping the concrete re-exports from `hindsight_api.extensions` broke
`tests/test_extensions.py`, which imported `ApiKeyTenantExtension` from the
package inside a multi-line parenthesised import. A collection ImportError
fails the whole shard, which is why all three test-api shards and all six LLM
acceptance jobs went red at once on the previous push.
I'd checked for this with a single-line grep, which cannot see a name inside a
parenthesised import list. Re-checked with an AST scan over every package in
the repo (this was the only occurrence) and by collecting the full suite:
7780 tests collect clean.
Supabase tenant extension
A Hindsight TenantExtension that authenticates requests with Supabase
Auth JWTs and gives every user their own PostgreSQL schema, so memories are isolated
at the database level.
- Local JWT verification using the project's JWKS public keys — no network call per
request. Falls back to
/auth/v1/userfor legacy HS256 projects. - Schema per user: a user with id
a1b2…7890gets the schemauser_a1b2…7890(hyphens become underscores), migrated on first access and cached afterwards. - No user management: your existing Supabase project is the source of identity.
This extension shipped inside the Hindsight server up to 0.9.2 as
hindsight_api.extensions.builtin.supabase_tenant. It is no longer bundled — see Migrating.
Install
Extensions are not published to PyPI. Build an image with this one in it, from the repository root:
docker build -f hindsight-extensions/supabase-tenant/Dockerfile -t hindsight-with-supabase .
See the Dockerfile for what it does, and the packaging guide for the general pattern.
To run the server outside Docker, put hindsight_ext_supabase_tenant/ on the
PYTHONPATH of the environment Hindsight runs in and install PyJWT[crypto] and
httpx there.
Configure
HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_supabase_tenant:SupabaseTenantExtension
HINDSIGHT_API_TENANT_SUPABASE_URL=https://xxx.supabase.co
| Variable | Required | Default | Description |
|---|---|---|---|
HINDSIGHT_API_TENANT_SUPABASE_URL |
yes | — | Supabase project URL |
HINDSIGHT_API_TENANT_SUPABASE_SERVICE_KEY |
only for HS256 projects | — | service_role key. Needed when JWKS is unavailable, and used for the startup health check |
HINDSIGHT_API_TENANT_SCHEMA_PREFIX |
no | user |
Schema name prefix; must be a valid Postgres identifier |
If your project uses legacy HS256 signing and no service key is set, the server fails at startup rather than accepting unverifiable tokens.
Give the API and the worker the same variables: the worker calls list_tenants()
to decide which schemas to consolidate, so a worker without the extension leaves every
tenant's background processing stopped.
Use
Clients send their Supabase JWT as a bearer token:
curl -H "Authorization: Bearer <supabase_jwt>" \
https://your-hindsight-server/v1/default/banks/my-bank/memories/recall
Migrating from the built-in extension
Add the extension to your image (above), then update the extension path:
-HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.supabase_tenant:SupabaseTenantExtension
+HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_supabase_tenant:SupabaseTenantExtension
Every HINDSIGHT_API_TENANT_* setting keeps its name and meaning, the schema naming is
unchanged, and existing tenant schemas are picked up as they were before — this is a
packaging move, not a behaviour change.
Develop
uv sync
uv run pytest tests -v
License
MIT. Originally contributed by BrighterBalance.