diff --git a/.env.example b/.env.example index cc3bce96..cdb9ca1f 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,7 @@ HINDSIGHT_API_LOG_LEVEL=info # Database (Optional - uses embedded pg0 by default) # HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/db +# HINDSIGHT_API_DATABASE_SCHEMA=public # PostgreSQL schema name (default: public) # Embeddings Configuration (Optional - uses local by default) # Provider: "local" (default) or "tei" (HuggingFace Text Embeddings Inference) diff --git a/hindsight-api/hindsight_api/config.py b/hindsight-api/hindsight_api/config.py index cbbc80e2..5461aef9 100644 --- a/hindsight-api/hindsight_api/config.py +++ b/hindsight-api/hindsight_api/config.py @@ -20,6 +20,7 @@ logger = logging.getLogger(__name__) # Environment variable names ENV_DATABASE_URL = "HINDSIGHT_API_DATABASE_URL" +ENV_DATABASE_SCHEMA = "HINDSIGHT_API_DATABASE_SCHEMA" ENV_LLM_PROVIDER = "HINDSIGHT_API_LLM_PROVIDER" ENV_LLM_API_KEY = "HINDSIGHT_API_LLM_API_KEY" ENV_LLM_MODEL = "HINDSIGHT_API_LLM_MODEL" @@ -125,6 +126,7 @@ ENV_REFLECT_MAX_ITERATIONS = "HINDSIGHT_API_REFLECT_MAX_ITERATIONS" # Default values DEFAULT_DATABASE_URL = "pg0" +DEFAULT_DATABASE_SCHEMA = "public" DEFAULT_LLM_PROVIDER = "openai" DEFAULT_LLM_MODEL = "gpt-5-mini" DEFAULT_LLM_MAX_CONCURRENT = 32 @@ -270,6 +272,7 @@ class HindsightConfig: # Database database_url: str + database_schema: str # LLM (default, used as fallback for per-operation config) llm_provider: str @@ -367,6 +370,7 @@ class HindsightConfig: return cls( # Database database_url=os.getenv(ENV_DATABASE_URL, DEFAULT_DATABASE_URL), + database_schema=os.getenv(ENV_DATABASE_SCHEMA, DEFAULT_DATABASE_SCHEMA), # LLM llm_provider=os.getenv(ENV_LLM_PROVIDER, DEFAULT_LLM_PROVIDER), llm_api_key=os.getenv(ENV_LLM_API_KEY), @@ -515,7 +519,7 @@ class HindsightConfig: def log_config(self) -> None: """Log the current configuration (without sensitive values).""" - logger.info(f"Database: {self.database_url}") + logger.info(f"Database: {self.database_url} (schema: {self.database_schema})") logger.info(f"LLM: provider={self.llm_provider}, model={self.llm_model}") if self.retain_llm_provider or self.retain_llm_model: retain_provider = self.retain_llm_provider or self.llm_provider diff --git a/hindsight-api/hindsight_api/engine/memory_engine.py b/hindsight-api/hindsight_api/engine/memory_engine.py index bea556b8..f8e1ebc8 100644 --- a/hindsight-api/hindsight_api/engine/memory_engine.py +++ b/hindsight-api/hindsight_api/engine/memory_engine.py @@ -23,12 +23,17 @@ from ..metrics import get_metrics_collector from .db_budget import budgeted_operation # Context variable for current schema (async-safe, per-task isolation) -_current_schema: contextvars.ContextVar[str] = contextvars.ContextVar("current_schema", default="public") +# Note: default is None, actual default comes from config via get_current_schema() +_current_schema: contextvars.ContextVar[str | None] = contextvars.ContextVar("current_schema", default=None) def get_current_schema() -> str: - """Get the current schema from context (default: 'public').""" - return _current_schema.get() + """Get the current schema from context (falls back to config default).""" + schema = _current_schema.get() + if schema is None: + # Fall back to configured default schema + return get_config().database_schema + return schema def fq_table(table_name: str) -> str: @@ -881,11 +886,12 @@ class MemoryEngine(MemoryEngineInterface): if not self.db_url: raise ValueError("Database URL is required for migrations") logger.info("Running database migrations...") - run_migrations(self.db_url) + # Use configured database schema for migrations (defaults to "public") + run_migrations(self.db_url, schema=get_config().database_schema) # Ensure embedding column dimension matches the model's dimension # This is done after migrations and after embeddings.initialize() - ensure_embedding_dimension(self.db_url, self.embeddings.dimension) + ensure_embedding_dimension(self.db_url, self.embeddings.dimension, schema=get_config().database_schema) logger.info(f"Connecting to PostgreSQL at {self.db_url}") diff --git a/hindsight-api/hindsight_api/extensions/builtin/tenant.py b/hindsight-api/hindsight_api/extensions/builtin/tenant.py index d56266c5..163bf572 100644 --- a/hindsight-api/hindsight_api/extensions/builtin/tenant.py +++ b/hindsight-api/hindsight_api/extensions/builtin/tenant.py @@ -1,5 +1,6 @@ """Built-in tenant extension implementations.""" +from hindsight_api.config import get_config from hindsight_api.extensions.tenant import AuthenticationError, Tenant, TenantContext, TenantExtension from hindsight_api.models import RequestContext @@ -10,11 +11,13 @@ class ApiKeyTenantExtension(TenantExtension): This is a simple implementation that: 1. Validates the API key matches HINDSIGHT_API_TENANT_API_KEY - 2. Returns 'public' as the schema for all authenticated requests + 2. Returns the configured schema (HINDSIGHT_API_DATABASE_SCHEMA, default 'public') + for all authenticated requests Configuration: HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension HINDSIGHT_API_TENANT_API_KEY=your-secret-key + HINDSIGHT_API_DATABASE_SCHEMA=your-schema (optional, defaults to 'public') For multi-tenant setups with separate schemas per tenant, implement a custom TenantExtension that looks up the schema based on the API key or token claims. @@ -27,11 +30,11 @@ class ApiKeyTenantExtension(TenantExtension): raise ValueError("HINDSIGHT_API_TENANT_API_KEY is required when using ApiKeyTenantExtension") async def authenticate(self, context: RequestContext) -> TenantContext: - """Validate API key and return public schema context.""" + """Validate API key and return configured schema context.""" if context.api_key != self.expected_api_key: raise AuthenticationError("Invalid API key") - return TenantContext(schema_name="public") + return TenantContext(schema_name=get_config().database_schema) async def list_tenants(self) -> list[Tenant]: - """Return public schema for single-tenant setup.""" - return [Tenant(schema="public")] + """Return configured schema for single-tenant setup.""" + return [Tenant(schema=get_config().database_schema)] diff --git a/hindsight-api/hindsight_api/main.py b/hindsight-api/hindsight_api/main.py index 6e016609..695bd172 100644 --- a/hindsight-api/hindsight_api/main.py +++ b/hindsight-api/hindsight_api/main.py @@ -170,6 +170,7 @@ def main(): if args.log_level != config.log_level: config = HindsightConfig( database_url=config.database_url, + database_schema=config.database_schema, llm_provider=config.llm_provider, llm_api_key=config.llm_api_key, llm_model=config.llm_model, diff --git a/hindsight-docs/docs/developer/configuration.md b/hindsight-docs/docs/developer/configuration.md index f534397e..52af0268 100644 --- a/hindsight-docs/docs/developer/configuration.md +++ b/hindsight-docs/docs/developer/configuration.md @@ -20,10 +20,24 @@ The API service handles all memory operations (retain, recall, reflect). | Variable | Description | Default | |----------|-------------|---------| | `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | `pg0` (embedded) | +| `HINDSIGHT_API_DATABASE_SCHEMA` | PostgreSQL schema name for tables | `public` | | `HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP` | Run database migrations on API startup | `true` | If not provided, the server uses embedded `pg0` — convenient for development but not recommended for production. +The `DATABASE_SCHEMA` setting allows you to use a custom PostgreSQL schema instead of the default `public` schema. This is useful for: +- Multi-database setups where you want Hindsight tables in a dedicated schema +- Hosting platforms (e.g., Supabase) where `public` schema is reserved or shared +- Organizational preferences for schema naming conventions + +```bash +# Example: Using a custom schema +export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/dbname +export HINDSIGHT_API_DATABASE_SCHEMA=hindsight +``` + +Migrations will automatically create the schema if it doesn't exist and create all tables in the configured schema. + ### Database Connection Pool | Variable | Description | Default | @@ -439,6 +453,7 @@ export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com:8888 ```bash # API Service HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight +# HINDSIGHT_API_DATABASE_SCHEMA=public # optional, defaults to 'public' HINDSIGHT_API_LLM_PROVIDER=groq HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx diff --git a/package-lock.json b/package-lock.json index a0257db2..f2992f4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ }, "hindsight-clients/typescript": { "name": "@vectorize-io/hindsight-client", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "devDependencies": { "@hey-api/openapi-ts": "0.88.0", @@ -131,7 +131,7 @@ }, "hindsight-control-plane": { "name": "@vectorize-io/hindsight-control-plane", - "version": "0.3.0", + "version": "0.4.0", "license": "ISC", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.15",