Skip to content

Commit 15826d5

Browse files
committed
updates
1 parent 90d11bd commit 15826d5

8 files changed

Lines changed: 411 additions & 220 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
-- ================================================================
2+
-- Migration: Add Default Apps for New Users
3+
-- ================================================================
4+
-- Adds is_default column to add_ons table and seeds Claude Agents
5+
-- as a new app. Marks Unicorn Chat (Open-WebUI) and Claude Agents
6+
-- as default apps for all newly registered users.
7+
--
8+
-- Date: 2026-02-17
9+
-- ================================================================
10+
11+
-- Step 1: Add is_default column to add_ons table
12+
ALTER TABLE add_ons ADD COLUMN IF NOT EXISTS is_default BOOLEAN DEFAULT FALSE;
13+
COMMENT ON COLUMN add_ons.is_default IS 'When TRUE, this app is automatically granted to all newly registered users regardless of their subscription tier.';
14+
15+
-- Step 2: Mark Unicorn Chat (Open-WebUI) as a default app
16+
UPDATE add_ons
17+
SET is_default = TRUE
18+
WHERE slug = 'open-webui';
19+
20+
-- Step 3: Insert Claude Agents into add_ons catalog (if not exists)
21+
INSERT INTO add_ons (
22+
name,
23+
slug,
24+
description,
25+
icon_url,
26+
launch_url,
27+
category,
28+
feature_key,
29+
base_price,
30+
billing_type,
31+
is_active,
32+
is_default,
33+
is_featured,
34+
sort_order,
35+
features
36+
)
37+
SELECT
38+
'Claude Agents',
39+
'claude-agents',
40+
'AI agent workflows powered by Claude — build, orchestrate, and execute multi-step agent pipelines',
41+
'/logos/claude-agents.png',
42+
'/admin/claude-agents',
43+
'AI Agents',
44+
'claude_agents_access',
45+
0.00,
46+
'included',
47+
TRUE,
48+
TRUE,
49+
TRUE,
50+
11,
51+
'{"highlights": ["Multi-step agent flows", "API key management", "Execution history", "Streaming output", "Claude SDK integration"], "use_cases": ["Automated workflows", "Code generation pipelines", "Research agents", "Data processing"]}'::jsonb
52+
WHERE NOT EXISTS (
53+
SELECT 1 FROM add_ons WHERE slug = 'claude-agents'
54+
);
55+
56+
-- If Claude Agents already exists, just make sure is_default is TRUE
57+
UPDATE add_ons
58+
SET is_default = TRUE
59+
WHERE slug = 'claude-agents';
60+
61+
-- Step 4: Add claude_agents_access to tier_feature_definitions (if not exists)
62+
INSERT INTO tier_feature_definitions (feature_key, feature_name, description, value_type, default_value, category, is_system)
63+
SELECT 'claude_agents_access', 'Claude Agents', 'Access to Claude Agent workflow builder', 'boolean', 'true', 'services', TRUE
64+
WHERE NOT EXISTS (
65+
SELECT 1 FROM tier_feature_definitions WHERE feature_key = 'claude_agents_access'
66+
);
67+
68+
-- Step 5: Enable claude_agents_access for ALL existing tiers
69+
-- (so every tier gets Claude Agents by default)
70+
INSERT INTO tier_features (tier_id, feature_key, feature_value, enabled)
71+
SELECT st.id, 'claude_agents_access', 'true', TRUE
72+
FROM subscription_tiers st
73+
WHERE NOT EXISTS (
74+
SELECT 1 FROM tier_features tf
75+
WHERE tf.tier_id = st.id AND tf.feature_key = 'claude_agents_access'
76+
);
77+
78+
-- Step 6: Ensure chat_access is enabled for ALL tiers too
79+
-- (Unicorn Chat should be available to everyone)
80+
INSERT INTO tier_features (tier_id, feature_key, feature_value, enabled)
81+
SELECT st.id, 'chat_access', 'true', TRUE
82+
FROM subscription_tiers st
83+
WHERE NOT EXISTS (
84+
SELECT 1 FROM tier_features tf
85+
WHERE tf.tier_id = st.id AND tf.feature_key = 'chat_access'
86+
);
87+
88+
-- Step 7: Auto-provision default apps for ALL existing users
89+
-- Creates user_add_ons records so existing users also get the default apps
90+
INSERT INTO user_add_ons (user_id, add_on_id, status, purchased_at)
91+
SELECT DISTINCT u.keycloak_id, ao.id, 'active', NOW()
92+
FROM users u
93+
CROSS JOIN add_ons ao
94+
WHERE ao.is_default = TRUE
95+
AND ao.is_active = TRUE
96+
AND u.keycloak_id IS NOT NULL
97+
AND NOT EXISTS (
98+
SELECT 1 FROM user_add_ons ua
99+
WHERE ua.user_id = u.keycloak_id AND ua.add_on_id = ao.id
100+
);
101+
102+
-- ================================================================
103+
-- Verification
104+
-- ================================================================
105+
DO $$
106+
DECLARE
107+
default_count INTEGER;
108+
BEGIN
109+
SELECT COUNT(*) INTO default_count FROM add_ons WHERE is_default = TRUE;
110+
RAISE NOTICE 'Default apps configured: % (expected: 2 — Unicorn Chat, Claude Agents)', default_count;
111+
END $$;

backend/my_apps_api.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ async def get_my_apps(user_tier: str = Depends(get_current_user_tier)):
169169
Get apps the current user is authorized to access based on their subscription tier.
170170
171171
Returns apps where:
172+
- App is marked as default (is_default=TRUE) — always included for all users
172173
- User's tier includes the app's feature_key
173174
- User has purchased the app separately (future: check user_add_ons table)
174175
"""
@@ -183,10 +184,11 @@ async def get_my_apps(user_tier: str = Depends(get_current_user_tier)):
183184
# Get enabled features for user's tier
184185
enabled_features = await get_user_tier_features(user_tier, conn)
185186

186-
# Get all active apps
187+
# Get all active apps (including is_default column)
187188
apps_query = """
188189
SELECT id, name, slug, description, icon_url, launch_url,
189-
category, feature_key, base_price, features
190+
category, feature_key, base_price, features,
191+
COALESCE(is_default, FALSE) as is_default
190192
FROM add_ons
191193
WHERE is_active = TRUE
192194
ORDER BY sort_order, name
@@ -197,21 +199,42 @@ async def get_my_apps(user_tier: str = Depends(get_current_user_tier)):
197199
# add_ons table doesn't exist yet - return empty list
198200
logger.warning("add_ons table does not exist - returning empty app list")
199201
return []
202+
except asyncpg.UndefinedColumnError:
203+
# is_default column doesn't exist yet - fall back to query without it
204+
logger.warning("is_default column not found, falling back to legacy query")
205+
apps_query = """
206+
SELECT id, name, slug, description, icon_url, launch_url,
207+
category, feature_key, base_price, features,
208+
FALSE as is_default
209+
FROM add_ons
210+
WHERE is_active = TRUE
211+
ORDER BY sort_order, name
212+
"""
213+
app_rows = await conn.fetch(apps_query)
200214

201-
# Filter apps based on user's tier
215+
# Filter apps based on user's tier and default status
202216
authorized_apps = []
217+
seen_slugs = set()
203218
for app in app_rows:
204219
app_dict = dict(app)
205220
feature_key = app_dict.get('feature_key')
221+
is_default = app_dict.get('is_default', False)
206222

207223
# Skip apps without launch URLs
208224
if not app_dict.get('launch_url'):
209225
continue
210226

227+
# Skip duplicates
228+
if app_dict['slug'] in seen_slugs:
229+
continue
230+
211231
# Check if user has access
212232
access_type = None
213233

214-
if feature_key and feature_key in enabled_features:
234+
if is_default:
235+
# Default apps are always included for all authenticated users
236+
access_type = 'default'
237+
elif feature_key and feature_key in enabled_features:
215238
# User's tier includes this app
216239
access_type = 'tier_included'
217240
elif app_dict['base_price'] == 0 and not feature_key:
@@ -222,6 +245,7 @@ async def get_my_apps(user_tier: str = Depends(get_current_user_tier)):
222245
# (Later: check if user purchased it separately)
223246
continue
224247

248+
seen_slugs.add(app_dict['slug'])
225249
authorized_apps.append({
226250
'id': str(app_dict['id']), # Convert UUID to string
227251
'name': app_dict['name'],
@@ -274,7 +298,8 @@ async def get_marketplace_apps(user_tier: str = Depends(get_current_user_tier)):
274298
# Get all active apps
275299
apps_query = """
276300
SELECT id, name, slug, description, icon_url, launch_url,
277-
category, feature_key, base_price, billing_type
301+
category, feature_key, base_price, billing_type,
302+
COALESCE(is_default, FALSE) as is_default
278303
FROM add_ons
279304
WHERE is_active = TRUE
280305
ORDER BY base_price DESC, name
@@ -286,11 +311,16 @@ async def get_marketplace_apps(user_tier: str = Depends(get_current_user_tier)):
286311
for app in app_rows:
287312
app_dict = dict(app)
288313
feature_key = app_dict.get('feature_key')
314+
is_default = app_dict.get('is_default', False)
289315

290316
# Skip apps without launch URLs
291317
if not app_dict.get('launch_url'):
292318
continue
293319

320+
# Skip default apps (users already have them)
321+
if is_default:
322+
continue
323+
294324
# Skip apps user already has access to
295325
if feature_key and feature_key in enabled_features:
296326
continue

backend/openwebui_api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -593,8 +593,8 @@ async def seed_app_records(request: Request):
593593
await conn.execute("""
594594
INSERT INTO add_ons (name, slug, description, icon_url, launch_url,
595595
category, feature_key, base_price, billing_type,
596-
is_active, sort_order, features)
597-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb)
596+
is_active, is_default, sort_order, features)
597+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb)
598598
""",
599599
"Unicorn Chat",
600600
"open-webui",
@@ -606,6 +606,7 @@ async def seed_app_records(request: Request):
606606
0.0,
607607
"included",
608608
True,
609+
True, # is_default — always granted to new users
609610
10,
610611
features_json,
611612
)

backend/server.py

Lines changed: 37 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2331,63 +2331,9 @@ async def require_user(current_user: dict = Depends(get_current_user)):
23312331
)
23322332
return current_user
23332333

2334-
# My Apps API
2335-
2336-
@app.get("/api/v1/my-apps/authorized")
2337-
async def get_my_apps_authorized(current_user: dict = Depends(get_current_user)):
2338-
"""Get apps the user has access to based on their subscription and add-ons"""
2339-
try:
2340-
# Get user ID - fallback to 'default' if not specified
2341-
user_id = current_user.get("preferred_username") or current_user.get("username") or current_user.get("sub") or "default"
2342-
2343-
logger.info(f"Fetching apps for user: {user_id}")
2344-
2345-
# Connect to Lago database to fetch user add-ons
2346-
lago_conn = await asyncpg.connect(os.getenv('LAGO_DATABASE_URL', 'postgresql://unicorn:s3cr3t@lago-db:5432/lago'))
2347-
2348-
try:
2349-
# Query user's active add-ons with app details
2350-
query = """
2351-
SELECT
2352-
a.id,
2353-
a.name,
2354-
a.slug,
2355-
a.description,
2356-
a.icon_url,
2357-
a.launch_url,
2358-
ua.status
2359-
FROM user_add_ons ua
2360-
JOIN add_ons a ON ua.add_on_id = a.id
2361-
WHERE ua.user_id = $1
2362-
AND ua.status = 'active'
2363-
AND a.is_active = true
2364-
ORDER BY a.name
2365-
"""
2366-
2367-
rows = await lago_conn.fetch(query, user_id)
2368-
2369-
logger.info(f"Found {len(rows)} active apps for user {user_id}")
2370-
2371-
apps = []
2372-
for row in rows:
2373-
apps.append({
2374-
"id": str(row["id"]),
2375-
"name": row["name"],
2376-
"slug": row["slug"],
2377-
"description": row["description"] or "",
2378-
"icon_url": row["icon_url"],
2379-
"launch_url": row["launch_url"] or f"/{row['slug']}",
2380-
"status": row["status"]
2381-
})
2382-
2383-
return apps
2384-
2385-
finally:
2386-
await lago_conn.close()
2387-
2388-
except Exception as e:
2389-
logger.error(f"Error fetching user apps: {str(e)}")
2390-
raise HTTPException(status_code=500, detail=f"Failed to fetch apps: {str(e)}")
2334+
# My Apps API — deferred to my_apps_api.py router (app.include_router(my_apps_router))
2335+
# The router at /api/v1/my-apps handles /authorized and /marketplace
2336+
# using tier-based feature matching and is_default flags.
23912337

23922338
# API Routes
23932339

@@ -5319,6 +5265,38 @@ async def register_user(request: Request):
53195265
logger.error(f"Failed to create credit account during registration for {email}: {credit_error}")
53205266
logger.warning(f"User will have credits auto-provisioned on first API call")
53215267

5268+
# Step 5.6: Auto-provision default apps (Unicorn Chat, Claude Agents, etc.)
5269+
default_apps_provisioned = False
5270+
try:
5271+
user_identifier = keycloak_user_id or local_user["id"]
5272+
db_conn = await asyncpg.connect(
5273+
host=os.getenv("POSTGRES_HOST", "unicorn-postgresql"),
5274+
port=int(os.getenv("POSTGRES_PORT", "5432")),
5275+
user=os.getenv("POSTGRES_USER", "unicorn"),
5276+
password=os.getenv("POSTGRES_PASSWORD", "unicorn"),
5277+
database=os.getenv("POSTGRES_DB", "unicorn_db")
5278+
)
5279+
try:
5280+
# Find all default apps and provision them for the new user
5281+
default_apps = await db_conn.fetch(
5282+
"SELECT id, name FROM add_ons WHERE is_default = TRUE AND is_active = TRUE"
5283+
)
5284+
for app in default_apps:
5285+
await db_conn.execute("""
5286+
INSERT INTO user_add_ons (user_id, add_on_id, status, purchased_at)
5287+
VALUES ($1, $2, 'active', NOW())
5288+
ON CONFLICT (user_id, add_on_id) DO NOTHING
5289+
""", user_identifier, app['id'])
5290+
logger.info(f"Provisioned default app '{app['name']}' for user {email}")
5291+
default_apps_provisioned = True
5292+
logger.info(f"Provisioned {len(default_apps)} default app(s) for {email}")
5293+
finally:
5294+
await db_conn.close()
5295+
except Exception as apps_error:
5296+
# Don't fail registration if default app provisioning fails
5297+
logger.error(f"Failed to provision default apps for {email}: {apps_error}")
5298+
logger.warning(f"Default apps will be available via tier features fallback")
5299+
53225300
# Step 6: Audit log successful registration
53235301
if AUDIT_ENABLED:
53245302
await log_auth_success(
@@ -5332,7 +5310,8 @@ async def register_user(request: Request):
53325310
"org_id": org_id,
53335311
"org_name": org_name,
53345312
"lago_customer_created": lago_customer_created,
5335-
"credit_account_created": credit_account_created
5313+
"credit_account_created": credit_account_created,
5314+
"default_apps_provisioned": default_apps_provisioned
53365315
}
53375316
)
53385317

0 commit comments

Comments
 (0)