Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions hindsight-docs/docs/developer/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ Validates [Supabase](https://supabase.com) JWTs and gives each authenticated use
Up to 0.9.2 this extension was built in, at `hindsight_api.extensions.builtin.supabase_tenant`. That path no longer exists, so an install still pointing at it fails at startup with `ModuleNotFoundError`. Add the extension to your image and set `HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_supabase_tenant:SupabaseTenantExtension`. All `HINDSIGHT_API_TENANT_*` settings and the schema naming are unchanged.
:::

**External: StaticKeysTenantExtension**

A fully self-hosted multi-user mode: users and their API keys are declared in environment variables (no external identity provider, no users table). Each user maps to their own PostgreSQL schema (`{prefix}_{user_id}`), provisioned lazily on first access, giving database-level memory isolation between users. Multiple API keys may map to the same user and schema.

User IDs are case-insensitive: they are lowercased (and dashes normalized to underscores) before building the schema name, so `Rafael`, `rafael` and `RAFAEL` all resolve to the same tenant schema.

It lives in the [extensions registry](https://github.com/vectorize-io/hindsight/tree/main/hindsight-extensions/static-keys-tenant), which documents its configuration and ships a Dockerfile that builds an image with it.

For other multi-tenant setups with separate schemas per tenant (e.g., custom JWT-based auth), implement a custom `TenantExtension`.

---
Expand Down
1 change: 1 addition & 0 deletions hindsight-extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Hindsight that copies the extension in — see [Packaging](#packaging-an-extensi
| Extension | Slot | What it does |
| --- | --- | --- |
| [`supabase-tenant`](./supabase-tenant) | `TENANT` | Validates [Supabase](https://supabase.com) Auth JWTs and gives each user their own Postgres schema |
| [`static-keys-tenant`](./static-keys-tenant) | `TENANT` | Authenticates static API keys from env vars and gives each user their own Postgres schema |

Extensions maintained outside this repository can be listed here too — open a PR
adding a row that links to yours.
Expand Down
32 changes: 32 additions & 0 deletions hindsight-extensions/static-keys-tenant/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# A Hindsight image with the static-keys tenant extension in it.
#
# Extensions are not bundled with the server and are not published to PyPI:
# you ship one by copying it into an image built on top of Hindsight.
#
# Build from the repository root so the extension sources are in context:
# docker build -f hindsight-extensions/static-keys-tenant/Dockerfile \
# -t hindsight-with-static-keys .
#
# Run:
# docker run -p 8888:8888 \
# -e HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_static_keys_tenant:StaticKeysTenantExtension \
# -e HINDSIGHT_API_TENANT_USERS=user1:key1,user2:key2 \
# hindsight-with-static-keys
#
# Override the base with `--build-arg HINDSIGHT_IMAGE=...:latest-slim` if you
# don't need the bundled local embedding/reranking models.
ARG HINDSIGHT_IMAGE=ghcr.io/vectorize-io/hindsight:latest
FROM ${HINDSIGHT_IMAGE}

# The static-keys extension has no third-party dependencies beyond the server
# itself — nothing to pip install here.

# Put the extension on the server's import path. /app/extensions is ours — the
# image does not use it — so this cannot shadow anything the server ships.
COPY hindsight-extensions/static-keys-tenant/hindsight_ext_static_keys_tenant \
/app/extensions/hindsight_ext_static_keys_tenant
ENV PYTHONPATH=/app/extensions

# Fail the build, rather than the first authenticated request, if the extension
# is not importable from the interpreter the server actually runs.
RUN /app/api/.venv/bin/python -c "import hindsight_ext_static_keys_tenant"
81 changes: 81 additions & 0 deletions hindsight-extensions/static-keys-tenant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Static-keys tenant extension

A Hindsight `TenantExtension` that authenticates requests with static API keys
declared in environment variables and gives every user their own PostgreSQL schema,
so memories are isolated at the database level.

- **Self-hosted, no identity provider**: users and keys come from the environment,
no external IdP, no users table.
- **Schema per user**: a user `rafael` gets the schema `user_rafael` (lowercased,
dashes become underscores), migrated on first access and cached afterwards.
- **Multiple keys per user**: `rafael:key1,rafael:key2` both authenticate as `rafael`
into the same schema.
- **Constant-time key comparison** with `hmac.compare_digest` on every request.
- **Fail-fast on misconfiguration**: invalid entries, duplicate keys, schema-name
collisions and over-long schema names are rejected at startup.

> This is a newer, dependency-free complement to
> [`supabase-tenant`](../supabase-tenant): where Supabase is the source of identity,
> this one is for fully self-hosted, single-node multi-user deployments with a
> handful of statically configured users.

## Install

Extensions are not published to PyPI. Build an image with this one in it, from the
repository root:

```bash
docker build -f hindsight-extensions/static-keys-tenant/Dockerfile -t hindsight-with-static-keys .
```

See the [Dockerfile](./Dockerfile) for what it does, and the
[packaging guide](../README.md#packaging-an-extension) for the general pattern.

To run the server outside Docker, put `hindsight_ext_static_keys_tenant/` on the
`PYTHONPATH` of the environment Hindsight runs in. There are no extra dependencies
to install — the extension only uses the server's own extension interfaces.

## Configure

```bash
HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_static_keys_tenant:StaticKeysTenantExtension
HINDSIGHT_API_TENANT_USERS=user1:key1,user1:key2,user2:key3
```

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `HINDSIGHT_API_TENANT_USERS` | yes | — | Comma-separated `user_id:api_key` pairs. Multiple keys may map to the same user |
| `HINDSIGHT_API_TENANT_SCHEMA_PREFIX` | no | `user` | Schema name prefix; must be a valid Postgres identifier |
| `HINDSIGHT_API_TENANT_MCP_AUTH_DISABLED` | no | `false` | Skip authentication for MCP endpoints (MCP clients land in the base schema) |

User IDs are **case-insensitive** and normalized before building the schema name:
they are lowercased and dashes become underscores (`Rafael`, `rafael` and `RAFAEL`
all resolve to `user_rafael`), matching how PostgreSQL folds unquoted identifiers.
Two distinct users whose ids collide after normalization (e.g. `jane-doe` vs
`jane_doe`, or ids longer than the 63-byte identifier limit) are rejected at startup.

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 pass their configured API key as a bearer token:

```bash
curl -H "Authorization: Bearer <key1>" \
http://localhost:8888/v1/default/banks
```

Unknown or missing keys get a 401; every key is compared in constant time.

## Develop

```bash
uv sync
uv run pytest tests -v
```

## License

MIT.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Static-keys tenant extension for the Hindsight API server.

Configure the server to load it with::

HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_static_keys_tenant:StaticKeysTenantExtension
"""

from hindsight_ext_static_keys_tenant.extension import StaticKeysTenantExtension

__all__ = ["StaticKeysTenantExtension"]
Loading