Extensions
Extensions allow you to customize and extend Hindsight behavior without modifying core code. They enable multi-tenancy, custom authentication, additional HTTP endpoints, and operation hooks.
Available Extensions
TenantExtension
Handles multi-tenancy and API key authentication. Validates incoming requests and determines which PostgreSQL schema to use for database operations, enabling tenant isolation at the database level.
Built-in: ApiKeyTenantExtension
A simple implementation that validates API keys against an environment variable and uses the public schema for all authenticated requests.
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
HINDSIGHT_API_TENANT_API_KEY=your-secret-key
Built-in: SupabaseTenantExtension
Validates Supabase JWTs and provides multi-tenant memory isolation. Each authenticated user gets their own PostgreSQL schema ({prefix}_{user_id}), ensuring complete data separation. Performs local JWT verification using JWKS for optimal performance (no network call per request).
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.supabase_tenant:SupabaseTenantExtension
HINDSIGHT_API_TENANT_SUPABASE_URL=https://your-project.supabase.co
# Optional - only needed for legacy HS256 projects or health check
HINDSIGHT_API_TENANT_SUPABASE_SERVICE_KEY=your-service-role-key
See the source code for complete configuration options and implementation details.
For other multi-tenant setups with separate schemas per tenant (e.g., custom JWT-based auth), implement a custom TenantExtension.
HttpExtension
Adds custom HTTP endpoints under the /ext/ path prefix. Useful for adding domain-specific APIs that integrate with Hindsight's memory engine.
Provides two router methods:
get_router(memory)— returns a FastAPI router mounted at/ext/get_root_router(memory)— returns a FastAPI router mounted at the application root (for well-known endpoints or other paths that must be at specific locations). ReturnsNoneby default.
No built-in implementation - implement your own to add custom endpoints.
HINDSIGHT_API_HTTP_EXTENSION=mypackage.ext:MyHttpExtension
OperationValidatorExtension
Hooks into retain/recall/reflect operations for validation and monitoring. Use cases include:
- Rate limiting and quota enforcement
- Permission checks and content filtering
- Audit logging and usage tracking
- Custom metrics collection
No built-in implementation - implement your own based on your requirements.
HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidator
MCPExtension
Registers additional MCP (Model Context Protocol) tools on the Hindsight MCP server. Enables external packages to add custom tools without modifying core code.
No built-in implementation - implement your own to add custom MCP tools.
HINDSIGHT_API_MCP_EXTENSION=mypackage.mcp:MyMCPExtension
Writing Custom Extensions
Extension Basics
Extensions are Python classes loaded via environment variables:
HINDSIGHT_API_<TYPE>_EXTENSION=mypackage.module:MyExtensionClass
Configuration is passed via prefixed environment variables:
HINDSIGHT_API_<TYPE>_SOME_CONFIG=value
# Extension receives: {"some_config": "value"}
All extensions support lifecycle hooks:
on_startup()- Called when the application startson_shutdown()- Called when the application shuts down
Extensions have access to an ExtensionContext that provides:
run_migration(schema)- Run database migrations for a schemaget_memory_engine()- Get the MemoryEngine interface
Example: Custom TenantExtension with JWT
import jwt
from hindsight_api.extensions import TenantExtension, TenantContext, AuthenticationError
class JwtTenantExtension(TenantExtension):
def __init__(self, config: dict[str, str]):
super().__init__(config)
self.jwt_secret = config.get("jwt_secret")
if not self.jwt_secret:
raise ValueError("HINDSIGHT_API_TENANT_JWT_SECRET is required")
async def authenticate(self, context: RequestContext) -> TenantContext:
token = context.api_key
if not token:
# Optional headers dict is forwarded in HTTP/MCP error responses
raise AuthenticationError("Bearer token required")
try:
payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
tenant_id = payload.get("tenant_id")
if not tenant_id:
raise AuthenticationError("Missing tenant_id in token")
return TenantContext(schema_name=f"tenant_{tenant_id}")
except jwt.InvalidTokenError as e:
raise AuthenticationError(str(e))
AuthenticationError accepts an optional headers dict that is forwarded in both HTTP and MCP error responses. This is useful for returning custom headers like WWW-Authenticate:
raise AuthenticationError(
"Authorization required",
headers={"WWW-Authenticate": 'Bearer realm="example"'},
)
Reading additional request headers
RequestContext carries the Authorization header as api_key. To authenticate on a different header — for instance when a gateway terminates auth with one shared identity and forwards the per-caller identity separately — name the headers you want forwarded:
HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS=x-user-assertion
They are available as context.extra_headers, keyed by lower-cased name:
async def authenticate(self, context: RequestContext) -> TenantContext:
assertion = context.extra_headers.get("x-user-assertion")
if not assertion:
raise AuthenticationError("x-user-assertion header required")
user_id = verify_assertion(assertion) # your verification
return TenantContext(schema_name=f"tenant_{user_id}")
This works on both the HTTP and MCP transports, and the same RequestContext is passed to OperationValidatorExtension hooks, so a validator can enforce rules against the identity resolved here.
Only headers you list are forwarded, and only when present on the request. The variable is unset by default, so extensions see no header data unless you opt in.
A header sent more than once is not forwarded at all, and a warning is logged. There is no safe way to choose between the copies — a proxy may append its trusted value either before or after a client-supplied one — so an extension reading it sees nothing and fails the request, rather than silently accepting a value that may be spoofed. Make sure your proxy replaces the identity header it injects instead of appending to it.
extra_headers describes the request being served. Operations that run later — a queued retain, a scheduled consolidation, a mental-model refresh — are executed by a background worker with no request behind them, so their RequestContext carries no headers. Authorize on the header at request time; do not rely on it inside work that continues after the response.
Example: Custom HttpExtension
from fastapi import APIRouter
from hindsight_api.extensions import HttpExtension
class MyHttpExtension(HttpExtension):
def get_router(self, memory: MemoryEngine) -> APIRouter:
router = APIRouter()
@router.get("/hello")
async def hello():
return {"message": "Hello from extension!"}
@router.post("/custom/{bank_id}/action")
async def custom_action(bank_id: str):
# Access memory engine for database operations
pool = await memory._get_pool()
# ... custom logic
return {"status": "ok"}
return router
def get_root_router(self, memory: MemoryEngine) -> APIRouter | None:
"""Optional: mount routes at the application root (not under /ext/)."""
router = APIRouter()
@router.get("/.well-known/my-metadata")
async def metadata():
return {"version": "1.0"}
return router
Routes from get_router are available at /ext/hello, /ext/custom/{bank_id}/action, etc.
Routes from get_root_router are mounted at the app root (e.g., /.well-known/my-metadata).
Example: Custom OperationValidatorExtension
from hindsight_api.extensions import (
OperationValidatorExtension,
ValidationResult,
PrecheckContext,
RetainContext,
RecallContext,
ReflectContext,
RetainResult,
)
class MyValidator(OperationValidatorExtension):
# Pre-body validation (optional)
async def precheck(self, ctx: PrecheckContext) -> ValidationResult:
if ctx.content_length is not None and ctx.content_length > 10_000_000:
return ValidationResult.reject("Payload is too large")
return ValidationResult.accept()
# Pre-operation validation (required)
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
# Implement your validation logic
return ValidationResult.accept()
# Or reject: return ValidationResult.reject("Reason")
async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
return ValidationResult.accept()
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
return ValidationResult.accept()
# Post-operation hooks (optional)
async def on_retain_complete(self, result: RetainResult) -> None:
# Log usage, update metrics, send notifications, etc.
pass
precheck runs before the request body is read or deserialized. Its
PrecheckContext.content_length is the parsed Content-Length header as an
integer, or None when the header is missing or cannot be parsed (for example,
chunked transfer encoding). Use it for cheap size-aware quota or cost guards;
the full validate_* hooks still run after parsing and should enforce precise
per-operation limits.
Deferring an operation
In addition to accept and reject, a validate_* hook can ask the
worker to requeue the operation for a future time by raising
DeferOperation. Use this for backpressure (rate-limited upstream,
quota window not yet open, dependency warming up) — unlike a retry, it
does not increment retry_count or write error_message. The worker
sets next_retry_at to your exec_date and the task is invisible to
claim queries until that time.
from datetime import datetime, timedelta, timezone
from hindsight_api.extensions import (
DeferOperation,
OperationValidatorExtension,
RetainContext,
ValidationResult,
)
class QuotaAwareValidator(OperationValidatorExtension):
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
if not await self._quota_available(ctx.bank_id):
raise DeferOperation(
exec_date=datetime.now(timezone.utc) + timedelta(minutes=5),
reason="bank quota window exhausted",
)
return ValidationResult.accept()
DeferOperation is worker-only: do not raise it from
validate_recall or validate_reflect in synchronous HTTP request
paths — there is no queue to defer to and it will surface as a 500.
Example: Custom MCPExtension
from mcp.server.fastmcp import FastMCP
from hindsight_api.extensions import MCPExtension
from hindsight_api.engine import MemoryEngine
class MyMCPExtension(MCPExtension):
async def register_tools(self, mcp: FastMCP, memory: MemoryEngine) -> None:
@mcp.tool()
async def custom_search(query: str) -> str:
"""Custom MCP tool for specialized search."""
# Access memory engine for operations
pool = await memory._get_pool()
# ... custom logic
return f"Results for: {query}"
Deploying Custom Extensions
With Docker
Mount your extension package as a volume and set the environment variable:
# docker-compose.yml
services:
hindsight-api:
image: vectorize/hindsight-api:latest
volumes:
- ./my_extensions:/app/my_extensions
environment:
- HINDSIGHT_API_TENANT_EXTENSION=my_extensions.auth:JwtTenantExtension
- HINDSIGHT_API_TENANT_JWT_SECRET=${JWT_SECRET}
- PYTHONPATH=/app
Or build a custom image with your extensions:
FROM vectorize/hindsight-api:latest
COPY my_extensions /app/my_extensions
ENV PYTHONPATH=/app
Bare Metal
Install your extension package in the same Python environment as Hindsight:
# Install Hindsight
pip install hindsight-api
# Install your extension package
pip install ./my-extensions
# or
pip install my-extensions-package
# Configure
export HINDSIGHT_API_TENANT_EXTENSION=my_extensions.auth:JwtTenantExtension
export HINDSIGHT_API_TENANT_JWT_SECRET=your-secret
# Run
hindsight-api
Contributing Extensions
Custom extensions that solve common use cases are welcome contributions to the Hindsight project. If you've built an extension for:
- Authentication providers (OAuth, SAML, API gateways)
- Rate limiting or quota management
- Audit logging integrations
- Metrics exporters (Datadog, New Relic, etc.)
- Custom HTTP endpoints for specific platforms
Consider contributing it to the hindsight_api.extensions.builtin package. Open an issue or pull request on GitHub to discuss your extension.