Quick Start
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.
Prerequisites
Before starting, ensure you have:
- Node.js and npm compatible with your Superset version
- Python compatible with your Superset version
- A running Superset development environment
- Basic knowledge of React, TypeScript, and Flask
Step 1: Install the Extensions CLI
First, install the Apache Superset Extensions CLI:
pip install apache-superset-extensions-cli
Step 2: Create a New Extension
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:
- NPM package:
@my-org/hello-world(scoped package for frontend distribution) - Module Federation name:
myOrg_helloWorld(collision-safe JavaScript identifier) - Backend package:
my_org-hello_world(collision-safe Python distribution name) - Python namespace:
superset_extensions.my_org.hello_world
This 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:
my-org.hello-world/
├── extension.json # Extension metadata and configuration
├── backend/ # Backend Python code
│ ├── src/
│ │ └── superset_extensions/
│ │ └── 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
Step 3: Configure Extension Metadata
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 requires
Step 4: Create Backend API
The CLI generated a basic backend/src/superset_extensions/my_org/hello_world/entrypoint.py. We'll create an API endpoint.
Create backend/src/superset_extensions/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:
- Uses
@apidecorator with automatic context detection - Extends
RestApifromsuperset_core.rest_api.api - Uses Flask-AppBuilder decorators (
@expose,@protect,@safe) - Returns responses using
self.response(status_code, result=data) - The endpoint will be accessible at
/extensions/my-org/hello-world/message(automatic extension context) - OpenAPI docstrings are crucial - Flask-AppBuilder uses them to automatically generate interactive API documentation at
/swagger/v1, allowing developers to explore endpoints, understand schemas, and test the API directly from the browser
Update backend/src/superset_extensions/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.
Step 5: Create Frontend Component
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.registerViewis called at module load time — noactivate/deactivatelifecycle needed- The first argument is a
{ id, name }descriptor; the second is the contribution area (e.g.,sqllab.panels); the third is a factory returning the React component authentication.getCSRFToken()retrieves the CSRF token for API calls (used inside components)- Fetch calls to
/extensions/{publisher}/{name}/{endpoint}reach your backend API
Step 6: Install Dependencies
Install the frontend dependencies:
cd frontend
npm install
cd ..
Step 7: Package the Extension
Create a .supx bundle for deployment:
superset-extensions bundle
This command automatically:
- Builds frontend assets using Webpack with Module Federation
- Collects backend Python source files
- Creates a
dist/directory with:manifest.json- Build metadata and asset referencesfrontend/dist/- Built frontend assets (remoteEntry.js, chunks)backend/- Python source files
- Packages everything into
my-org.hello-world-0.1.0.supx- a zip archive with the specific structure required by Superset
Step 8: Deploy to Superset
To 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.
Step 9: Test Your Extension
- Open SQL Lab in Superset
- Look for the "Hello World" panel in the panels dropdown or sidebar
- Open the panel - it should display "Hello from the backend!"
- Check that the message was fetched from your API endpoint
Understanding the Flow
Here's what happens when your extension loads:
- Superset starts: Reads
manifest.jsonfrom the.supxbundle and loads the backend entrypoint - Backend registration:
entrypoint.pyimports your API class, triggering the@apidecorator to register it automatically - Frontend loads: When SQL Lab opens, Superset fetches the remote entry file
- Module Federation: Webpack loads your extension module and resolves
@apache-superset/coretowindow.superset - Registration: The module executes at load time, calling
views.registerViewto register your panel - Rendering: When the user opens your panel, React renders
<HelloWorldPanel /> - API call: The component fetches data from
/extensions/my-org/hello-world/message - Backend response: Your Flask API returns the hello world message
- Display: The component shows the message to the user
Next Steps
Now that you have a working extension, explore:
- Development - Project structure, APIs, and development workflow
- Contribution Types - Other contribution points beyond panels
- Deployment - Packaging and deploying your extension
- Security - Security best practices for extensions
For a complete real-world example, examine the query insights extension in the Superset codebase.