This guide walks you through creating your first Superset extension - a simple “Hello World” panel that displays a message fetched from a backend API endpoint. You'll learn the essential structure and patterns for building full-stack Superset extensions.
Before starting, ensure you have:
First, install the Apache Superset Extensions CLI:
pip install apache-superset-extensions-cli
Use the CLI to scaffold a new extension project. Extensions can include frontend functionality, backend functionality, or both, depending on your needs. This quickstart demonstrates a full-stack extension with both frontend UI components and backend API endpoints to show the complete integration pattern.
superset-extensions init
The CLI will prompt you for information using a three-step publisher workflow:
Extension display name: Hello World Extension name (hello-world): hello-world Publisher (e.g., my-org): my-org Initial version [0.1.0]: 0.1.0 License [Apache-2.0]: Apache-2.0 Include frontend? [Y/n]: Y Include backend? [Y/n]: Y
Publisher Namespaces: Extensions use organizational namespaces similar to VS Code extensions, providing collision-safe naming across organizations:
@my-org/hello-world (scoped package for frontend distribution)myOrg_helloWorld (collision-safe JavaScript identifier)my_org-hello_world (collision-safe Python distribution name)my_org.hello_worldThis approach ensures that extensions from different organizations cannot conflict, even if they use the same technical name (e.g., both acme.dashboard-widgets and corp.dashboard-widgets can coexist).
This creates a complete project structure:
hello-world/
├── extension.json # Extension metadata and configuration
├── backend/ # Backend Python code
│ ├── src/
│ │ └── my_org/
│ │ └── hello_world/
│ │ └── entrypoint.py # Backend registration
│ └── pyproject.toml
└── frontend/ # Frontend TypeScript/React code
├── src/
│ └── index.tsx # Frontend entry point
├── package.json
├── tsconfig.json
└── webpack.config.js
The generated extension.json contains the extension's metadata.
{ "publisher": "my-org", "name": "hello-world", "displayName": "Hello World", "version": "0.1.0", "license": "Apache-2.0", "permissions": ["can_read"] }
Key fields:
publisher: Organizational namespace for the extensionname: Technical identifier (kebab-case)displayName: Human-readable name shown to userspermissions: List of permissions the extension requiresThe CLI generated a basic backend/src/my_org/hello_world/entrypoint.py. We'll create an API endpoint.
Create backend/src/my_org/hello_world/api.py
from flask import Response from flask_appbuilder.api import expose, protect, safe from superset_core.rest_api.api import RestApi from superset_core.rest_api.decorators import api @api( id="hello_world_api", name="Hello World API", description="API endpoints for the Hello World extension" ) class HelloWorldAPI(RestApi): openapi_spec_tag = "Hello World" class_permission_name = "hello_world" @expose("/message", methods=("GET",)) @protect() @safe def get_message(self) -> Response: """Gets a hello world message --- get: description: >- Get a hello world message from the backend responses: 200: description: Hello world message content: application/json: schema: type: object properties: result: type: object properties: message: type: string 401: $ref: '#/components/responses/401' """ return self.response( 200, result={"message": "Hello from the backend!"} )
Key points:
@api decorator with automatic context detectionRestApi from superset_core.rest_api.api@expose, @protect, @safe)self.response(status_code, result=data)/extensions/my-org/hello-world/message (automatic extension context)/swagger/v1, allowing developers to explore endpoints, understand schemas, and test the API directly from the browserUpdate backend/src/my_org/hello_world/entrypoint.py
Replace the generated print statement with API import to trigger registration:
# Importing the API class triggers the @api decorator registration from .api import HelloWorldAPI # noqa: F401
The @api decorator automatically detects extension context and registers your API with proper namespacing.
The CLI generates the frontend configuration files. Below are the key configurations that enable Module Federation integration with Superset.
frontend/package.json
The @apache-superset/core package must be listed in both peerDependencies (to declare runtime compatibility) and devDependencies (to provide TypeScript types during build):
{ "name": "@my-org/hello-world", "version": "0.1.0", "private": true, "license": "Apache-2.0", "scripts": { "start": "webpack serve --mode development", "build": "webpack --stats-error-details --mode production" }, "peerDependencies": { "@apache-superset/core": "^x.x.x", "react": "^x.x.x", "react-dom": "^x.x.x" }, "devDependencies": { "@apache-superset/core": "^x.x.x", "@types/react": "^x.x.x", "ts-loader": "^x.x.x", "typescript": "^x.x.x", "webpack": "^5.x.x", "webpack-cli": "^x.x.x", "webpack-dev-server": "^x.x.x" } }
frontend/webpack.config.js
The webpack configuration requires specific settings for Module Federation. Key settings include externalsType: "window" and externals to map @apache-superset/core to window.superset at runtime, import: false for shared modules to use the host's React instead of bundling a separate copy, and remoteEntry.[contenthash].js for cache busting.
Convention: Superset always loads extensions by requesting the ./index module from the Module Federation container. The exposes entry must be exactly './index': './src/index.tsx' — do not rename or add additional entries. All API registrations must be reachable from that file. See Architecture for a full explanation.
const path = require('path'); const { ModuleFederationPlugin } = require('webpack').container; const packageConfig = require('./package'); const extensionConfig = require('../extension.json'); module.exports = (env, argv) => { const isProd = argv.mode === 'production'; return { entry: isProd ? {} : './src/index.tsx', mode: isProd ? 'production' : 'development', devServer: { port: 3000, headers: { 'Access-Control-Allow-Origin': '*', }, }, output: { clean: true, filename: isProd ? undefined : '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), publicPath: `/api/v1/extensions/${extensionConfig.publisher}/${extensionConfig.name}/`, }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, // Map @apache-superset/core imports to window.superset at runtime externalsType: 'window', externals: { '@apache-superset/core': 'superset', }, module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, plugins: [ new ModuleFederationPlugin({ name: 'myOrg_helloWorld', filename: 'remoteEntry.[contenthash].js', exposes: { './index': './src/index.tsx', }, shared: { react: { singleton: true, requiredVersion: packageConfig.peerDependencies.react, import: false, // Use host's React, don't bundle }, 'react-dom': { singleton: true, requiredVersion: packageConfig.peerDependencies['react-dom'], import: false, }, antd: { singleton: true, requiredVersion: packageConfig.peerDependencies['antd'], import: false, }, }, }), ], }; };
frontend/tsconfig.json
{ "compilerOptions": { "target": "es5", "module": "esnext", "moduleResolution": "node10", "jsx": "react", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src"] }
Create frontend/src/HelloWorldPanel.tsx
Create a new file for the component implementation:
import React, { useEffect, useState } from 'react'; import { authentication } from '@apache-superset/core'; const HelloWorldPanel: React.FC = () => { const [message, setMessage] = useState<string>(''); const [loading, setLoading] = useState(true); const [error, setError] = useState<string>(''); useEffect(() => { const fetchMessage = async () => { try { const csrfToken = await authentication.getCSRFToken(); const response = await fetch('/extensions/my-org/hello-world/message', { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken!, }, }); if (!response.ok) { throw new Error(`Server returned ${response.status}`); } const data = await response.json(); setMessage(data.result.message); } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred'); } finally { setLoading(false); } }; fetchMessage(); }, []); if (loading) { return ( <div style={{ padding: '20px', textAlign: 'center' }}> <p>Loading...</p> </div> ); } if (error) { return ( <div style={{ padding: '20px', color: 'red' }}> <strong>Error:</strong> {error} </div> ); } return ( <div style={{ padding: '20px' }}> <h3>Hello World Extension</h3> <div style={{ padding: '16px', backgroundColor: '#f6ffed', border: '1px solid #b7eb8f', borderRadius: '4px', marginBottom: '16px', }} > <strong>{message}</strong> </div> <p>This message was fetched from the backend API! 🎉</p> </div> ); }; export default HelloWorldPanel;
Update frontend/src/index.tsx
This file is the single entry point Superset loads from every extension. All registrations — views, commands, menus, editors, event listeners — must be made here (or imported and executed from here). Replace the generated code with:
import React from 'react'; import { views } from '@apache-superset/core'; import HelloWorldPanel from './HelloWorldPanel'; views.registerView( { id: 'my-org.hello-world.main', name: 'Hello World' }, 'sqllab.panels', () => <HelloWorldPanel />, );
Key patterns:
views.registerView is called at module load time — no activate/deactivate lifecycle needed{ id, name } descriptor; the second is the contribution area (e.g., sqllab.panels); the third is a factory returning the React componentauthentication.getCSRFToken() retrieves the CSRF token for API calls (used inside components)/extensions/{publisher}/{name}/{endpoint} reach your backend APIInstall the frontend dependencies:
cd frontend npm install cd ..
Create a .supx bundle for deployment:
superset-extensions bundle
This command automatically:
dist/ directory with:manifest.json - Build metadata and asset referencesfrontend/dist/ - Built frontend assets (remoteEntry.js, chunks)backend/ - Python source filesmy-org.hello-world-0.1.0.supx - a zip archive with the specific structure required by SupersetTo deploy your extension, you need to enable extensions support and configure where Superset should load them from.
Configure Superset
Add the following to your superset_config.py:
# Enable extensions feature FEATURE_FLAGS = { "ENABLE_EXTENSIONS": True, } # Set the directory where extensions are stored EXTENSIONS_PATH = "/path/to/extensions/folder"
Copy Extension Bundle
Copy your .supx file to the configured extensions path:
cp my-org.hello-world-0.1.0.supx /path/to/extensions/folder/
Restart Superset
Restart your Superset instance to load the extension:
# Restart your Superset server superset run
Superset will extract and validate the extension metadata, load the assets, register the extension with its capabilities, and make it available for use.
Here's what happens when your extension loads:
manifest.json from the .supx bundle and loads the backend entrypointentrypoint.py imports your API class, triggering the @api decorator to register it automatically@apache-superset/core to window.supersetviews.registerView to register your panel<HelloWorldPanel />/extensions/my-org/hello-world/messageNow that you have a working extension, explore:
For a complete real-world example, examine the query insights extension in the Superset codebase.