Deep Dive: RAGFlow’s Admin & Superuser Initialization System

RAGFlow is an open-source RAG (Retrieval-Augmented Generation) engine. One of its less-documented but critical subsystems is how it bootstraps administrator accounts at startup. This post dissects the dual-service initialization architecture, the authentication pipeline, and the security implications you should be aware of before deploying to production.


Table of Contents

  1. Architecture Overview
  2. How the Default Superuser Is Created
  3. Docker Environment Defaults
  4. Manual Initialization with --init-superuser
  5. Enabling the Admin Server
  6. init_superuser vs init_default_admin
  7. Authentication Flow
  8. The First Registered User Is NOT a Superuser
  9. Customizing the Default Superuser
  10. Security Recommendations

Architecture Overview

RAGFlow’s codebase contains two independent Python services that share the same MySQL database:

Service Directory Purpose Default Port
Web Service api/ Handles user documents, conversations, and the RAG pipeline 9380
Admin Service admin/ System monitoring, user management, and operational CLI 9381

Both services independently attempt to ensure an administrator account exists at startup, but they use different functions with different behaviors.


How the Default Superuser Is Created

When the Web service starts, it imports init_superuser from api/db/init_data.py:

# api/ragflow_server.py
from api.db.init_data import init_web_data, init_superuser

The init_superuser() function reads its configuration from environment variables, falling back to hardcoded defaults:

DEFAULT_SUPERUSER_NICKNAME = os.getenv("DEFAULT_SUPERUSER_NICKNAME", "admin")
DEFAULT_SUPERUSER_EMAIL = os.getenv("DEFAULT_SUPERUSER_EMAIL", "admin@ragflow.io")
DEFAULT_SUPERUSER_PASSWORD = os.getenv("DEFAULT_SUPERUSER_PASSWORD", "admin")

The password is Base64-encoded before being persisted to MySQL:

user_info = {
    "id": uuid.uuid1().hex,
    "password": encode_to_base64(password),
    "nickname": nickname,
    "is_superuser": True,
    "email": email,
    "creator": "system",
    "status": "1",
}

The record is then saved via UserService.save() into the user table, along with associated tenant, user_tenant, and tenant_llm records.

The Admin service has its own parallel logic. When admin_server.py starts, it calls init_default_admin():

def init_default_admin():
    users = UserService.query(is_superuser=True)
    if not users:
        default_admin = {
            "id": uuid.uuid1().hex,
            "password": encode_to_base64("admin"),
            "nickname": "admin",
            "is_superuser": True,
            "email": "admin@ragflow.io",
            "creator": "system",
            "status": "1",
        }
        if not UserService.save(**default_admin):
            raise AdminException("Can't init admin.", 500)
        add_tenant_for_admin(default_admin, UserTenantRole.OWNER)

Both functions include existence checks to prevent duplicate creation. The key properties of the default account:

Property Value
Email admin@ragflow.io (overridable via DEFAULT_SUPERUSER_EMAIL)
Password admin (overridable via DEFAULT_SUPERUSER_PASSWORD)
Auto-created Yes, at system startup
Storage MySQL user table
Encoding Base64
Role Superuser (is_superuser=True)

Docker Environment Defaults

In a Docker deployment, if no .env file or environment variables are provided, the system uses the hardcoded fallback values and creates a fully functional admin account out of the box.

This “works-without-configuration” design means a fresh docker run produces:

  • Email: admin@ragflow.io
  • Password: admin
  • Nickname: admin

The Docker entrypoint.sh also supports a --init-superuser flag that gets forwarded to the Python process:

--init-superuser)
  INIT_SUPERUSER_ARGS="--init-superuser"
"$PY" api/ragflow_server.py ${INIT_SUPERUSER_ARGS} &

For production, always override the default password via environment variables.


Manual Initialization with --init-superuser

Instead of relying on automatic startup behavior, you can explicitly trigger superuser creation:

python api/ragflow_server.py --init-superuser

The argument is parsed via argparse:

parser.add_argument(
    "--init-superuser", default=False, help="init superuser", action="store_true"
)

When the flag is detected:

if args.init_superuser:
    init_superuser()

This triggers the same init_superuser() function, which:

  1. Checks existence - queries by email to see if the user already exists
  2. Creates the user - builds the user record using environment variables or defaults
  3. Persists to MySQL - saves user, tenant, and LLM configuration records
  4. Validates LLM connectivity - tests that configured chat and embedding models respond

Manual vs Automatic Initialization

Aspect Automatic Manual (--init-superuser)
Trigger System startup Explicit CLI argument
Entry point init_web_data() call Direct init_superuser() call
Flexibility Uses defaults Respects environment variables
Repeat behavior Checked every startup Only when explicitly invoked

Docker Usage

# Direct execution
python api/ragflow_server.py --init-superuser

# Via Docker entrypoint
docker run --entrypoint="/ragflow/docker/entrypoint.sh" ragflow --init-superuser

Enabling the Admin Server

By default, only the Web service runs. The Admin service is activated with --enable-adminserver:

# In docker-compose.yml
command:
  - --enable-adminserver

The entrypoint script processes this flag:

ENABLE_ADMIN_SERVER=0  # Default: disabled

--enable-adminserver)
  ENABLE_ADMIN_SERVER=1

When enabled, an additional process is spawned:

if [[ "${ENABLE_ADMIN_SERVER}" -eq 1 ]]; then
    echo "Starting admin_server..."
    while true; do
        "$PY" admin/server/admin_server.py &
        wait;
        sleep 1;
    done &
fi

You also need to expose the Admin port:

ports:
  - ${SVR_HTTP_PORT}:9380
  - ${ADMIN_SVR_HTTP_PORT}:9381

What the Admin Service Provides

  • Real-time monitoring of the RAGFlow server, Task Executor processes, and dependency services (MySQL, Elasticsearch, Redis, MinIO)
  • User management - create, modify, delete users and their associated knowledge bases and agents
  • Service management - list and inspect system service status
  • CLI interface - the ragflow-cli command-line tool for administrative operations

Kubernetes / Helm Support

args:
  - "--enable-adminserver"

init_superuser vs init_default_admin

These two functions serve different services and differ in several important ways:

Aspect init_superuser (Web) init_default_admin (Admin)
Location api/db/init_data.py admin/server/auth.py
Config source Environment variables Hardcoded values
Password DEFAULT_SUPERUSER_PASSWORD env var (default: "admin") Always "admin"
Existence check Checks by email only Checks superuser existence AND active status
LLM validation Tests chat and embedding model connectivity None
Tenant setup Creates user + tenant + user_tenant + tenant_llm Creates user + tenant via add_tenant_for_admin()

init_superuser - Full User Bootstrapping

def init_superuser(
    nickname=DEFAULT_SUPERUSER_NICKNAME,
    email=DEFAULT_SUPERUSER_EMAIL,
    password=DEFAULT_SUPERUSER_PASSWORD,
    role=UserTenantRole.OWNER
):

This function is designed for the Web service context. It:

  • Supports full customization via environment variables
  • Sets up the complete tenant hierarchy (user, tenant, LLM bindings)
  • Validates that configured LLM models actually respond
  • Is invoked via --init-superuser or during init_web_data()

init_default_admin - Conservative Fallback

def init_default_admin():
    users = UserService.query(is_superuser=True)
    if not users:
        # Create with hardcoded defaults
    elif not any([u.is_active == ActiveEnum.ACTIVE.value for u in users]):
        raise AdminException("No active admin. Please update 'is_active' in db manually.", 500)

This function is the Admin service’s safety net. It:

  • Uses hardcoded credentials (not configurable via env vars)
  • Performs stricter validation - checks not just existence but also active status
  • Raises an exception if all superusers are inactive, forcing manual database intervention
  • Only runs when the Admin service starts

This dual-function design follows the separation of concerns principle:

  • init_superuser is user-configurable for the Web service
  • init_default_admin is a system-level guarantee for the Admin service

Authentication Flow

Both services use JWT-based token authentication, but with different framework integrations.

Admin Service - Flask-Login request_loader

The Admin service registers a request_loader with Flask-Login:

def setup_auth(login_manager):
    @login_manager.request_loader
    def load_user(web_request):
        jwt = Serializer(secret_key=settings.SECRET_KEY)
        authorization = web_request.headers.get("Authorization")

The authentication pipeline:

  1. Extract the Authorization header from the HTTP request
  2. Decode the JWT token using the application secret key
  3. Validate format - the token must be at least 32 characters (UUID format)
  4. Query the database for a user matching the access_token
  5. Return the user object or None

This integrates with the check_admin_auth decorator for permission enforcement:

def check_admin_auth(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        user = UserService.filter_by_id(current_user.id)
        if not user:
            raise UserNotFoundError(current_user.email)
        if not user.is_superuser:
            raise AdminException("Not admin", 403)
        if user.is_active == ActiveEnum.INACTIVE.value:
            raise AdminException(f"User {current_user.email} inactive", 403)
        return func(*args, **kwargs)
    return wrapper

Web Service - Custom LocalProxy Pattern

The Web service uses a custom _load_user() function with Werkzeug’s LocalProxy:

def _load_user():
    jwt = Serializer(secret_key=settings.SECRET_KEY)
    authorization = request.headers.get("Authorization")
    # ... JWT validation logic ...
    # Falls back to API token authentication if JWT fails

current_user = LocalProxy(_load_user)

The Web service adds a fallback: if JWT authentication fails, it attempts API token-based authentication by splitting the Authorization header and looking up the token in the APIToken table.


The First Registered User Is NOT a Superuser

A common misconception: the first user to register through the Web UI does not get superuser privileges. The init_web_data() function has its automatic superuser creation commented out:

# if not UserService.get_all().count():
#    init_superuser()

The registration endpoint explicitly sets is_superuser to False:

"is_superuser": False,

Superuser accounts can only be created through:

  1. python api/ragflow_server.py --init-superuser
  2. The Admin service’s init_default_admin() at startup
User Type Creation Method is_superuser Scope
Regular user Web registration False Own data only
Superuser --init-superuser or Admin service True Full system administration

The system functions normally without a superuser - users can still create knowledge bases and chat - but system-level management operations are unavailable.


Customizing the Default Superuser

Method 1: Docker .env File

DEFAULT_SUPERUSER_EMAIL=custom@example.com
DEFAULT_SUPERUSER_NICKNAME=myadmin
DEFAULT_SUPERUSER_PASSWORD=a_strong_password_here

Method 2: Docker Compose Environment

services:
  ragflow:
    environment:
      - DEFAULT_SUPERUSER_EMAIL=custom@example.com
      - DEFAULT_SUPERUSER_NICKNAME=myadmin
      - DEFAULT_SUPERUSER_PASSWORD=a_strong_password_here

Method 3: Inline Docker Run

docker run -e DEFAULT_SUPERUSER_EMAIL=custom@example.com \
           -e DEFAULT_SUPERUSER_NICKNAME=myadmin \
           -e DEFAULT_SUPERUSER_PASSWORD=a_strong_password_here \
           infiniflow/ragflow:latest

Caveat: The Admin service’s init_default_admin() ignores these environment variables and always uses hardcoded admin@ragflow.io. If you need a fully custom admin email, the Admin service source code must be modified.


Security Recommendations

  1. Always override the default password in production via DEFAULT_SUPERUSER_PASSWORD
  2. Change the password immediately after first login - the default admin is publicly known
  3. Enable the Admin service (--enable-adminserver) for production monitoring and management
  4. Restrict port 9381 - the Admin API should not be publicly accessible
  5. Be aware of Base64 encoding - passwords are Base64-encoded, not hashed. This is a reversible encoding, not a security measure. Treat database access as equivalent to plaintext password access

References