Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 9 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,12 @@ redis:
warm_after_invalidation: true # Pre-populate cache after invalidation


# =============================================================================
# USAGE LIMITS CONFIGURATION
# =============================================================================
# Per-user, tier-based usage limits. Only enforced when auth is enabled.
# Complete no-op in local dev (when SUPABASE_URL is unset).
usage_limits:
enabled: true
plan_cache_ttl: 300 # seconds — PlanService in-memory cache TTL
burst_counter_ttl: 300 # seconds (5 minutes, short-lived burst window)
1 change: 0 additions & 1 deletion frontend
Submodule frontend deleted from e36726
28 changes: 25 additions & 3 deletions scripts/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,31 @@ async def run_migrations():
sql = migration_file.read_text()

try:
# Split and execute statements separately
# (psycopg3 doesn't support multiple statements in one execute)
statements = [s.strip() for s in sql.split(';') if s.strip() and not s.strip().startswith('--')]
# Split SQL into individual statements, respecting
# parenthesized blocks (e.g. CREATE TABLE (...;)).
# Only split on ';' at top-level (depth == 0).
statements = []
buf = []
depth = 0
for line in sql.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith('--'):
continue
depth += stripped.count('(') - stripped.count(')')
if stripped.endswith(';') and depth <= 0:
buf.append(stripped[:-1]) # drop trailing ;
stmt = ' '.join(buf).strip()
if stmt:
statements.append(stmt)
buf = []
depth = 0
else:
buf.append(stripped)
# Catch trailing statement without semicolon
trailing = ' '.join(buf).strip()
if trailing:
statements.append(trailing)

for stmt in statements:
await cur.execute(stmt)
await cur.execute(
Expand Down
56 changes: 56 additions & 0 deletions scripts/migrations/002_add_user_plan.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-- Migration 002: Plans table + user plan_id FK + redemption codes system
-- Purpose: Move tier/plan definitions from config.yaml to DB for dynamic management

-- 1. Plans table (source of truth for tiers)
CREATE TABLE IF NOT EXISTS plans (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
display_name VARCHAR(100) NOT NULL,
rank INT NOT NULL DEFAULT 0,
daily_credits NUMERIC(10,2) NOT NULL DEFAULT 500.0,
max_active_workspaces INT NOT NULL DEFAULT 3,
max_concurrent_requests INT NOT NULL DEFAULT 5,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_default ON plans (is_default) WHERE is_default = TRUE;
CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_rank ON plans (rank);

-- 2. Seed initial plans
INSERT INTO plans (name, display_name, rank, daily_credits, max_active_workspaces, max_concurrent_requests, is_default) VALUES
('free', 'Free', 0, 1000.0, 3, 5, TRUE),
('pro', 'Pro', 1, 5000.0, 10, 20, FALSE),
('enterprise', 'Enterprise', 2, -1, -1, -1, FALSE)
ON CONFLICT (name) DO NOTHING;

-- 3. Add plan_id FK to users
ALTER TABLE users ADD COLUMN IF NOT EXISTS plan_id INT;
UPDATE users SET plan_id = (SELECT id FROM plans WHERE is_default = TRUE LIMIT 1) WHERE plan_id IS NULL;
ALTER TABLE users ALTER COLUMN plan_id SET NOT NULL;
ALTER TABLE users ALTER COLUMN plan_id SET DEFAULT 1;
ALTER TABLE users ADD CONSTRAINT fk_users_plan FOREIGN KEY (plan_id) REFERENCES plans(id);
CREATE INDEX IF NOT EXISTS idx_users_plan_id ON users (plan_id);

-- 4. Redemption codes
CREATE TABLE IF NOT EXISTS redemption_codes (
code VARCHAR(50) PRIMARY KEY,
plan_id INT NOT NULL REFERENCES plans(id),
max_redemptions INT NOT NULL DEFAULT 1,
current_redemptions INT NOT NULL DEFAULT 0,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
is_active BOOLEAN NOT NULL DEFAULT TRUE
);

-- 5. Redemption history (plan names as strings for audit trail)
CREATE TABLE IF NOT EXISTS redemption_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(50) NOT NULL REFERENCES redemption_codes(code),
user_id VARCHAR(255) NOT NULL REFERENCES users(user_id) ON UPDATE CASCADE,
previous_plan VARCHAR(50) NOT NULL,
new_plan VARCHAR(50) NOT NULL,
redeemed_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(code, user_id)
);
CREATE INDEX IF NOT EXISTS idx_redemption_history_user ON redemption_history(user_id);
19 changes: 19 additions & 0 deletions scripts/migrations/003_user_api_keys.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Migration 003: User API keys for BYOK (Bring Your Own Key) support
-- Purpose: Allow users to provide their own LLM API keys to bypass credit limits
-- Requires: pgcrypto extension for symmetric encryption of API keys at rest

CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- 1. Per-provider API keys (one row per user+provider)
CREATE TABLE IF NOT EXISTS user_api_keys (
user_id VARCHAR(255)
REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE,
provider VARCHAR(50) NOT NULL,
api_key BYTEA NOT NULL, -- encrypted via pgp_sym_encrypt
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (user_id, provider)
);

-- 2. BYOK toggle on users table (global per-user switch)
ALTER TABLE users ADD COLUMN IF NOT EXISTS byok_enabled BOOLEAN NOT NULL DEFAULT FALSE;
9 changes: 9 additions & 0 deletions src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,15 @@ def get_search_api() -> str:



# =============================================================================
# Usage Limits Configuration
# =============================================================================

def get_usage_limits_config() -> Dict[str, Any]:
"""Get the full usage_limits section from config.yaml."""
return get_config('usage_limits', {})


# =============================================================================
# Redis Configuration
# =============================================================================
Expand Down
Loading