This guide provides a complete solution for automatically adding jo@example.com as an organization admin to all Langfuse organizations using a Kubernetes CronJob that runs every hour.
- Overview
- Prerequisites
- Database Schema Discovery
- Complete Solution
- YAML Configuration
- SQL Script
- Deployment Instructions
- Testing and Verification
- Troubleshooting
- Customization Options
This solution creates a Kubernetes CronJob that:
- Runs every hour at the top of the hour (
0 * * * *) - Automatically creates the user
jo@example.comif they don't exist - Assigns
jo@example.comas OWNER of all organizations - Is safe to run multiple times (idempotent)
- Provides comprehensive logging and error handling
- Langfuse deployment with PostgreSQL
- kubectl access to the cluster
- Namespace:
langfuse-test(or your Langfuse namespace)
- PostgreSQL connection details
- Appropriate permissions to create users and manage organization memberships
Through database exploration, we discovered the following Langfuse schema:
-- Users table
users (id, email, name, created_at, updated_at)
-- Organizations table
organizations (id, name, created_at, updated_at)
-- Organization Memberships table
organization_memberships (id, org_id, user_id, role, created_at, updated_at)OWNER: Highest level (equivalent to admin)- Other roles may exist depending on Langfuse configuration
| User Email | User Name | Organization | Role |
|---|---|---|---|
| barakplasma@gmail.com | Michael | Test 1 | OWNER |
| barakplasma@gmail.com | Michael | Test 2 | OWNER |
| User Email | User Name | Organization | Role |
|---|---|---|---|
| barakplasma@gmail.com | Michael | Test 1 | OWNER |
| jo@example.com | Jo Admin | Test 1 | OWNER |
| barakplasma@gmail.com | Michael | Test 2 | OWNER |
| jo@example.com | Jo Admin | Test 2 | OWNER |
Create a file langfuse-org-admin-cronjob.yaml:
# Complete Langfuse Organization Admin CronJob Configuration
# This file adds a CronJob to automatically add jo@example.com as an organization admin to all organizations
# Add this section to your existing values.yaml or use as a standalone override
extraManifests:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: langfuse-sql-scripts
namespace: langfuse-test # Replace with your namespace
labels:
app: langfuse-sql-runner
component: sql-scripts
data:
# SQL script to add jo@example.com as org admin to all organizations
add_org_admin.sql: |
DO $$
DECLARE
jo_user_id UUID;
org_record RECORD;
processed_count INTEGER := 0;
error_count INTEGER := 0;
BEGIN
RAISE NOTICE '=== Starting Organization Admin Assignment Job ===';
RAISE NOTICE 'Target: jo@example.com';
RAISE NOTICE 'Timestamp: %', NOW();
-- Step 1: Find or create jo@example.com user
SELECT id INTO jo_user_id FROM "users" WHERE email = 'jo@example.com';
IF jo_user_id IS NULL THEN
-- Create the user if they don't exist
INSERT INTO "users" (id, email, name, created_at, updated_at)
VALUES (
gen_random_uuid(),
'jo@example.com',
'Jo Admin',
NOW(),
NOW()
) RETURNING id INTO jo_user_id;
RAISE NOTICE '✓ Created new user jo@example.com with ID: %', jo_user_id;
ELSE
RAISE NOTICE '✓ Found existing user jo@example.com with ID: %', jo_user_id;
END IF;
-- Step 2: Add jo@example.com as admin to all organizations
FOR org_record IN
SELECT id, name FROM "organizations" ORDER BY name
LOOP
BEGIN
-- Check if user is already a member of this organization
IF NOT EXISTS (
SELECT 1 FROM "organization_memberships"
WHERE "org_id" = org_record.id
AND "user_id" = jo_user_id
) THEN
-- Add as organization member with admin role
INSERT INTO "organization_memberships" (
id,
"org_id",
"user_id",
role,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
org_record.id,
jo_user_id,
'OWNER',
NOW(),
NOW()
);
RAISE NOTICE '✓ Added jo@example.com as OWNER to organization: % (ID: %)',
org_record.name, org_record.id;
processed_count := processed_count + 1;
ELSE
RAISE NOTICE '! User jo@example.com already member of organization: %', org_record.name;
END IF;
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE '! User already has admin role in organization: % (ID: %)',
org_record.name, org_record.id;
WHEN OTHERS THEN
RAISE WARNING '! Error processing organization % (ID: %): %',
org_record.name, org_record.id, SQLERRM;
error_count := error_count + 1;
END;
END LOOP;
-- Step 3: Log completion summary
RAISE NOTICE '=== Job Summary ===';
RAISE NOTICE 'Organizations processed: %', processed_count;
RAISE NOTICE 'Errors encountered: %', error_count;
RAISE NOTICE 'Completed at: %', NOW();
RAISE NOTICE '=== End Organization Admin Assignment Job ===';
END $$;
-- Return summary statistics
SELECT
u.email,
COUNT(*) as total_org_memberships,
COUNT(CASE WHEN om.role = 'OWNER' THEN 1 END) as owner_assignments,
COUNT(CASE WHEN om.role = 'ADMIN' THEN 1 END) as admin_assignments,
MIN(om.created_at) as first_assignment,
MAX(om.updated_at) as last_update
FROM "users" u
LEFT JOIN "organization_memberships" om ON u.id = om."user_id"
WHERE u.email = 'jo@example.com'
GROUP BY u.email;
- |
apiVersion: batch/v1
kind: CronJob
metadata:
name: langfuse-org-admin-updater
namespace: langfuse-test # Replace with your namespace
labels:
app: langfuse-sql-runner
component: org-admin-updater
annotations:
description: "CronJob to add jo@example.com as organization admin to all organizations"
managed-by: "Helm"
version: "1.2.0"
spec:
# Run every hour at the top of the hour
schedule: "0 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
# Give the job 10 minutes to complete
startingDeadlineSeconds: 600
jobTemplate:
spec:
# Clean up the job after 24 hours
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
app: langfuse-sql-runner
component: org-admin-updater
spec:
restartPolicy: OnFailure
containers:
- name: postgresql-client
image: bitnamilegacy/postgresql:17.3.0-debian-12-r1
env:
# Database connection (uses same config as PostgreSQL subchart)
- name: PGHOST
value: "langfuse-test-postgresql"
- name: PGPORT
value: "5432"
- name: PGUSER
value: "langfuse"
- name: PGPASSWORD
valueFrom:
secretKeyRef:
key: "postgres-password"
name: "langfuse-test-postgresql"
- name: PGDATABASE
value: "postgres_langfuse"
- name: PGOPTIONS
value: "-c client_min_messages=notice"
- name: PGAPPNAME
value: "langfuse-org-admin-updater"
command:
- /bin/sh
- -c
- |
echo "═══════════════════════════════════════════════════════════"
echo " Langfuse Organization Admin Assignment Job"
echo "═══════════════════════════════════════════════════════════"
echo "Started at: $(date '+%Y-%m-%d %H:%M:%S UTC')"
echo "Target user: jo@example.com"
echo "Database: ${PGDATABASE}@${PGHOST}:${PGPORT}"
echo "Namespace: ${POD_NAMESPACE:-default}"
echo "Pod: ${HOSTNAME}"
echo "═══════════════════════════════════════════════════════════"
echo ""
# Set error handling (avoiding -o pipefail for compatibility)
set -eu
# Pre-flight checks
echo "🔍 Performing pre-flight checks..."
# Test database connection
echo " • Testing database connection..."
if psql -c "SELECT 1;" >/dev/null 2>&1; then
echo " ✓ Database connection successful"
else
echo " ✗ Database connection failed"
exit 1
fi
# Check if required tables exist
echo " • Checking required tables..."
for table in "users" "organizations" "organization_memberships"; do
if psql -c "SELECT 1 FROM information_schema.tables WHERE table_name = '$table';" 2>/dev/null | grep -q "1"; then
echo " ✓ Table '$table' exists"
else
echo " ✗ Table '$table' not found"
exit 1
fi
done
# Check if target user already exists
echo " • Checking for existing user jo@example.com..."
existing_user=$(psql -t -c "SELECT COUNT(*) FROM \"users\" WHERE email = 'jo@example.com';" 2>/dev/null | tr -d ' ' || echo "0")
if [ "$existing_user" -gt 0 ]; then
echo " ✓ User jo@example.com already exists"
else
echo " ℹ User jo@example.com will be created"
fi
# Count organizations
org_count=$(psql -t -c "SELECT COUNT(*) FROM \"organizations\";" 2>/dev/null | tr -d ' ' || echo "0")
echo " ℹ Found $org_count organizations"
echo ""
echo "🚀 Executing SQL script..."
echo "═══════════════════════════════════════════════════════════"
# Execute SQL script with detailed output
start_time=$(date +%s)
if psql -v ON_ERROR_STOP=1 -v VERBOSITY=verbose -f /sql-scripts/add_org_admin.sql; then
end_time=$(date +%s)
duration=$((end_time - start_time))
echo ""
echo "═══════════════════════════════════════════════════════════"
echo "✅ Job completed successfully!"
echo "Duration: ${duration} seconds"
echo "Completed at: $(date '+%Y-%m-%d %H:%M:%S UTC')"
# Display final summary
echo ""
echo "📊 Current Organization Memberships for jo@example.com:"
if psql -c "
SELECT
o.name as organization,
om.role,
om.created_at as added_on
FROM organization_memberships om
JOIN organizations o ON om.org_id = o.id
JOIN users u ON om.user_id = u.id
WHERE u.email = 'jo@example.com'
ORDER BY o.name;
" 2>/dev/null; then
echo "Membership details displayed successfully"
else
echo "Could not retrieve membership details"
fi
else
echo "❌ SQL execution failed!"
exit 1
fi
echo ""
echo "═══════════════════════════════════════════════════════════"
echo " End Organization Admin Assignment Job"
echo "═══════════════════════════════════════════════════════════"
volumeMounts:
- name: sql-scripts
mountPath: /sql-scripts
readOnly: true
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: sql-scripts
configMap:
name: langfuse-sql-scriptsapiVersion: v1
kind: ConfigMap
metadata:
name: langfuse-sql-scripts
namespace: langfuse-test
labels:
app: langfuse-sql-runner
component: sql-scripts
data:
add_org_admin.sql: |
DO $$
DECLARE
jo_user_id UUID;
org_record RECORD;
processed_count INTEGER := 0;
error_count INTEGER := 0;
BEGIN
-- Find or create jo@example.com user
SELECT id INTO jo_user_id FROM "users" WHERE email = 'jo@example.com';
IF jo_user_id IS NULL THEN
INSERT INTO "users" (id, email, name, created_at, updated_at)
VALUES (
gen_random_uuid(),
'jo@example.com',
'Jo Admin',
NOW(),
NOW()
) RETURNING id INTO jo_user_id;
RAISE NOTICE '✓ Created new user jo@example.com';
ELSE
RAISE NOTICE '✓ Found existing user jo@example.com';
END IF;
-- Add as admin to all organizations
FOR org_record IN
SELECT id, name FROM "organizations" ORDER BY name
LOOP
IF NOT EXISTS (
SELECT 1 FROM "organization_memberships"
WHERE "org_id" = org_record.id
AND "user_id" = jo_user_id
) THEN
INSERT INTO "organization_memberships" (
id, "org_id", "user_id", role, created_at, updated_at
) VALUES (
gen_random_uuid(),
org_record.id,
jo_user_id,
'OWNER',
NOW(),
NOW()
);
processed_count := processed_count + 1;
END IF;
END LOOP;
RAISE NOTICE 'Processed % organizations', processed_count;
END $$;apiVersion: batch/v1
kind: CronJob
metadata:
name: langfuse-org-admin-updater
namespace: langfuse-test
spec:
schedule: "0 * * * *" # Every hour
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: postgresql-client
image: bitnamilegacy/postgresql:17.3.0-debian-12-r1
env:
- name: PGHOST
value: "langfuse-test-postgresql"
- name: PGPORT
value: "5432"
- name: PGUSER
value: "langfuse"
- name: PGPASSWORD
valueFrom:
secretKeyRef:
key: "postgres-password"
name: "langfuse-test-postgresql"
- name: PGDATABASE
value: "postgres_langfuse"
command:
- /bin/sh
- -c
- |
set -eu
psql -v ON_ERROR_STOP=1 -f /sql-scripts/add_org_admin.sql
volumeMounts:
- name: sql-scripts
mountPath: /sql-scripts
volumes:
- name: sql-scripts
configMap:
name: langfuse-sql-scripts-- Complete SQL script for adding jo@example.com as organization admin
DO $$
DECLARE
jo_user_id UUID;
org_record RECORD;
processed_count INTEGER := 0;
error_count INTEGER := 0;
BEGIN
-- Step 1: Find or create jo@example.com user
SELECT id INTO jo_user_id FROM "users" WHERE email = 'jo@example.com';
IF jo_user_id IS NULL THEN
INSERT INTO "users" (id, email, name, created_at, updated_at)
VALUES (
gen_random_uuid(),
'jo@example.com',
'Jo Admin',
NOW(),
NOW()
) RETURNING id INTO jo_user_id;
END IF;
-- Step 2: Add to all organizations
FOR org_record IN
SELECT id, name FROM "organizations" ORDER BY name
LOOP
IF NOT EXISTS (
SELECT 1 FROM "organization_memberships"
WHERE "org_id" = org_record.id
AND "user_id" = jo_user_id
) THEN
INSERT INTO "organization_memberships" (
id, "org_id", "user_id", role, created_at, updated_at
) VALUES (
gen_random_uuid(),
org_record.id,
jo_user_id,
'OWNER',
NOW(),
NOW()
);
processed_count := processed_count + 1;
END IF;
END LOOP;
END $$;# Create user manually
INSERT INTO users (id, email, name, created_at, updated_at)
VALUES (gen_random_uuid(), 'jo@example.com', 'Jo Admin', NOW(), NOW())
ON CONFLICT (email) DO NOTHING;
# Add to all organizations
INSERT INTO organization_memberships (id, org_id, user_id, role, created_at, updated_at)
SELECT gen_random_uuid(), o.id, u.id, 'OWNER', NOW(), NOW()
FROM organizations o
CROSS JOIN users u
WHERE u.email = 'jo@example.com'
AND NOT EXISTS (
SELECT 1 FROM organization_memberships om
WHERE om.org_id = o.id
AND om.user_id = u.id
);# Option 1: Using the complete YAML file
kubectl apply -f langfuse-org-admin-cronjob.yaml
# Option 2: Using separate files
kubectl apply -f configmap.yaml
kubectl apply -f cronjob.yaml# Check ConfigMap
kubectl get configmap langfuse-sql-scripts -n langfuse-test
# Check CronJob
kubectl get cronjob langfuse-org-admin-updater -n langfuse-test
# View CronJob details
kubectl describe cronjob langfuse-org-admin-updater -n langfuse-test# View upcoming executions
kubectl get events -n langfuse-test --field-selector involvedObject.name=langfuse-org-admin-updater
# Check job history
kubectl get jobs -n langfuse-test -l component=org-admin-updater# Create a manual job to test the cron job
kubectl create job --from=cronjob/langfuse-org-admin-updater manual-test -n langfuse-test
# Monitor the test job
kubectl get jobs -n langfuse-test manual-test
kubectl get pods -n langfuse-test -l job-name=manual-test
# View logs
kubectl logs -l job-name=manual-test -n langfuse-test-- Check if jo@example.com exists
SELECT id, email, name, created_at FROM users WHERE email = 'jo@example.com';
-- Check organization memberships
SELECT
u.email,
u.name,
o.name as org_name,
om.role,
om.created_at as member_since
FROM organization_memberships om
JOIN users u ON om.user_id = u.id
JOIN organizations o ON om.org_id = o.id
WHERE u.email = 'jo@example.com'
ORDER BY o.name;# Quick count of jo@example.com's memberships
kubectl exec -it deployment/langfuse-postgresql -n langfuse-test -- \
psql -U langfuse -d postgres_langfuse -c "
SELECT COUNT(*) as total_org_memberships,
COUNT(CASE WHEN role = 'OWNER' THEN 1 END) as owner_count
FROM organization_memberships om
JOIN users u ON om.user_id = u.id
WHERE u.email = 'jo@example.com';
"Problem: ✗ Database connection failed
Solution: Check PostgreSQL credentials and service names:
# Check PostgreSQL service
kubectl get svc -n langfuse-test | grep postgres
# Check secret
kubectl get secret langfuse-test-postgresql -n langfuse-test -o yaml
# Verify credentials
kubectl get secret langfuse-test-postgresql -n langfuse-test -o jsonpath='{.data.postgres-password}' | base64 -dProblem: ✗ Table 'organization_memberships' not found
Solution: Verify table names in your database:
SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;Problem: User lacks permissions to modify tables
Solution: Ensure the database user has appropriate permissions:
-- Grant necessary permissions
GRANT INSERT, UPDATE, SELECT ON users, organizations, organization_memberships TO langfuse;
GRANT USAGE ON SCHEMA public TO langfuse;Problem: CronJob created but no jobs are executed
Solution: Check CronJob status and schedule:
kubectl get cronjob langfuse-org-admin-updater -n langfuse-test -o yamlProblem: Pods are crashing repeatedly
Solution: Check pod logs and events:
kubectl describe pod -n langfuse-test -l component=org-admin-updater
kubectl logs -n langfuse-test -l component=org-admin-updater# Check pod logs in real-time
kubectl logs -f -n langfuse-test -l component=org-admin-updater
# Delete failed jobs and pods
kubectl delete jobs -n langfuse-test -l component=org-admin-updater
# Clean up and redeploy
kubectl delete configmap langfuse-sql-scripts -n langfuse-test
kubectl delete cronjob langfuse-org-admin-updater -n langfuse-testUpdate the SQL script to use a different email:
-- Change this line in the SQL script:
SELECT id INTO jo_user_id FROM "users" WHERE email = 'your-email@domain.com';Use a different role instead of 'OWNER':
-- Change 'OWNER' to your preferred role:
'ADMIN', 'MANAGER', 'MEMBER', etc.Modify the cron schedule in the CronJob spec:
spec:
schedule: "30 */6 * * *" # Every 6 hours at minute 30
# "0 2 * * *" # Daily at 2 AM
# "0 0 * * 0" # Weekly on Sunday at midnight
# "0 0 1 * *" # Monthly on the 1st at midnightUpdate all namespace references:
# Change from:
namespace: langfuse-test
# To:
namespace: your-namespaceenv:
- name: SMTP_HOST
value: "smtp.gmail.com"
- name: SMTP_PORT
value: "587"
- name: SMTP_USER
value: "your-email@gmail.com"
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
key: smtp-password
name: email-secretsenv:
- name: SLACK_WEBHOOK
valueFrom:
secretKeyRef:
key: webhook-url
name: slack-secretsAdjust resources based on your database size:
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "1000m"# Check current jobs
kubectl get jobs -n langfuse-test -l component=org-admin-updater
# Check recent logs
kubectl logs -n langfuse-test -l component=org-admin-updater --tail=50
# Monitor resource usage
kubectl top pods -n langfuse-test -l component=org-admin-updater
# Check CronJob status
kubectl get cronjob langfuse-org-admin-updater -n langfuse-test -o wide# Delete jobs older than 7 days
kubectl delete jobs -n langfuse-test -l component=org-admin-updater --field-selector=status.successful=1# Update the ConfigMap
kubectl edit configmap langfuse-sql-scripts -n langfuse-test
# Force recreation of jobs
kubectl delete jobs -n langfuse-test -l component=org-admin-updaterAfter successful deployment and execution:
- ✅ CronJob Created:
langfuse-org-admin-updaterscheduled to run hourly - ✅ User Exists:
jo@example.comis created in theuserstable - ✅ Admin Access:
jo@example.comhas OWNER role in ALL organizations - ✅ Idempotent: Job can be run multiple times without issues
- ✅ Logging: Comprehensive logs show processing details
- CronJob schedule is active (
kubectl get cronjob) - ConfigMap contains correct SQL script
- jo@example.com exists in users table
- jo@example.com has OWNER role in organizations table
- Job logs show successful execution
- No errors in recent job runs
- Kubernetes CronJob Documentation
- Langfuse Documentation
- PostgreSQL SQL Reference
- Kubernetes Secrets Documentation
This setup guide provides a complete, production-ready solution for automatically managing organization admin access in Langfuse through a Kubernetes CronJob.