This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 35c135852e feat(extensions): add mandatory publisher field to 
extension metadata (#38200)
35c135852e is described below

commit 35c135852e858297c3e54c925919658b9310bb50
Author: Ville Brofeldt <[email protected]>
AuthorDate: Tue Feb 24 09:42:17 2026 -0800

    feat(extensions): add mandatory publisher field to extension metadata 
(#38200)
---
 docs/developer_portal/extensions/quick-start.md    |  73 +-
 .../src/superset_core/extensions/constants.py      |  33 +-
 .../src/superset_core/extensions/types.py          |  26 +-
 .../src/superset_extensions_cli/cli.py             | 197 +++--
 .../templates/extension.json.j2                    |   8 +-
 .../templates/frontend/package.json.j2             |   2 +-
 .../templates/frontend/webpack.config.js.j2        |   2 +-
 .../src/superset_extensions_cli/types.py           |  25 +-
 .../src/superset_extensions_cli/utils.py           | 226 ++++--
 superset-extensions-cli/tests/conftest.py          |  39 +-
 superset-extensions-cli/tests/test_cli_build.py    |  85 ++-
 superset-extensions-cli/tests/test_cli_bundle.py   |  19 +-
 superset-extensions-cli/tests/test_cli_dev.py      |  29 +-
 superset-extensions-cli/tests/test_cli_init.py     | 158 ++--
 .../tests/test_name_transformations.py             | 834 ++++++++++++---------
 superset-extensions-cli/tests/test_templates.py    | 138 ++--
 superset/extensions/api.py                         |  35 +-
 superset/extensions/utils.py                       |  11 +-
 tests/unit_tests/extensions/test_types.py          | 108 ++-
 19 files changed, 1221 insertions(+), 827 deletions(-)

diff --git a/docs/developer_portal/extensions/quick-start.md 
b/docs/developer_portal/extensions/quick-start.md
index a3a08566f1..f14e0be23b 100644
--- a/docs/developer_portal/extensions/quick-start.md
+++ b/docs/developer_portal/extensions/quick-start.md
@@ -51,36 +51,39 @@ Use the CLI to scaffold a new extension project. Extensions 
can include frontend
 superset-extensions init
 ```
 
-The CLI will prompt you for information:
+The CLI will prompt you for information using a three-step publisher workflow:
 
 ```
-Extension name (e.g. Hello World): Hello World
-Extension ID [hello-world]: hello-world
+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
 ```
 
-**Important**: The extension ID must be **globally unique** across all 
Superset extensions and serves as the basis for all technical identifiers:
-- **Frontend package name**: `hello-world` (same as ID, used in package.json)
-- **Webpack Module Federation name**: `helloWorld` (camelCase from ID)
-- **Backend package name**: `hello_world` (snake_case from ID, used in 
project.toml)
-- **Python namespace**: `superset_extensions.hello_world`
+**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 ensures consistent naming across all technical components, even when the 
display name differs significantly from the ID. Since all technical names 
derive from the extension ID, choosing a unique ID automatically ensures all 
generated names are also unique, preventing conflicts between extensions.
+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:
 
 ```
-hello-world/
+my-org.hello-world/
 ├── extension.json           # Extension metadata and configuration
 ├── backend/                 # Backend Python code
 │   ├── src/
 │   │   └── superset_extensions/
-│   │       └── hello_world/
+│   │       └── my_org/
 │   │           ├── __init__.py
-│   │           └── entrypoint.py  # Backend registration
+│   │           └── hello_world/
+│   │               ├── __init__.py
+│   │               └── entrypoint.py  # Backend registration
 │   └── pyproject.toml
 └── frontend/                # Frontend TypeScript/React code
     ├── src/
@@ -96,8 +99,9 @@ The generated `extension.json` contains basic metadata. 
Update it to register yo
 
 ```json
 {
-  "id": "hello-world",
-  "name": "Hello World",
+  "publisher": "my-org",
+  "name": "hello-world",
+  "displayName": "Hello World",
   "version": "0.1.0",
   "license": "Apache-2.0",
   "frontend": {
@@ -106,7 +110,7 @@ The generated `extension.json` contains basic metadata. 
Update it to register yo
         "sqllab": {
           "panels": [
             {
-              "id": "hello-world.main",
+              "id": "my-org.hello-world.main",
               "name": "Hello World"
             }
           ]
@@ -115,29 +119,32 @@ The generated `extension.json` contains basic metadata. 
Update it to register yo
     },
     "moduleFederation": {
       "exposes": ["./index"],
-      "name": "helloWorld"
+      "name": "myOrg_helloWorld"
     }
   },
   "backend": {
-    "entryPoints": ["superset_extensions.hello_world.entrypoint"],
-    "files": ["backend/src/superset_extensions/hello_world/**/*.py"]
+    "entryPoints": ["superset_extensions.my_org.hello_world.entrypoint"],
+    "files": ["backend/src/superset_extensions/my_org/hello_world/**/*.py"]
   },
   "permissions": ["can_read"]
 }
 ```
 
-**Note**: The `moduleFederation.name` is automatically derived from the 
extension ID (`hello-world` → `helloWorld`), and backend entry points use the 
full Python namespace (`superset_extensions.hello_world`).
+**Note**: The `moduleFederation.name` uses collision-safe naming 
(`myOrg_helloWorld`), and backend entry points use the full nested Python 
namespace (`superset_extensions.my_org.hello_world`).
 
 **Key fields:**
 
+- `publisher`: Organizational namespace for the extension
+- `name`: Technical identifier (kebab-case)
+- `displayName`: Human-readable name shown to users
 - `frontend.contributions.views.sqllab.panels`: Registers your panel in SQL Lab
 - `backend.entryPoints`: Python modules to load eagerly when extension starts
 
 ## Step 4: Create Backend API
 
-The CLI generated a basic 
`backend/src/superset_extensions/hello_world/entrypoint.py`. We'll create an 
API endpoint.
+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/hello_world/api.py`**
+**Create `backend/src/superset_extensions/my_org/hello_world/api.py`**
 
 ```python
 from flask import Response
@@ -186,10 +193,10 @@ class HelloWorldAPI(RestApi):
 - Extends `RestApi` from `superset_core.api.types.rest_api`
 - Uses Flask-AppBuilder decorators (`@expose`, `@protect`, `@safe`)
 - Returns responses using `self.response(status_code, result=data)`
-- The endpoint will be accessible at `/extensions/hello-world/message`
+- The endpoint will be accessible at `/extensions/my-org/hello-world/message`
 - 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/hello_world/entrypoint.py`**
+**Update `backend/src/superset_extensions/my_org/hello_world/entrypoint.py`**
 
 Replace the generated print statement with API registration:
 
@@ -213,7 +220,7 @@ The `@apache-superset/core` package must be listed in both 
`peerDependencies` (t
 
 ```json
 {
-  "name": "hello-world",
+  "name": "@my-org/hello-world",
   "version": "0.1.0",
   "private": true,
   "license": "Apache-2.0",
@@ -264,7 +271,7 @@ module.exports = (env, argv) => {
       chunkFilename: "[name].[contenthash].js",
       clean: true,
       path: path.resolve(__dirname, "dist"),
-      publicPath: `/api/v1/extensions/${packageConfig.name}/`,
+      publicPath: `/api/v1/extensions/my-org/hello-world/`,
     },
     resolve: {
       extensions: [".ts", ".tsx", ".js", ".jsx"],
@@ -285,7 +292,7 @@ module.exports = (env, argv) => {
     },
     plugins: [
       new ModuleFederationPlugin({
-        name: packageConfig.name,
+        name: "myOrg_helloWorld",
         filename: "remoteEntry.[contenthash].js",
         exposes: {
           "./index": "./src/index.tsx",
@@ -342,7 +349,7 @@ const HelloWorldPanel: React.FC = () => {
     const fetchMessage = async () => {
       try {
         const csrfToken = await authentication.getCSRFToken();
-        const response = await fetch('/extensions/hello-world/message', {
+        const response = await fetch('/extensions/my-org/hello-world/message', 
{
           method: 'GET',
           headers: {
             'Content-Type': 'application/json',
@@ -415,7 +422,7 @@ import HelloWorldPanel from './HelloWorldPanel';
 
 export const activate = (context: core.ExtensionContext) => {
   context.disposables.push(
-    core.registerViewProvider('hello-world.main', () => <HelloWorldPanel />),
+    core.registerViewProvider('my-org.hello-world.main', () => 
<HelloWorldPanel />),
   );
 };
 
@@ -425,9 +432,9 @@ export const deactivate = () => {};
 **Key patterns:**
 
 - `activate` function is called when the extension loads
-- `core.registerViewProvider` registers the component with ID 
`hello-world.main` (matching `extension.json`)
+- `core.registerViewProvider` registers the component with ID 
`my-org.hello-world.main` (matching `extension.json`)
 - `authentication.getCSRFToken()` retrieves the CSRF token for API calls
-- Fetch calls to `/extensions/{extension_id}/{endpoint}` reach your backend API
+- Fetch calls to `/extensions/{publisher}/{name}/{endpoint}` reach your 
backend API
 - `context.disposables.push()` ensures proper cleanup
 
 ## Step 6: Install Dependencies
@@ -456,7 +463,7 @@ This command automatically:
   - `manifest.json` - Build metadata and asset references
   - `frontend/dist/` - Built frontend assets (remoteEntry.js, chunks)
   - `backend/` - Python source files
-- Packages everything into `hello-world-0.1.0.supx` - a zip archive with the 
specific structure required by Superset
+- 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
 
@@ -481,7 +488,7 @@ EXTENSIONS_PATH = "/path/to/extensions/folder"
 Copy your `.supx` file to the configured extensions path:
 
 ```bash
-cp hello-world-0.1.0.supx /path/to/extensions/folder/
+cp my-org.hello-world-0.1.0.supx /path/to/extensions/folder/
 ```
 
 **Restart Superset**
@@ -512,7 +519,7 @@ Here's what happens when your extension loads:
 4. **Module Federation**: Webpack loads your extension code and resolves 
`@apache-superset/core` to `window.superset`
 5. **Activation**: `activate()` is called, registering your view provider
 6. **Rendering**: When the user opens your panel, React renders 
`<HelloWorldPanel />`
-7. **API call**: Component fetches data from `/extensions/hello-world/message`
+7. **API call**: Component fetches data from 
`/extensions/my-org/hello-world/message`
 8. **Backend response**: Your Flask API returns the hello world message
 9. **Display**: Component shows the message to the user
 
diff --git a/superset-extensions-cli/src/superset_extensions_cli/types.py 
b/superset-core/src/superset_core/extensions/constants.py
similarity index 52%
copy from superset-extensions-cli/src/superset_extensions_cli/types.py
copy to superset-core/src/superset_core/extensions/constants.py
index c7b774ab59..965270d04c 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/types.py
+++ b/superset-core/src/superset_core/extensions/constants.py
@@ -15,26 +15,21 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from typing import TypedDict
+"""
+Constants for extension validation and naming.
+"""
 
+# Publisher validation pattern: lowercase letters, numbers, hyphens; must 
start with
+# letter; no consecutive hyphens or trailing hyphens
+PUBLISHER_PATTERN = r"^[a-z]([a-z0-9]*(-[a-z0-9]+)*)?$"
 
-class ExtensionNames(TypedDict):
-    """Type definition for extension name variants following platform 
conventions."""
+# Technical name validation pattern: lowercase letters, numbers, hyphens; must 
start
+# with letter; no consecutive hyphens or trailing hyphens
+TECHNICAL_NAME_PATTERN = r"^[a-z]([a-z0-9]*(-[a-z0-9]+)*)?$"
 
-    # Extension name (e.g., "Hello World")
-    name: str
+# Display name validation pattern: must start with letter, can contain letters,
+# numbers, spaces, hyphens, underscores, dots
+DISPLAY_NAME_PATTERN = r"^[a-zA-Z][a-zA-Z0-9\s\-_\.]*$"
 
-    # Extension ID - kebab-case primary identifier and npm package name (e.g., 
"hello-world")
-    id: str
-
-    # Module Federation library - camelCase JS identifier (e.g., "helloWorld")
-    mf_name: str
-
-    # Backend package name - snake_case (e.g., "hello_world")
-    backend_name: str
-
-    # Full backend package (e.g., "superset_extensions.hello_world")
-    backend_package: str
-
-    # Backend entry point (e.g., "superset_extensions.hello_world.entrypoint")
-    backend_entry: str
+# Version pattern for semantic versioning
+VERSION_PATTERN = r"^\d+\.\d+\.\d+$"
diff --git a/superset-core/src/superset_core/extensions/types.py 
b/superset-core/src/superset_core/extensions/types.py
index 41ab83d40c..49f3eefe74 100644
--- a/superset-core/src/superset_core/extensions/types.py
+++ b/superset-core/src/superset_core/extensions/types.py
@@ -29,6 +29,13 @@ from typing import Any
 
 from pydantic import BaseModel, Field  # noqa: I001
 
+from superset_core.extensions.constants import (
+    DISPLAY_NAME_PATTERN,
+    PUBLISHER_PATTERN,
+    TECHNICAL_NAME_PATTERN,
+    VERSION_PATTERN,
+)
+
 # =============================================================================
 # Shared components
 # =============================================================================
@@ -102,20 +109,28 @@ class ContributionConfig(BaseModel):
 class BaseExtension(BaseModel):
     """Base fields shared by ExtensionConfig and Manifest."""
 
-    id: str = Field(
+    publisher: str = Field(
         ...,
-        description="Unique extension identifier",
+        description="Publisher/organization namespace",
         min_length=1,
+        pattern=PUBLISHER_PATTERN,
     )
     name: str = Field(
+        ...,
+        description="Technical extension identifier",
+        min_length=1,
+        pattern=TECHNICAL_NAME_PATTERN,
+    )
+    displayName: str = Field(  # noqa: N815
         ...,
         description="Human-readable extension name",
         min_length=1,
+        pattern=DISPLAY_NAME_PATTERN,
     )
     version: str = Field(
         default="0.0.0",
         description="Semantic version string",
-        pattern=r"^\d+\.\d+\.\d+$",
+        pattern=VERSION_PATTERN,
     )
     license: str | None = Field(
         default=None,
@@ -221,6 +236,11 @@ class Manifest(BaseExtension):
     This file is generated by the build tool from extension.json.
     """
 
+    id: str = Field(
+        ...,
+        description="Composite extension ID (publisher.name)",
+        min_length=1,
+    )
     frontend: ManifestFrontend | None = Field(
         default=None,
         description="Frontend manifest",
diff --git a/superset-extensions-cli/src/superset_extensions_cli/cli.py 
b/superset-extensions-cli/src/superset_extensions_cli/cli.py
index 6f333ec694..b26a0eb87d 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/cli.py
+++ b/superset-extensions-cli/src/superset_extensions_cli/cli.py
@@ -42,15 +42,13 @@ from superset_extensions_cli.exceptions import 
ExtensionNameError
 from superset_extensions_cli.types import ExtensionNames
 from superset_extensions_cli.utils import (
     generate_extension_names,
-    kebab_to_camel_case,
     kebab_to_snake_case,
     read_json,
     read_toml,
-    to_kebab_case,
-    to_snake_case,
-    validate_extension_id,
-    validate_npm_package_name,
-    validate_python_package_name,
+    suggest_technical_name,
+    validate_display_name,
+    validate_publisher,
+    validate_technical_name,
 )
 
 REMOTE_ENTRY_REGEX = re.compile(r"^remoteEntry\..+\.js$")
@@ -150,6 +148,9 @@ def build_manifest(cwd: Path, remote_entry: str | None) -> 
Manifest:
 
     extension = ExtensionConfig.model_validate(extension_data)
 
+    # Generate composite ID from publisher and name
+    composite_id = f"{extension.publisher}.{extension.name}"
+
     frontend: ManifestFrontend | None = None
     if extension.frontend and remote_entry:
         frontend = ManifestFrontend(
@@ -163,8 +164,10 @@ def build_manifest(cwd: Path, remote_entry: str | None) -> 
Manifest:
         backend = ManifestBackend(entryPoints=extension.backend.entryPoints)
 
     return Manifest(
-        id=extension.id,
+        id=composite_id,
+        publisher=extension.publisher,
         name=extension.name,
+        displayName=extension.displayName,
         version=extension.version,
         permissions=extension.permissions,
         dependencies=extension.dependencies,
@@ -416,126 +419,110 @@ def dev(ctx: click.Context) -> None:
         click.secho("❌ No directories to watch. Exiting.", fg="red")
 
 
-def prompt_for_extension_name(
-    display_name_opt: str | None, id_opt: str | None
+def prompt_for_extension_info(
+    display_name_opt: str | None,
+    publisher_opt: str | None,
+    technical_name_opt: str | None,
 ) -> ExtensionNames:
     """
-    Prompt for extension name with graceful validation and re-prompting.
+    Prompt for extension info with graceful validation and re-prompting.
 
     Args:
         display_name_opt: Display name provided via CLI option (if any)
-        id_opt: Extension ID provided via CLI option (if any)
+        publisher_opt: Publisher provided via CLI option (if any)
+        technical_name_opt: Technical name provided via CLI option (if any)
 
     Returns:
         ExtensionNames: Validated extension name variants
     """
 
-    # Case 1: Both provided via CLI - validate they work together
-    if display_name_opt and id_opt:
+    # Step 1: Get display name
+    if display_name_opt:
+        display_name = display_name_opt
         try:
-            # Generate all names from display name for consistency
-            temp_names = generate_extension_names(display_name_opt)
-            # Check if the provided ID matches what we'd generate
-            if temp_names["id"] == id_opt:
-                return temp_names
-            else:
-                # If IDs don't match, use the provided ID but validate it
-                validate_extension_id(id_opt)
-                validate_python_package_name(to_snake_case(id_opt))
-                validate_npm_package_name(id_opt)
-                # Create names with the provided ID (derive technical names 
from ID)
-                return ExtensionNames(
-                    name=display_name_opt,
-                    id=id_opt,
-                    mf_name=kebab_to_camel_case(id_opt),
-                    backend_name=kebab_to_snake_case(id_opt),
-                    
backend_package=f"superset_extensions.{kebab_to_snake_case(id_opt)}",
-                    
backend_entry=f"superset_extensions.{kebab_to_snake_case(id_opt)}.entrypoint",
-                )
+            display_name = validate_display_name(display_name)
         except ExtensionNameError as e:
             click.secho(f"❌ {e}", fg="red")
             sys.exit(1)
-
-    # Case 2: Only display name provided - suggest ID
-    if display_name_opt and not id_opt:
-        display_name = display_name_opt
-        try:
-            suggested_names = generate_extension_names(display_name)
-            suggested_id = suggested_names["id"]
-        except ExtensionNameError:
-            suggested_id = to_kebab_case(display_name)
-
-        extension_id = click.prompt("Extension ID", default=suggested_id, 
type=str)
-
-    # Case 3: Only ID provided - ask for display name
-    elif id_opt and not display_name_opt:
-        extension_id = id_opt
-        # Validate the provided ID first
+    else:
+        while True:
+            display_name = click.prompt("Extension display name", type=str)
+            try:
+                display_name = validate_display_name(display_name)
+                break
+            except ExtensionNameError as e:
+                click.secho(f"❌ {e}", fg="red")
+
+    # Step 2: Get technical name (with suggestion from display name)
+    if technical_name_opt:
+        technical_name = technical_name_opt
         try:
-            validate_extension_id(id_opt)
+            validate_technical_name(technical_name)
         except ExtensionNameError as e:
             click.secho(f"❌ {e}", fg="red")
             sys.exit(1)
-
-        # Suggest display name from kebab ID
-        suggested_display = " ".join(word.capitalize() for word in 
id_opt.split("-"))
-        display_name = click.prompt(
-            "Extension name", default=suggested_display, type=str
-        )
-
-    # Case 4: Neither provided - ask for both
     else:
-        display_name = click.prompt("Extension name (e.g. Hello World)", 
type=str)
+        # Suggest technical name from display name
         try:
-            suggested_names = generate_extension_names(display_name)
-            suggested_id = suggested_names["id"]
+            suggested_technical = suggest_technical_name(display_name)
         except ExtensionNameError:
-            suggested_id = to_kebab_case(display_name)
-
-        extension_id = click.prompt("Extension ID", default=suggested_id, 
type=str)
+            suggested_technical = "extension"
 
-    # Final validation loop - try to use generate_extension_names for 
consistent results
-    display_name_failed = False  # Track if display name validation failed
-    while True:
-        try:
-            # First try to generate from display name if possible and it 
hasn't failed before
-            if display_name and not display_name_failed:
-                temp_names = generate_extension_names(display_name)
-                if temp_names["id"] == extension_id:
-                    # Perfect match - use generated names
-                    return temp_names
-
-            # If no match or display name failed, validate manually and 
construct
-            validate_extension_id(extension_id)
-            validate_python_package_name(to_snake_case(extension_id))
-            validate_npm_package_name(extension_id)
-
-            return ExtensionNames(
-                name=display_name,
-                id=extension_id,
-                mf_name=kebab_to_camel_case(extension_id),
-                backend_name=kebab_to_snake_case(extension_id),
-                
backend_package=f"superset_extensions.{kebab_to_snake_case(extension_id)}",
-                
backend_entry=f"superset_extensions.{kebab_to_snake_case(extension_id)}.entrypoint",
+        while True:
+            technical_name = click.prompt(
+                f"Extension name ({suggested_technical})",
+                default=suggested_technical,
+                type=str,
             )
-
+            try:
+                validate_technical_name(technical_name)
+                break
+            except ExtensionNameError as e:
+                click.secho(f"❌ {e}", fg="red")
+
+    # Step 3: Get publisher
+    if publisher_opt:
+        publisher = publisher_opt
+        try:
+            validate_publisher(publisher)
         except ExtensionNameError as e:
             click.secho(f"❌ {e}", fg="red")
-            # If the error came from generate_extension_names, stop trying it
-            if "display_name" in str(e) or not display_name_failed:
-                display_name_failed = True
-            extension_id = click.prompt("Extension ID", type=str)
+            sys.exit(1)
+    else:
+        while True:
+            publisher = click.prompt("Publisher (e.g., my-org)", type=str)
+            try:
+                validate_publisher(publisher)
+                break
+            except ExtensionNameError as e:
+                click.secho(f"❌ {e}", fg="red")
+
+    # Generate all name variants
+    try:
+        return generate_extension_names(display_name, publisher, 
technical_name)
+    except ExtensionNameError as e:
+        click.secho(f"❌ {e}", fg="red")
+        sys.exit(1)
 
 
 @app.command()
 @click.option(
-    "--id",
-    "id_opt",
+    "--publisher",
+    "publisher_opt",
+    default=None,
+    help="Publisher namespace (kebab-case, e.g. my-org)",
+)
[email protected](
+    "--name",
+    "name_opt",
     default=None,
-    help="Extension ID (kebab-case, e.g. hello-world)",
+    help="Technical extension name (kebab-case, e.g. dashboard-widgets)",
 )
 @click.option(
-    "--name", "name_opt", default=None, help="Extension display name (e.g. 
Hello World)"
+    "--display-name",
+    "display_name_opt",
+    default=None,
+    help="Extension display name (e.g. Dashboard Widgets)",
 )
 @click.option(
     "--version", "version_opt", default=None, help="Initial version (default: 
0.1.0)"
@@ -550,15 +537,16 @@ def prompt_for_extension_name(
     "--backend/--no-backend", "backend_opt", default=None, help="Include 
backend"
 )
 def init(
-    id_opt: str | None,
+    publisher_opt: str | None,
     name_opt: str | None,
+    display_name_opt: str | None,
     version_opt: str | None,
     license_opt: str | None,
     frontend_opt: bool | None,
     backend_opt: bool | None,
 ) -> None:
     # Get extension names with graceful validation
-    names = prompt_for_extension_name(name_opt, id_opt)
+    names = prompt_for_extension_info(display_name_opt, publisher_opt, 
name_opt)
 
     version = version_opt or click.prompt("Initial version", default="0.1.0")
     license_ = license_opt or click.prompt("License", default="Apache-2.0")
@@ -618,7 +606,7 @@ def init(
         (frontend_src_dir / "index.tsx").write_text(index_tsx)
         click.secho("✅ Created frontend folder structure", fg="green")
 
-    # Initialize backend files with superset_extensions namespace
+    # Initialize backend files with superset_extensions.publisher.name 
structure
     if include_backend:
         backend_dir = target_dir / "backend"
         backend_dir.mkdir()
@@ -629,8 +617,14 @@ def init(
         namespace_dir = backend_src_dir / "superset_extensions"
         namespace_dir.mkdir()
 
-        # Create extension package directory
-        extension_package_dir = namespace_dir / names["backend_name"]
+        # Create publisher directory (e.g., superset_extensions/my_org)
+        publisher_snake = kebab_to_snake_case(names["publisher"])
+        publisher_dir = namespace_dir / publisher_snake
+        publisher_dir.mkdir()
+
+        # Create extension package directory (e.g., 
superset_extensions/my_org/dashboard_widgets)
+        name_snake = kebab_to_snake_case(names["name"])
+        extension_package_dir = publisher_dir / name_snake
         extension_package_dir.mkdir()
 
         # backend files
@@ -639,6 +633,7 @@ def init(
 
         # Namespace package __init__.py (empty for namespace)
         (namespace_dir / "__init__.py").write_text("")
+        (publisher_dir / "__init__.py").write_text("")
 
         # Extension package files
         init_py = 
env.get_template("backend/src/package/__init__.py.j2").render(ctx)
@@ -651,7 +646,7 @@ def init(
         click.secho("✅ Created backend folder structure", fg="green")
 
     click.secho(
-        f"🎉 Extension {names['name']} (ID: {names['id']}) initialized at 
{target_dir}",
+        f"🎉 Extension {names['display_name']} (ID: {names['id']}) initialized 
at {target_dir}",
         fg="cyan",
     )
 
diff --git 
a/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
 
b/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
index 4f4fb32e16..08410b6d6d 100644
--- 
a/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
+++ 
b/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
@@ -1,6 +1,7 @@
 {
-  "id": "{{ id }}",
+  "publisher": "{{ publisher }}",
   "name": "{{ name }}",
+  "displayName": "{{ display_name }}",
   "version": "{{ version }}",
   "license": "{{ license }}",
   {% if include_frontend -%}
@@ -8,7 +9,8 @@
     "contributions": {
       "commands": [],
       "views": {},
-      "menus": {}
+      "menus": {},
+      "editors": []
     },
     "moduleFederation": {
       "name": "{{ mf_name }}",
@@ -19,7 +21,7 @@
   {% if include_backend -%}
   "backend": {
     "entryPoints": ["{{ backend_entry }}"],
-    "files": ["backend/src/superset_extensions/{{ backend_name }}/**/*.py"]
+    "files": ["backend/src/{{ backend_path|replace('.', '/') }}/**/*.py"]
   },
   {% endif -%}
   "permissions": []
diff --git 
a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/package.json.j2
 
b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/package.json.j2
index 6b602b9845..be4d844aa2 100644
--- 
a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/package.json.j2
+++ 
b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/package.json.j2
@@ -1,5 +1,5 @@
 {
-  "name": "{{ id }}",
+  "name": "{{ npm_name }}",
   "version": "{{ version }}",
   "main": "dist/main.js",
   "types": "dist/publicAPI.d.ts",
diff --git 
a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
 
b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
index 5a2342ed69..b3bf513989 100644
--- 
a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
+++ 
b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
@@ -19,7 +19,7 @@ module.exports = (env, argv) => {
       filename: isProd ? undefined : "[name].[contenthash].js",
       chunkFilename: "[name].[contenthash].js",
       path: path.resolve(__dirname, "dist"),
-      publicPath: `/api/v1/extensions/${packageConfig.name}/`,
+      publicPath: `/api/v1/extensions/{{ publisher }}/{{ name }}/`,
     },
     resolve: {
       extensions: [".ts", ".tsx", ".js", ".jsx"],
diff --git a/superset-extensions-cli/src/superset_extensions_cli/types.py 
b/superset-extensions-cli/src/superset_extensions_cli/types.py
index c7b774ab59..740b48eb55 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/types.py
+++ b/superset-extensions-cli/src/superset_extensions_cli/types.py
@@ -21,20 +21,29 @@ from typing import TypedDict
 class ExtensionNames(TypedDict):
     """Type definition for extension name variants following platform 
conventions."""
 
-    # Extension name (e.g., "Hello World")
+    # Publisher namespace (e.g., "my-org")
+    publisher: str
+
+    # Technical extension name (e.g., "dashboard-widgets")
     name: str
 
-    # Extension ID - kebab-case primary identifier and npm package name (e.g., 
"hello-world")
+    # Human-readable display name (e.g., "Dashboard Widgets")
+    display_name: str
+
+    # Composite extension ID - publisher.name (e.g., 
"my-org.dashboard-widgets")
     id: str
 
-    # Module Federation library - camelCase JS identifier (e.g., "helloWorld")
-    mf_name: str
+    # NPM package name - @publisher/name (e.g., "@my-org/dashboard-widgets")
+    npm_name: str
 
-    # Backend package name - snake_case (e.g., "hello_world")
-    backend_name: str
+    # Module Federation library - publisherCamel_nameCamel (e.g., 
"myOrg_dashboardWidgets")
+    mf_name: str
 
-    # Full backend package (e.g., "superset_extensions.hello_world")
+    # Backend package name with hyphens for distribution (e.g., 
"my_org-dashboard_widgets")
     backend_package: str
 
-    # Backend entry point (e.g., "superset_extensions.hello_world.entrypoint")
+    # Full backend import path (e.g., 
"superset_extensions.my_org.dashboard_widgets")
+    backend_path: str
+
+    # Backend entry point (e.g., 
"superset_extensions.my_org.dashboard_widgets.entrypoint")
     backend_entry: str
diff --git a/superset-extensions-cli/src/superset_extensions_cli/utils.py 
b/superset-extensions-cli/src/superset_extensions_cli/utils.py
index 9aa17e0936..26c1f28584 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/utils.py
+++ b/superset-extensions-cli/src/superset_extensions_cli/utils.py
@@ -21,6 +21,11 @@ import sys
 from pathlib import Path
 from typing import Any
 
+from superset_core.extensions.constants import (
+    DISPLAY_NAME_PATTERN,
+    PUBLISHER_PATTERN,
+    TECHNICAL_NAME_PATTERN,
+)
 from superset_extensions_cli.exceptions import ExtensionNameError
 from superset_extensions_cli.types import ExtensionNames
 
@@ -82,8 +87,10 @@ NPM_RESERVED = {
     "bower_components",
 }
 
-# Extension name pattern: lowercase, start with letter or number, alphanumeric 
+ hyphens
-EXTENSION_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9]*(?:-[a-z0-9]+)*$")
+# Compiled patterns for publisher/name validation
+PUBLISHER_REGEX = re.compile(PUBLISHER_PATTERN)
+TECHNICAL_NAME_REGEX = re.compile(TECHNICAL_NAME_PATTERN)
+DISPLAY_NAME_REGEX = re.compile(DISPLAY_NAME_PATTERN)
 
 
 def read_toml(path: Path) -> dict[str, Any] | None:
@@ -166,139 +173,210 @@ def name_to_kebab_case(name: str) -> str:
     return _normalized_to_kebab(normalized)
 
 
-# Legacy functions for backward compatibility
-def to_kebab_case(name: str) -> str:
-    """Convert display name to kebab-case. For new code, use 
name_to_kebab_case."""
-    return name_to_kebab_case(name)
+def validate_python_package_name(name: str) -> None:
+    """
+    Validate Python package name (snake_case format).
+
+    Raises:
+        ExtensionNameError: If name is invalid
+    """
+    # Check if it starts with a number (invalid for Python identifiers)
+    if name[0].isdigit():
+        raise ExtensionNameError(f"Package name '{name}' cannot start with a 
number")
+
+    # Check if the first part (before any underscore) is a Python keyword
+    if (first_part := name.split("_")[0]) in PYTHON_KEYWORDS:
+        raise ExtensionNameError(
+            f"Package name cannot start with Python keyword '{first_part}'"
+        )
+
+    # Check if it's a valid Python identifier
+    if not name.replace("_", "a").isalnum():
+        raise ExtensionNameError(f"'{name}' is not a valid Python package 
name")
+
 
+def validate_npm_package_name(name: str) -> None:
+    """
+    Validate npm package name (kebab-case format).
 
-def to_snake_case(kebab_name: str) -> str:
-    """Convert kebab-case to snake_case. For new code, use 
kebab_to_snake_case."""
-    return kebab_to_snake_case(kebab_name)
+    Raises:
+        ExtensionNameError: If name is invalid
+    """
+    if name.lower() in NPM_RESERVED:
+        raise ExtensionNameError(f"'{name}' is a reserved npm package name")
 
 
-def validate_extension_id(extension_id: str) -> None:
+def validate_publisher(publisher: str) -> None:
     """
-    Validate extension ID format (kebab-case).
+    Validate publisher namespace format.
+
+    Args:
+        publisher: Publisher namespace (e.g., 'my-org')
 
     Raises:
-        ExtensionNameError: If ID is invalid
+        ExtensionNameError: If publisher is invalid
     """
-    if not extension_id:
-        raise ExtensionNameError("Extension ID cannot be empty")
+    if not publisher:
+        raise ExtensionNameError("Publisher cannot be empty")
 
-    # Check for leading/trailing hyphens first
-    if extension_id.startswith("-"):
-        raise ExtensionNameError("Extension ID cannot start with hyphens")
+    if not PUBLISHER_REGEX.match(publisher):
+        raise ExtensionNameError(
+            "Publisher must start with a letter and contain only lowercase 
letters, numbers, and hyphens (e.g., 'my-org')"
+        )
 
-    if extension_id.endswith("-"):
-        raise ExtensionNameError("Extension ID cannot end with hyphens")
 
-    # Check for consecutive hyphens
-    if "--" in extension_id:
-        raise ExtensionNameError("Extension ID cannot have consecutive 
hyphens")
+def validate_technical_name(name: str) -> None:
+    """
+    Validate technical extension name format.
+
+    Args:
+        name: Technical extension name (e.g., 'dashboard-widgets')
+
+    Raises:
+        ExtensionNameError: If name is invalid
+    """
+    if not name:
+        raise ExtensionNameError("Extension name cannot be empty")
 
-    # Check overall pattern
-    if not EXTENSION_NAME_PATTERN.match(extension_id):
+    if not TECHNICAL_NAME_REGEX.match(name):
         raise ExtensionNameError(
-            "Use lowercase letters, numbers, and hyphens only (e.g. 
hello-world)"
+            "Extension name must start with a letter and contain only 
lowercase letters, numbers, and hyphens (e.g., 'dashboard-widgets')"
         )
 
 
-def validate_extension_name(name: str) -> str:
+def validate_display_name(display_name: str) -> str:
     """
-    Validate and normalize extension name (human-readable).
+    Validate and normalize display name format.
 
     Args:
-        extension_name: Raw extension name input
+        display_name: Human-readable extension name
 
     Returns:
-        Cleaned extension name
+        Cleaned display name
 
     Raises:
-        ExtensionNameError: If extension name is invalid
+        ExtensionNameError: If display name is invalid
     """
-    if not name or not name.strip():
-        raise ExtensionNameError("Extension name cannot be empty")
+    if not display_name or not display_name.strip():
+        raise ExtensionNameError("Display name cannot be empty")
 
     # Normalize whitespace: strip and collapse multiple spaces
-    normalized = " ".join(name.strip().split())
+    normalized = " ".join(display_name.strip().split())
+
+    if not DISPLAY_NAME_REGEX.match(normalized):
+        raise ExtensionNameError(
+            "Display name must start with a letter and can contain letters, 
numbers, spaces, hyphens, underscores, and dots (e.g., 'Dashboard Widgets')"
+        )
 
     # Check for only whitespace/special chars after normalization
     if not any(c.isalnum() for c in normalized):
         raise ExtensionNameError(
-            "Extension name must contain at least one letter or number"
+            "Display name must contain at least one letter or number"
         )
 
     return normalized
 
 
-def validate_python_package_name(name: str) -> None:
+def suggest_technical_name(display_name: str) -> str:
     """
-    Validate Python package name (snake_case format).
+    Suggest technical name from display name.
 
-    Raises:
-        ExtensionNameError: If name is invalid
+    Args:
+        display_name: Human-readable name (e.g., "Dashboard Widgets!")
+
+    Returns:
+        Technical name suggestion (e.g., "dashboard-widgets")
     """
-    # Check if it starts with a number (invalid for Python identifiers)
-    if name[0].isdigit():
-        raise ExtensionNameError(f"Package name '{name}' cannot start with a 
number")
+    # Normalize for identifiers
+    normalized = _normalize_for_identifiers(display_name)
 
-    # Check if the first part (before any underscore) is a Python keyword
-    if (first_part := name.split("_")[0]) in PYTHON_KEYWORDS:
+    # Convert to kebab-case
+    technical_name = _normalized_to_kebab(normalized)
+
+    # Remove any leading/trailing hyphens that might result from edge cases
+    technical_name = technical_name.strip("-")
+
+    # Ensure we have something left
+    if not technical_name:
         raise ExtensionNameError(
-            f"Package name cannot start with Python keyword '{first_part}'"
+            "Display name must contain at least one letter or number"
         )
 
-    # Check if it's a valid Python identifier
-    if not name.replace("_", "a").isalnum():
-        raise ExtensionNameError(f"'{name}' is not a valid Python package 
name")
+    return technical_name
 
 
-def validate_npm_package_name(name: str) -> None:
+def get_module_federation_name(publisher: str, name: str) -> str:
     """
-    Validate npm package name (kebab-case format).
+    Generate Module Federation container name.
 
-    Raises:
-        ExtensionNameError: If name is invalid
+    Args:
+        publisher: Publisher namespace (e.g., 'my-org')
+        name: Technical name (e.g., 'dashboard-widgets')
+
+    Returns:
+        Module Federation name (e.g., 'myOrg_dashboardWidgets')
     """
-    if name.lower() in NPM_RESERVED:
-        raise ExtensionNameError(f"'{name}' is a reserved npm package name")
+    publisher_camel = kebab_to_camel_case(publisher)
+    name_camel = kebab_to_camel_case(name)
+    return f"{publisher_camel}_{name_camel}"
 
 
-def generate_extension_names(name: str) -> ExtensionNames:
+def generate_extension_names(
+    display_name: str, publisher: str, technical_name: str | None = None
+) -> ExtensionNames:
     """
-    Generate all extension name variants from display name input.
+    Generate all extension name variants from input.
 
-    Flow: Display Name -> Generate ID -> Derive Technical Names from ID
-    Example: "Hello World" -> "hello-world" -> "helloWorld"/"hello_world" 
(from ID)
+    Args:
+        display_name: Human-readable name (e.g., "Dashboard Widgets")
+        publisher: Publisher namespace (e.g., "my-org")
+        technical_name: Technical name override, or None to auto-generate
 
     Returns:
         ExtensionNames: Dictionary with all name variants
 
     Raises:
-        ExtensionNameError: If any generated name is invalid
+        ExtensionNameError: If any name is invalid
     """
-    # Validate and normalize the extension name
-    name = validate_extension_name(name)
+    # Validate and normalize inputs
+    display_name = validate_display_name(display_name)
+    validate_publisher(publisher)
+
+    # Use provided technical name or generate from display name
+    if technical_name is None:
+        technical_name = suggest_technical_name(display_name)
+    else:
+        validate_technical_name(technical_name)
+
+    # Generate composite ID
+    composite_id = f"{publisher}.{technical_name}"
+
+    # Generate NPM package name
+    npm_name = f"@{publisher}/{technical_name}"
 
-    # Generate ID from display name
-    kebab_name = name_to_kebab_case(name)
+    # Generate Module Federation name
+    mf_name = get_module_federation_name(publisher, technical_name)
 
-    # Derive all technical names from the generated ID (not display name)
-    snake_name = kebab_to_snake_case(kebab_name)
-    camel_name = kebab_to_camel_case(kebab_name)
+    # Generate backend names with collision protection
+    publisher_snake = kebab_to_snake_case(publisher)
+    name_snake = kebab_to_snake_case(technical_name)
+    backend_package = f"{publisher_snake}-{name_snake}"
+    backend_path = f"superset_extensions.{publisher_snake}.{name_snake}"
+    backend_entry = f"{backend_path}.entrypoint"
 
     # Validate the generated names
-    validate_extension_id(kebab_name)
-    validate_python_package_name(snake_name)
-    validate_npm_package_name(kebab_name)
+    validate_python_package_name(publisher_snake)
+    validate_python_package_name(name_snake)
+    validate_npm_package_name(technical_name)
 
     return ExtensionNames(
-        name=name,
-        id=kebab_name,
-        mf_name=camel_name,
-        backend_name=snake_name,
-        backend_package=f"superset_extensions.{snake_name}",
-        backend_entry=f"superset_extensions.{snake_name}.entrypoint",
+        publisher=publisher,
+        name=technical_name,
+        display_name=display_name,
+        id=composite_id,
+        npm_name=npm_name,
+        mf_name=mf_name,
+        backend_package=backend_package,
+        backend_path=backend_path,
+        backend_entry=backend_entry,
     )
diff --git a/superset-extensions-cli/tests/conftest.py 
b/superset-extensions-cli/tests/conftest.py
index 7292428c4e..2e683835ac 100644
--- a/superset-extensions-cli/tests/conftest.py
+++ b/superset-extensions-cli/tests/conftest.py
@@ -46,8 +46,9 @@ def isolated_filesystem(tmp_path):
 def extension_params():
     """Default parameters for extension creation."""
     return {
-        "id": "test_extension",
-        "name": "Test Extension",
+        "publisher": "test-org",
+        "name": "test-extension",
+        "displayName": "Test Extension",
         "version": "0.1.0",
         "license": "Apache-2.0",
         "include_frontend": True,
@@ -58,25 +59,25 @@ def extension_params():
 @pytest.fixture
 def cli_input_both():
     """CLI input for creating extension with both frontend and backend."""
-    return "Test Extension\n\n0.1.0\nApache-2.0\ny\ny\n"
+    return "Test Extension\n\ntest-org\n0.1.0\nApache-2.0\ny\ny\n"
 
 
 @pytest.fixture
 def cli_input_frontend_only():
     """CLI input for creating extension with frontend only."""
-    return "Test Extension\n\n0.1.0\nApache-2.0\ny\nn\n"
+    return "Test Extension\n\ntest-org\n0.1.0\nApache-2.0\ny\nn\n"
 
 
 @pytest.fixture
 def cli_input_backend_only():
     """CLI input for creating extension with backend only."""
-    return "Test Extension\n\n0.1.0\nApache-2.0\nn\ny\n"
+    return "Test Extension\n\ntest-org\n0.1.0\nApache-2.0\nn\ny\n"
 
 
 @pytest.fixture
 def cli_input_neither():
     """CLI input for creating extension with neither frontend nor backend."""
-    return "Test Extension\n\n0.1.0\nApache-2.0\nn\nn\n"
+    return "Test Extension\n\ntest-org\n0.1.0\nApache-2.0\nn\nn\n"
 
 
 @pytest.fixture
@@ -86,10 +87,11 @@ def extension_setup_for_dev():
     def _setup(base_path: Path) -> None:
         import json
 
-        # Create extension.json
+        # Create extension.json with new structure
         extension_json = {
-            "id": "test_extension",
-            "name": "Test Extension",
+            "publisher": "test-org",
+            "name": "test-extension",
+            "displayName": "Test Extension",
             "version": "1.0.0",
             "permissions": [],
         }
@@ -113,10 +115,12 @@ def extension_setup_for_bundling():
         dist_dir = base_path / "dist"
         dist_dir.mkdir(parents=True)
 
-        # Create manifest.json
+        # Create manifest.json with composite ID
         manifest = {
-            "id": "test_extension",
-            "name": "Test Extension",
+            "id": "test-org.test-extension",
+            "publisher": "test-org",
+            "name": "test-extension",
+            "displayName": "Test Extension",
             "version": "1.0.0",
             "permissions": [],
         }
@@ -128,8 +132,15 @@ def extension_setup_for_bundling():
         (frontend_dir / "remoteEntry.abc123.js").write_text("// remote entry")
         (frontend_dir / "main.js").write_text("// main js")
 
-        # Create some backend files
-        backend_dir = dist_dir / "backend" / "src" / "test_extension"
+        # Create some backend files - updated path structure
+        backend_dir = (
+            dist_dir
+            / "backend"
+            / "src"
+            / "superset_extensions"
+            / "test_org"
+            / "test_extension"
+        )
         backend_dir.mkdir(parents=True)
         (backend_dir / "__init__.py").write_text("# init")
 
diff --git a/superset-extensions-cli/tests/test_cli_build.py 
b/superset-extensions-cli/tests/test_cli_build.py
index 7704a2dccd..94116e76b5 100644
--- a/superset-extensions-cli/tests/test_cli_build.py
+++ b/superset-extensions-cli/tests/test_cli_build.py
@@ -52,20 +52,33 @@ def extension_with_build_structure():
 
         # Create extension.json
         extension_json = {
-            "id": "test_extension",
-            "name": "Test Extension",
+            "publisher": "test-org",
+            "name": "test-extension",
+            "displayName": "Test Extension",
             "version": "1.0.0",
             "permissions": [],
         }
 
         if include_frontend:
             extension_json["frontend"] = {
-                "contributions": {"commands": []},
-                "moduleFederation": {"exposes": ["./index"]},
+                "contributions": {
+                    "commands": [],
+                    "views": {},
+                    "menus": {},
+                    "editors": [],
+                },
+                "moduleFederation": {
+                    "exposes": ["./index"],
+                    "name": "testOrg_testExtension",
+                },
             }
 
         if include_backend:
-            extension_json["backend"] = {"entryPoints": 
["test_extension.entrypoint"]}
+            extension_json["backend"] = {
+                "entryPoints": [
+                    "superset_extensions.test_org.test_extension.entrypoint"
+                ]
+            }
 
         (base_path / "extension.json").write_text(json.dumps(extension_json))
 
@@ -230,16 +243,27 @@ def 
test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
     """Test build_manifest creates correct manifest from extension.json."""
     # Create extension.json
     extension_data = {
-        "id": "test_extension",
-        "name": "Test Extension",
+        "publisher": "test-org",
+        "name": "test-extension",
+        "displayName": "Test Extension",
         "version": "1.0.0",
         "permissions": ["read_data"],
         "dependencies": ["some_dep"],
         "frontend": {
-            "contributions": {"commands": [{"id": "test_command", "title": 
"Test"}]},
-            "moduleFederation": {"exposes": ["./index"]},
+            "contributions": {
+                "commands": [{"id": "test_command", "title": "Test"}],
+                "views": {},
+                "menus": {},
+                "editors": [],
+            },
+            "moduleFederation": {
+                "exposes": ["./index"],
+                "name": "testOrg_testExtension",
+            },
+        },
+        "backend": {
+            "entryPoints": 
["superset_extensions.test_org.test_extension.entrypoint"]
         },
-        "backend": {"entryPoints": ["test_extension.entrypoint"]},
     }
     extension_json = isolated_filesystem / "extension.json"
     extension_json.write_text(json.dumps(extension_data))
@@ -247,8 +271,10 @@ def 
test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
     manifest = build_manifest(isolated_filesystem, "remoteEntry.abc123.js")
 
     # Verify manifest structure
-    assert manifest.id == "test_extension"
-    assert manifest.name == "Test Extension"
+    assert manifest.id == "test-org.test-extension"  # Composite ID
+    assert manifest.publisher == "test-org"
+    assert manifest.name == "test-extension"
+    assert manifest.displayName == "Test Extension"
     assert manifest.version == "1.0.0"
     assert manifest.permissions == ["read_data"]
     assert manifest.dependencies == ["some_dep"]
@@ -263,15 +289,18 @@ def 
test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
 
     # Verify backend section
     assert manifest.backend is not None
-    assert manifest.backend.entryPoints == ["test_extension.entrypoint"]
+    assert manifest.backend.entryPoints == [
+        "superset_extensions.test_org.test_extension.entrypoint"
+    ]
 
 
 @pytest.mark.unit
 def test_build_manifest_handles_minimal_extension(isolated_filesystem):
     """Test build_manifest with minimal extension.json (no 
frontend/backend)."""
     extension_data = {
-        "id": "minimal_extension",
-        "name": "Minimal Extension",
+        "publisher": "minimal-org",
+        "name": "minimal-extension",
+        "displayName": "Minimal Extension",
         "version": "0.1.0",
         "permissions": [],
     }
@@ -280,8 +309,10 @@ def 
test_build_manifest_handles_minimal_extension(isolated_filesystem):
 
     manifest = build_manifest(isolated_filesystem, None)
 
-    assert manifest.id == "minimal_extension"
-    assert manifest.name == "Minimal Extension"
+    assert manifest.id == "minimal-org.minimal-extension"  # Composite ID
+    assert manifest.publisher == "minimal-org"
+    assert manifest.name == "minimal-extension"
+    assert manifest.displayName == "Minimal Extension"
     assert manifest.version == "0.1.0"
     assert manifest.permissions == []
     assert manifest.dependencies == []  # Default empty list
@@ -393,8 +424,9 @@ def 
test_rebuild_backend_calls_copy_and_shows_message(isolated_filesystem):
 
     # Create extension.json
     extension_json = {
-        "id": "test",
-        "name": "Test Extension",
+        "publisher": "test-org",
+        "name": "test-extension",
+        "displayName": "Test Extension",
         "version": "1.0.0",
         "permissions": [],
     }
@@ -420,8 +452,9 @@ def 
test_copy_backend_files_skips_non_files(isolated_filesystem):
 
     # Create extension.json with backend file patterns
     extension_data = {
-        "id": "test_ext",
-        "name": "Test Extension",
+        "publisher": "test-org",
+        "name": "test-ext",
+        "displayName": "Test Extension",
         "version": "1.0.0",
         "permissions": [],
         "backend": {
@@ -457,8 +490,9 @@ def 
test_copy_backend_files_copies_matched_files(isolated_filesystem):
 
     # Create extension.json with backend file patterns
     extension_data = {
-        "id": "test_ext",
-        "name": "Test Extension",
+        "publisher": "test-org",
+        "name": "test-ext",
+        "displayName": "Test Extension",
         "version": "1.0.0",
         "permissions": [],
         "backend": {"files": ["backend/src/test_ext/**/*.py"]},
@@ -480,8 +514,9 @@ def 
test_copy_backend_files_copies_matched_files(isolated_filesystem):
 def test_copy_backend_files_handles_no_backend_config(isolated_filesystem):
     """Test copy_backend_files handles extension.json without backend 
config."""
     extension_data = {
-        "id": "frontend_only",
-        "name": "Frontend Only Extension",
+        "publisher": "frontend-org",
+        "name": "frontend-only",
+        "displayName": "Frontend Only Extension",
         "version": "1.0.0",
         "permissions": [],
     }
diff --git a/superset-extensions-cli/tests/test_cli_bundle.py 
b/superset-extensions-cli/tests/test_cli_bundle.py
index a9c4ffd625..3b09c59a2f 100644
--- a/superset-extensions-cli/tests/test_cli_bundle.py
+++ b/superset-extensions-cli/tests/test_cli_bundle.py
@@ -43,10 +43,10 @@ def test_bundle_command_creates_zip_with_default_name(
     result = cli_runner.invoke(app, ["bundle"])
 
     assert result.exit_code == 0
-    assert "✅ Bundle created: test_extension-1.0.0.supx" in result.output
+    assert "✅ Bundle created: test-org.test-extension-1.0.0.supx" in 
result.output
 
     # Verify zip file was created
-    zip_path = isolated_filesystem / "test_extension-1.0.0.supx"
+    zip_path = isolated_filesystem / "test-org.test-extension-1.0.0.supx"
     assert_file_exists(zip_path)
 
     # Verify zip contents
@@ -55,7 +55,10 @@ def test_bundle_command_creates_zip_with_default_name(
         assert "manifest.json" in file_list
         assert "frontend/dist/remoteEntry.abc123.js" in file_list
         assert "frontend/dist/main.js" in file_list
-        assert "backend/src/test_extension/__init__.py" in file_list
+        assert (
+            
"backend/src/superset_extensions/test_org/test_extension/__init__.py"
+            in file_list
+        )
 
 
 @pytest.mark.cli
@@ -100,7 +103,7 @@ def test_bundle_command_with_output_directory(
     assert result.exit_code == 0
 
     # Verify zip file was created in output directory
-    expected_path = output_dir / "test_extension-1.0.0.supx"
+    expected_path = output_dir / "test-org.test-extension-1.0.0.supx"
     assert_file_exists(expected_path)
     assert f"✅ Bundle created: {expected_path}" in result.output
 
@@ -159,8 +162,10 @@ def test_bundle_includes_all_files_recursively(
 
     # Manifest
     manifest = {
-        "id": "complex_extension",
-        "name": "Complex Extension",
+        "id": "complex-org.complex-extension",
+        "publisher": "complex-org",
+        "name": "complex-extension",
+        "displayName": "Complex Extension",
         "version": "2.1.0",
         "permissions": [],
     }
@@ -191,7 +196,7 @@ def test_bundle_includes_all_files_recursively(
     assert result.exit_code == 0
 
     # Verify zip file and contents
-    zip_path = isolated_filesystem / "complex_extension-2.1.0.supx"
+    zip_path = isolated_filesystem / "complex-org.complex-extension-2.1.0.supx"
     assert_file_exists(zip_path)
 
     with zipfile.ZipFile(zip_path, "r") as zipf:
diff --git a/superset-extensions-cli/tests/test_cli_dev.py 
b/superset-extensions-cli/tests/test_cli_dev.py
index 8d4d4f42a8..1a186907d9 100644
--- a/superset-extensions-cli/tests/test_cli_dev.py
+++ b/superset-extensions-cli/tests/test_cli_dev.py
@@ -49,7 +49,13 @@ def test_dev_command_starts_watchers(
     """Test dev command starts file watchers."""
     # Setup mocks
     mock_rebuild_frontend.return_value = "remoteEntry.abc123.js"
-    mock_build_manifest.return_value = Manifest(id="test", name="test", 
version="1.0.0")
+    mock_build_manifest.return_value = Manifest(
+        id="test-org.test-extension",
+        publisher="test-org",
+        name="test-extension",
+        displayName="Test Extension",
+        version="1.0.0",
+    )
 
     mock_observer = Mock()
     mock_observer_class.return_value = mock_observer
@@ -101,7 +107,13 @@ def test_dev_command_initial_build(
     """Test dev command performs initial build setup."""
     # Setup mocks
     mock_rebuild_frontend.return_value = "remoteEntry.abc123.js"
-    mock_build_manifest.return_value = Manifest(id="test", name="test", 
version="1.0.0")
+    mock_build_manifest.return_value = Manifest(
+        id="test-org.test-extension",
+        publisher="test-org",
+        name="test-extension",
+        displayName="Test Extension",
+        version="1.0.0",
+    )
 
     extension_setup_for_dev(isolated_filesystem)
 
@@ -178,8 +190,9 @@ def 
test_frontend_watcher_function_coverage(isolated_filesystem):
     """Test frontend watcher function for coverage."""
     # Create extension.json
     extension_json = {
-        "id": "test_extension",
-        "name": "Test Extension",
+        "publisher": "test-org",
+        "name": "test-extension",
+        "displayName": "Test Extension",
         "version": "1.0.0",
         "permissions": [],
     }
@@ -189,7 +202,13 @@ def 
test_frontend_watcher_function_coverage(isolated_filesystem):
     dist_dir = isolated_filesystem / "dist"
     dist_dir.mkdir()
 
-    mock_manifest = Manifest(id="test", name="test", version="1.0.0")
+    mock_manifest = Manifest(
+        id="test-org.test-extension",
+        publisher="test-org",
+        name="test-extension",
+        displayName="Test Extension",
+        version="1.0.0",
+    )
     with patch("superset_extensions_cli.cli.rebuild_frontend") as mock_rebuild:
         with patch("superset_extensions_cli.cli.build_manifest") as mock_build:
             with patch("superset_extensions_cli.cli.write_manifest") as 
mock_write:
diff --git a/superset-extensions-cli/tests/test_cli_init.py 
b/superset-extensions-cli/tests/test_cli_init.py
index 78e0d6888c..7d9d5f284b 100644
--- a/superset-extensions-cli/tests/test_cli_init.py
+++ b/superset-extensions-cli/tests/test_cli_init.py
@@ -43,16 +43,17 @@ def 
test_init_creates_extension_with_both_frontend_and_backend(
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
     assert (
-        "🎉 Extension Test Extension (ID: test-extension) initialized" in 
result.output
+        "🎉 Extension Test Extension (ID: test-org.test-extension) initialized"
+        in result.output
     )
 
     # Verify directory structure
-    extension_path = isolated_filesystem / "test-extension"
+    extension_path = isolated_filesystem / "test-org.test-extension"
     assert_directory_exists(extension_path, "main extension directory")
 
     expected_structure = create_test_extension_structure(
         isolated_filesystem,
-        "test-extension",
+        "test-org.test-extension",
         include_frontend=True,
         include_backend=True,
     )
@@ -73,7 +74,7 @@ def test_init_creates_extension_with_frontend_only(
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "test-extension"
+    extension_path = isolated_filesystem / "test-org.test-extension"
     assert_directory_exists(extension_path)
 
     # Should have frontend directory and package.json
@@ -96,7 +97,7 @@ def test_init_creates_extension_with_backend_only(
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "test-extension"
+    extension_path = isolated_filesystem / "test-org.test-extension"
     assert_directory_exists(extension_path)
 
     # Should have backend directory and pyproject.toml
@@ -119,7 +120,7 @@ def 
test_init_creates_extension_with_neither_frontend_nor_backend(
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "test-extension"
+    extension_path = isolated_filesystem / "test-org.test-extension"
     assert_directory_exists(extension_path)
 
     # Should only have extension.json
@@ -131,43 +132,45 @@ def 
test_init_creates_extension_with_neither_frontend_nor_backend(
 
 
 @pytest.mark.cli
-def test_init_accepts_any_display_name(cli_runner, isolated_filesystem):
-    """Test that init accepts any display name and generates proper ID."""
-    cli_input = "My Awesome Extension!\n\n0.1.0\nApache-2.0\ny\ny\n"
+def test_init_accepts_valid_display_name(cli_runner, isolated_filesystem):
+    """Test that init accepts valid display names and generates proper ID."""
+    cli_input = "My Awesome Extension\n\ntest-org\n0.1.0\nApache-2.0\ny\ny\n"
     result = cli_runner.invoke(app, ["init"], input=cli_input)
 
     assert result.exit_code == 0, f"Should accept display name: 
{result.output}"
-    assert Path("my-awesome-extension").exists(), (
-        "Directory for generated ID should be created"
+    assert Path("test-org.my-awesome-extension").exists(), (
+        "Directory for generated composite ID should be created"
     )
 
 
 @pytest.mark.cli
 def test_init_accepts_mixed_alphanumeric_name(cli_runner, isolated_filesystem):
     """Test that init accepts mixed alphanumeric display names."""
-    cli_input = "Tool 123\n\n0.1.0\nApache-2.0\ny\ny\n"
+    cli_input = "Tool 123\n\ntest-org\n0.1.0\nApache-2.0\ny\ny\n"
     result = cli_runner.invoke(app, ["init"], input=cli_input)
 
     assert result.exit_code == 0, (
         f"Mixed alphanumeric display name should be valid: {result.output}"
     )
-    assert Path("tool-123").exists(), "Directory for 'tool-123' should be 
created"
+    assert Path("test-org.tool-123").exists(), (
+        "Directory for 'test-org.tool-123' should be created"
+    )
 
 
 @pytest.mark.cli
 @pytest.mark.parametrize(
     "display_name,expected_id",
     [
-        ("Test Extension", "test-extension"),
-        ("My Tool v2", "my-tool-v2"),
-        ("Dashboard Helper", "dashboard-helper"),
-        ("Chart Builder Pro", "chart-builder-pro"),
+        ("Test Extension", "test-org.test-extension"),
+        ("My Tool v2", "test-org.my-tool-v2"),
+        ("Dashboard Helper", "test-org.dashboard-helper"),
+        ("Chart Builder Pro", "test-org.chart-builder-pro"),
     ],
 )
 def test_init_with_various_display_names(cli_runner, display_name, 
expected_id):
     """Test that init accepts various display names and generates proper 
IDs."""
     with cli_runner.isolated_filesystem():
-        cli_input = f"{display_name}\n\n0.1.0\nApache-2.0\ny\ny\n"
+        cli_input = f"{display_name}\n\ntest-org\n0.1.0\nApache-2.0\ny\ny\n"
         result = cli_runner.invoke(app, ["init"], input=cli_input)
 
         assert result.exit_code == 0, (
@@ -184,7 +187,7 @@ def test_init_fails_when_directory_already_exists(
 ):
     """Test that init fails gracefully when target directory already exists."""
     # Create the directory first
-    existing_dir = isolated_filesystem / "test-extension"
+    existing_dir = isolated_filesystem / "test-org.test-extension"
     existing_dir.mkdir()
 
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
@@ -201,15 +204,16 @@ def test_extension_json_content_is_correct(
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "test-extension"
+    extension_path = isolated_filesystem / "test-org.test-extension"
     extension_json_path = extension_path / "extension.json"
 
     # Verify the JSON structure and values
     assert_json_content(
         extension_json_path,
         {
-            "id": "test-extension",
-            "name": "Test Extension",
+            "publisher": "test-org",
+            "name": "test-extension",
+            "displayName": "Test Extension",
             "version": "0.1.0",
             "license": "Apache-2.0",
             "permissions": [],
@@ -224,10 +228,15 @@ def test_extension_json_content_is_correct(
     frontend = content["frontend"]
     assert "contributions" in frontend
     assert "moduleFederation" in frontend
-    assert frontend["contributions"] == {"commands": [], "views": {}, "menus": 
{}}
+    assert frontend["contributions"] == {
+        "commands": [],
+        "views": {},
+        "menus": {},
+        "editors": [],
+    }
     assert frontend["moduleFederation"] == {
         "exposes": ["./index"],
-        "name": "testExtension",
+        "name": "testOrg_testExtension",
     }
 
     # Verify backend section exists and has correct structure
@@ -235,9 +244,11 @@ def test_extension_json_content_is_correct(
     backend = content["backend"]
     assert "entryPoints" in backend
     assert "files" in backend
-    assert backend["entryPoints"] == 
["superset_extensions.test_extension.entrypoint"]
+    assert backend["entryPoints"] == [
+        "superset_extensions.test_org.test_extension.entrypoint"
+    ]
     assert backend["files"] == [
-        "backend/src/superset_extensions/test_extension/**/*.py"
+        "backend/src/superset_extensions/test_org/test_extension/**/*.py"
     ]
 
 
@@ -249,14 +260,14 @@ def test_frontend_package_json_content_is_correct(
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "test-extension"
+    extension_path = isolated_filesystem / "test-org.test-extension"
     package_json_path = extension_path / "frontend" / "package.json"
 
     # Verify the package.json structure and values
     assert_json_content(
         package_json_path,
         {
-            "name": "test-extension",
+            "name": "@test-org/test-extension",
             "version": "0.1.0",
             "license": "Apache-2.0",
         },
@@ -278,14 +289,16 @@ def test_backend_pyproject_toml_is_created(
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "test-extension"
+    extension_path = isolated_filesystem / "test-org.test-extension"
     pyproject_path = extension_path / "backend" / "pyproject.toml"
 
     assert_file_exists(pyproject_path, "backend pyproject.toml")
 
     # Basic content verification (without parsing TOML for now)
     content = pyproject_path.read_text()
-    assert "superset_extensions.test_extension" in content
+    assert (
+        "test_org-test_extension" in content
+    )  # Package name uses collision-safe naming
     assert "0.1.0" in content
     assert "Apache-2.0" in content
 
@@ -303,7 +316,9 @@ def test_init_command_output_messages(cli_runner, 
isolated_filesystem, cli_input
     assert "Created .gitignore" in output
     assert "Created frontend folder structure" in output
     assert "Created backend folder structure" in output
-    assert "Extension Test Extension (ID: test-extension) initialized" in 
output
+    assert (
+        "Extension Test Extension (ID: test-org.test-extension) initialized" 
in output
+    )
 
 
 @pytest.mark.cli
@@ -312,7 +327,7 @@ def test_gitignore_content_is_correct(cli_runner, 
isolated_filesystem, cli_input
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "test-extension"
+    extension_path = isolated_filesystem / "test-org.test-extension"
     gitignore_path = extension_path / ".gitignore"
 
     assert_file_exists(gitignore_path, ".gitignore")
@@ -332,19 +347,20 @@ def test_gitignore_content_is_correct(cli_runner, 
isolated_filesystem, cli_input
 @pytest.mark.cli
 def test_init_with_custom_version_and_license(cli_runner, isolated_filesystem):
     """Test init with custom version and license parameters."""
-    cli_input = "My Extension\n\n2.1.0\nMIT\ny\nn\n"
+    cli_input = "My Extension\n\ntest-org\n2.1.0\nMIT\ny\nn\n"
     result = cli_runner.invoke(app, ["init"], input=cli_input)
 
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "my-extension"
+    extension_path = isolated_filesystem / "test-org.my-extension"
     extension_json_path = extension_path / "extension.json"
 
     assert_json_content(
         extension_json_path,
         {
-            "id": "my-extension",
-            "name": "My Extension",
+            "publisher": "test-org",
+            "name": "my-extension",
+            "displayName": "My Extension",
             "version": "2.1.0",
             "license": "MIT",
         },
@@ -356,17 +372,17 @@ def test_init_with_custom_version_and_license(cli_runner, 
isolated_filesystem):
 def test_full_init_workflow_integration(cli_runner, isolated_filesystem):
     """Integration test for the complete init workflow."""
     # Test the complete flow with realistic user input
-    cli_input = "Awesome Charts\n\n1.0.0\nApache-2.0\ny\ny\n"
+    cli_input = "Awesome Charts\n\nawesome-org\n1.0.0\nApache-2.0\ny\ny\n"
     result = cli_runner.invoke(app, ["init"], input=cli_input)
 
     # Verify success
     assert result.exit_code == 0
 
     # Verify complete directory structure
-    extension_path = isolated_filesystem / "awesome-charts"
+    extension_path = isolated_filesystem / "awesome-org.awesome-charts"
     expected_structure = create_test_extension_structure(
         isolated_filesystem,
-        "awesome-charts",
+        "awesome-org.awesome-charts",
         include_frontend=True,
         include_backend=True,
     )
@@ -377,16 +393,19 @@ def test_full_init_workflow_integration(cli_runner, 
isolated_filesystem):
 
     # Verify all generated files have correct content
     extension_json = load_json_file(extension_path / "extension.json")
-    assert extension_json["id"] == "awesome-charts"
-    assert extension_json["name"] == "Awesome Charts"
+    assert extension_json["publisher"] == "awesome-org"
+    assert extension_json["name"] == "awesome-charts"
+    assert extension_json["displayName"] == "Awesome Charts"
     assert extension_json["version"] == "1.0.0"
     assert extension_json["license"] == "Apache-2.0"
 
     package_json = load_json_file(extension_path / "frontend" / "package.json")
-    assert package_json["name"] == "awesome-charts"
+    assert package_json["name"] == "@awesome-org/awesome-charts"
 
     pyproject_content = (extension_path / "backend" / 
"pyproject.toml").read_text()
-    assert "superset_extensions.awesome_charts" in pyproject_content
+    assert (
+        "awesome_org-awesome_charts" in pyproject_content
+    )  # Package name uses collision-safe naming
 
 
 # Non-interactive mode tests
@@ -397,9 +416,11 @@ def test_init_non_interactive_with_all_options(cli_runner, 
isolated_filesystem):
         app,
         [
             "init",
-            "--id",
-            "my-ext",
+            "--publisher",
+            "my-org",
             "--name",
+            "my-ext",
+            "--display-name",
             "My Extension",
             "--version",
             "1.0.0",
@@ -411,16 +432,17 @@ def 
test_init_non_interactive_with_all_options(cli_runner, isolated_filesystem):
     )
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
-    assert "🎉 Extension My Extension (ID: my-ext) initialized" in result.output
+    assert "🎉 Extension My Extension (ID: my-org.my-ext) initialized" in 
result.output
 
-    extension_path = isolated_filesystem / "my-ext"
+    extension_path = isolated_filesystem / "my-org.my-ext"
     assert_directory_exists(extension_path)
     assert_directory_exists(extension_path / "frontend")
     assert_directory_exists(extension_path / "backend")
 
     extension_json = load_json_file(extension_path / "extension.json")
-    assert extension_json["id"] == "my-ext"
-    assert extension_json["name"] == "My Extension"
+    assert extension_json["publisher"] == "my-org"
+    assert extension_json["name"] == "my-ext"
+    assert extension_json["displayName"] == "My Extension"
     assert extension_json["version"] == "1.0.0"
     assert extension_json["license"] == "MIT"
 
@@ -432,9 +454,11 @@ def test_init_frontend_only_with_cli_options(cli_runner, 
isolated_filesystem):
         app,
         [
             "init",
-            "--id",
-            "frontend-ext",
+            "--publisher",
+            "frontend-org",
             "--name",
+            "frontend-ext",
+            "--display-name",
             "Frontend Extension",
             "--version",
             "1.0.0",
@@ -447,7 +471,7 @@ def test_init_frontend_only_with_cli_options(cli_runner, 
isolated_filesystem):
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "frontend-ext"
+    extension_path = isolated_filesystem / "frontend-org.frontend-ext"
     assert_directory_exists(extension_path / "frontend")
     assert not (extension_path / "backend").exists()
 
@@ -459,9 +483,11 @@ def test_init_backend_only_with_cli_options(cli_runner, 
isolated_filesystem):
         app,
         [
             "init",
-            "--id",
-            "backend-ext",
+            "--publisher",
+            "backend-org",
             "--name",
+            "backend-ext",
+            "--display-name",
             "Backend Extension",
             "--version",
             "1.0.0",
@@ -474,7 +500,7 @@ def test_init_backend_only_with_cli_options(cli_runner, 
isolated_filesystem):
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "backend-ext"
+    extension_path = isolated_filesystem / "backend-org.backend-ext"
     assert not (extension_path / "frontend").exists()
     assert_directory_exists(extension_path / "backend")
 
@@ -482,14 +508,16 @@ def test_init_backend_only_with_cli_options(cli_runner, 
isolated_filesystem):
 @pytest.mark.cli
 def test_init_prompts_for_missing_options(cli_runner, isolated_filesystem):
     """Test that init prompts for options not provided via CLI and uses 
defaults."""
-    # Provide id and name via CLI, but version/license will be prompted 
(accept defaults)
+    # Provide publisher, name, and display-name via CLI, but version/license 
will be prompted (accept defaults)
     result = cli_runner.invoke(
         app,
         [
             "init",
-            "--id",
-            "default-ext",
+            "--publisher",
+            "default-org",
             "--name",
+            "default-ext",
+            "--display-name",
             "Default Extension",
             "--frontend",
             "--backend",
@@ -499,22 +527,24 @@ def test_init_prompts_for_missing_options(cli_runner, 
isolated_filesystem):
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "default-ext"
+    extension_path = isolated_filesystem / "default-org.default-ext"
     extension_json = load_json_file(extension_path / "extension.json")
     assert extension_json["version"] == "0.1.0"
     assert extension_json["license"] == "Apache-2.0"
 
 
 @pytest.mark.cli
-def test_init_non_interactive_validates_id(cli_runner, isolated_filesystem):
-    """Test that non-interactive mode validates extension ID."""
+def test_init_non_interactive_validates_technical_name(cli_runner, 
isolated_filesystem):
+    """Test that non-interactive mode validates technical name."""
     result = cli_runner.invoke(
         app,
         [
             "init",
-            "--id",
-            "invalid_name",
+            "--publisher",
+            "test-org",
             "--name",
+            "invalid_name",
+            "--display-name",
             "Invalid Extension",
             "--frontend",
             "--backend",
@@ -522,4 +552,4 @@ def test_init_non_interactive_validates_id(cli_runner, 
isolated_filesystem):
     )
 
     assert result.exit_code == 1
-    assert "Use lowercase letters, numbers, and hyphens only" in result.output
+    assert "must start with a letter" in result.output.lower()
diff --git a/superset-extensions-cli/tests/test_name_transformations.py 
b/superset-extensions-cli/tests/test_name_transformations.py
index d376c04e17..caa490c28d 100644
--- a/superset-extensions-cli/tests/test_name_transformations.py
+++ b/superset-extensions-cli/tests/test_name_transformations.py
@@ -20,381 +20,483 @@ import pytest
 from superset_extensions_cli.exceptions import ExtensionNameError
 from superset_extensions_cli.utils import (
     generate_extension_names,
+    get_module_federation_name,
     kebab_to_camel_case,
     kebab_to_snake_case,
     name_to_kebab_case,
-    to_snake_case,  # Keep this for backward compatibility testing only
-    validate_extension_name,
-    validate_extension_id,
+    suggest_technical_name,
+    validate_display_name,
     validate_npm_package_name,
+    validate_publisher,
     validate_python_package_name,
+    validate_technical_name,
 )
 
 
-class TestNameTransformations:
-    """Test name transformation functions."""
-
-    @pytest.mark.parametrize(
-        "display_name,expected",
-        [
-            ("Hello World", "hello-world"),
-            ("Data Explorer", "data-explorer"),
-            ("My Extension", "my-extension"),
-            ("hello-world", "hello-world"),  # Already normalized
-            ("Hello@World!", "helloworld"),  # Special chars removed
-            (
-                "Data_Explorer",
-                "data-explorer",
-            ),  # Underscores become spaces then hyphens
-            ("My   Extension", "my-extension"),  # Multiple spaces normalized
-            ("  Hello World  ", "hello-world"),  # Trimmed
-            ("API v2 Client", "api-v2-client"),  # Numbers preserved
-            ("Simple", "simple"),  # Single word
-        ],
-    )
-    def test_name_to_kebab_case(self, display_name, expected):
-        """Test direct kebab case conversion from display names."""
-        assert name_to_kebab_case(display_name) == expected
-
-    @pytest.mark.parametrize(
-        "kebab_name,expected",
-        [
-            ("hello-world", "helloWorld"),
-            ("data-explorer", "dataExplorer"),
-            ("my-extension", "myExtension"),
-            ("api-v2-client", "apiV2Client"),
-            ("simple", "simple"),  # Single word
-            ("chart-tool", "chartTool"),
-            ("dashboard-helper", "dashboardHelper"),
-        ],
-    )
-    def test_kebab_to_camel_case(self, kebab_name, expected):
-        """Test kebab-case to camelCase conversion."""
-        assert kebab_to_camel_case(kebab_name) == expected
-
-    @pytest.mark.parametrize(
-        "kebab_name,expected",
-        [
-            ("hello-world", "hello_world"),
-            ("data-explorer", "data_explorer"),
-            ("my-extension", "my_extension"),
-            ("api-v2-client", "api_v2_client"),
-            ("simple", "simple"),  # Single word
-            ("chart-tool", "chart_tool"),
-            ("dashboard-helper", "dashboard_helper"),
-        ],
-    )
-    def test_kebab_to_snake_case(self, kebab_name, expected):
-        """Test kebab-case to snake_case conversion."""
-        assert kebab_to_snake_case(kebab_name) == expected
-
-    # Backward compatibility test for remaining legacy function
-    @pytest.mark.parametrize(
-        "input_name,expected",
-        [
-            ("hello-world", "hello_world"),
-            ("data-explorer", "data_explorer"),
-            ("my-extension-name", "my_extension_name"),
-        ],
-    )
-    def test_to_snake_case_legacy(self, input_name, expected):
-        """Test legacy kebab-to-snake conversion function."""
-        assert to_snake_case(input_name) == expected
-
-
-class TestValidation:
-    """Test validation functions."""
-
-    @pytest.mark.parametrize(
-        "valid_display",
-        [
-            "Hello World",
-            "Data Explorer",
-            "My Extension",
-            "Simple",
-            "   Extra   Spaces   ",  # Gets normalized
-        ],
-    )
-    def test_validate_extension_name_valid(self, valid_display):
-        """Test valid display names."""
-        result = validate_extension_name(valid_display)
-        assert result  # Should return normalized name
-        assert "  " not in result  # No double spaces
-
-    @pytest.mark.parametrize(
-        "invalid_display,error_match",
-        [
-            ("", "cannot be empty"),
-            ("   ", "cannot be empty"),
-            ("@#$%", "must contain at least one letter or number"),
-        ],
-    )
-    def test_validate_extension_name_invalid(self, invalid_display, 
error_match):
-        """Test invalid extension names."""
-        with pytest.raises(ExtensionNameError, match=error_match):
-            validate_extension_name(invalid_display)
-
-    @pytest.mark.parametrize(
-        "valid_id",
-        [
-            "hello-world",
-            "data-explorer",
-            "myext",
-            "chart123",
-            "my-tool-v2",
-            "a",  # Single character
-            "extension-with-many-parts",
-        ],
-    )
-    def test_validate_extension_id_valid(self, valid_id):
-        """Test valid extension IDs."""
-        # Should not raise exceptions
-        validate_extension_id(valid_id)
-
-    @pytest.mark.parametrize(
-        "invalid_id,error_match",
-        [
-            ("", "cannot be empty"),
-            ("Hello-World", "Use lowercase"),
-            ("-hello", "cannot start with hyphens"),
-            ("hello-", "cannot end with hyphens"),
-            ("hello--world", "consecutive hyphens"),
-        ],
-    )
-    def test_validate_extension_id_invalid(self, invalid_id, error_match):
-        """Test invalid extension IDs."""
-        with pytest.raises(ExtensionNameError, match=error_match):
-            validate_extension_id(invalid_id)
-
-    @pytest.mark.parametrize(
-        "valid_package",
-        [
-            "hello_world",
-            "data_explorer",
-            "myext",
-            "test123",
-            "package_with_many_parts",
-        ],
-    )
-    def test_validate_python_package_name_valid(self, valid_package):
-        """Test valid Python package names."""
-        # Should not raise exceptions
-        validate_python_package_name(valid_package)
-
-    @pytest.mark.parametrize(
-        "keyword",
-        [
-            "class",
-            "import",
-            "def",
-            "return",
-            "if",
-            "else",
-            "for",
-            "while",
-            "try",
-            "except",
-            "finally",
-            "with",
-            "as",
-            "lambda",
-            "yield",
-            "False",
-            "None",
-            "True",
-        ],
-    )
-    def test_validate_python_package_name_keywords(self, keyword):
-        """Test that Python reserved keywords are rejected."""
-        with pytest.raises(
-            ExtensionNameError, match="Package name cannot start with Python 
keyword"
-        ):
-            validate_python_package_name(keyword)
-
-    @pytest.mark.parametrize(
-        "invalid_package",
-        [
-            "hello-world",  # Hyphens not allowed in Python identifiers
-        ],
-    )
-    def test_validate_python_package_name_invalid(self, invalid_package):
-        """Test invalid Python package names."""
-        with pytest.raises(ExtensionNameError, match="not a valid Python 
package"):
-            validate_python_package_name(invalid_package)
-
-    @pytest.mark.parametrize(
-        "valid_npm",
-        [
-            "hello-world",
+# Name transformation tests
+
+
[email protected](
+    ("display_name", "expected"),
+    [
+        ("Hello World", "hello-world"),
+        ("Data Explorer", "data-explorer"),
+        ("My Extension", "my-extension"),
+        ("hello-world", "hello-world"),  # Already normalized
+        ("Hello@World!", "helloworld"),  # Special chars removed
+        (
+            "Data_Explorer",
             "data-explorer",
-            "myext",
-            "package-with-many-parts",
-        ],
-    )
-    def test_validate_npm_package_name_valid(self, valid_npm):
-        """Test valid npm package names."""
-        # Should not raise exceptions
-        validate_npm_package_name(valid_npm)
-
-    @pytest.mark.parametrize(
-        "reserved_name",
-        ["node_modules", "npm", "yarn", "package.json", "localhost", 
"favicon.ico"],
-    )
-    def test_validate_npm_package_name_reserved(self, reserved_name):
-        """Test that npm reserved names are rejected."""
-        with pytest.raises(ExtensionNameError, match="reserved npm package 
name"):
-            validate_npm_package_name(reserved_name)
-
-
-class TestNameGeneration:
-    """Test complete name generation."""
-
-    @pytest.mark.parametrize(
-        "display_name,expected_kebab,expected_snake,expected_camel",
-        [
-            ("Hello World", "hello-world", "hello_world", "helloWorld"),
-            ("Data Explorer", "data-explorer", "data_explorer", 
"dataExplorer"),
-            ("My Extension v2", "my-extension-v2", "my_extension_v2", 
"myExtensionV2"),
-            ("Chart Tool", "chart-tool", "chart_tool", "chartTool"),
-            ("Simple", "simple", "simple", "simple"),
-            ("API v2 Client", "api-v2-client", "api_v2_client", "apiV2Client"),
-            (
-                "Dashboard Helper",
-                "dashboard-helper",
-                "dashboard_helper",
-                "dashboardHelper",
-            ),
-        ],
-    )
-    def test_generate_extension_names_complete_flow(
-        self, display_name, expected_kebab, expected_snake, expected_camel
+        ),  # Underscores become spaces then hyphens
+        ("My   Extension", "my-extension"),  # Multiple spaces normalized
+        ("  Hello World  ", "hello-world"),  # Trimmed
+        ("API v2 Client", "api-v2-client"),  # Numbers preserved
+        ("Simple", "simple"),  # Single word
+    ],
+)
+def test_name_to_kebab_case(display_name, expected):
+    """Test direct kebab case conversion from display names."""
+    assert name_to_kebab_case(display_name) == expected
+
+
[email protected](
+    ("kebab_name", "expected"),
+    [
+        ("hello-world", "helloWorld"),
+        ("data-explorer", "dataExplorer"),
+        ("my-extension", "myExtension"),
+        ("api-v2-client", "apiV2Client"),
+        ("simple", "simple"),  # Single word
+        ("chart-tool", "chartTool"),
+        ("dashboard-helper", "dashboardHelper"),
+    ],
+)
+def test_kebab_to_camel_case(kebab_name, expected):
+    """Test kebab-case to camelCase conversion."""
+    assert kebab_to_camel_case(kebab_name) == expected
+
+
[email protected](
+    ("kebab_name", "expected"),
+    [
+        ("hello-world", "hello_world"),
+        ("data-explorer", "data_explorer"),
+        ("my-extension", "my_extension"),
+        ("api-v2-client", "api_v2_client"),
+        ("simple", "simple"),  # Single word
+        ("chart-tool", "chart_tool"),
+        ("dashboard-helper", "dashboard_helper"),
+    ],
+)
+def test_kebab_to_snake_case(kebab_name, expected):
+    """Test kebab-case to snake_case conversion."""
+    assert kebab_to_snake_case(kebab_name) == expected
+
+
+# Display name validation tests
+
+
[email protected](
+    ("valid_display", "expected_normalized"),
+    [
+        ("Hello World", "Hello World"),
+        ("Data Explorer", "Data Explorer"),
+        ("My Extension", "My Extension"),
+        ("Simple", "Simple"),
+        ("   Extra   Spaces   ", "Extra Spaces"),  # Gets normalized
+        ("Dashboard Widgets", "Dashboard Widgets"),
+        ("Chart Builder Pro", "Chart Builder Pro"),
+        ("API Client v2.0", "API Client v2.0"),
+        ("Tool_123", "Tool_123"),  # Underscores allowed
+        ("My-Extension", "My-Extension"),  # Hyphens allowed
+    ],
+)
+def test_validate_display_name_valid(valid_display, expected_normalized):
+    """Test valid display names return correctly normalized output."""
+    result = validate_display_name(valid_display)
+    assert result == expected_normalized
+
+
[email protected](
+    ("invalid_display", "error_match"),
+    [
+        ("", "cannot be empty"),
+        ("   ", "cannot be empty"),
+        ("@#$%", "must start with a letter"),
+        ("123 Tool", "must start with a letter"),
+        ("-My Extension", "must start with a letter"),
+    ],
+)
+def test_validate_display_name_invalid(invalid_display, error_match):
+    """Test invalid display names."""
+    with pytest.raises(ExtensionNameError, match=error_match):
+        validate_display_name(invalid_display)
+
+
+# Python package name validation tests
+
+
[email protected](
+    ("valid_package",),
+    [
+        ("hello_world",),
+        ("data_explorer",),
+        ("myext",),
+        ("test123",),
+        ("package_with_many_parts",),
+    ],
+)
+def test_validate_python_package_name_valid(valid_package):
+    """Test valid Python package names."""
+    # Should not raise exceptions
+    validate_python_package_name(valid_package)
+
+
[email protected](
+    ("keyword",),
+    [
+        ("class",),
+        ("import",),
+        ("def",),
+        ("return",),
+        ("if",),
+        ("else",),
+        ("for",),
+        ("while",),
+        ("try",),
+        ("except",),
+        ("finally",),
+        ("with",),
+        ("as",),
+        ("lambda",),
+        ("yield",),
+        ("False",),
+        ("None",),
+        ("True",),
+    ],
+)
+def test_validate_python_package_name_keywords(keyword):
+    """Test that Python reserved keywords are rejected."""
+    with pytest.raises(
+        ExtensionNameError, match="Package name cannot start with Python 
keyword"
     ):
-        """Test complete name generation flow from display name to all 
variants."""
-        names = generate_extension_names(display_name)
-
-        # Test all transformations from single source
-        assert names["name"] == display_name
-        assert names["id"] == expected_kebab  # Extension ID (kebab-case)
-        assert names["mf_name"] == expected_camel  # Module Federation 
(camelCase)
-        assert names["backend_name"] == expected_snake  # Python package 
(snake_case)
-        assert names["backend_package"] == 
f"superset_extensions.{expected_snake}"
-        assert (
-            names["backend_entry"] == 
f"superset_extensions.{expected_snake}.entrypoint"
-        )
-
-    @pytest.mark.parametrize(
-        "invalid_display",
-        [
-            "Class Helper",  # Would create 'class_helper' - reserved keyword
-            "Import Tool",  # Would create 'import_tool' - reserved keyword
-            "@#$%",  # All special chars - becomes empty
-            "123 Tool",  # Starts with number after kebab conversion
-        ],
+        validate_python_package_name(keyword)
+
+
[email protected](
+    ("invalid_package",),
+    [
+        ("hello-world",),  # Hyphens not allowed in Python identifiers
+    ],
+)
+def test_validate_python_package_name_invalid(invalid_package):
+    """Test invalid Python package names."""
+    with pytest.raises(ExtensionNameError, match="not a valid Python package"):
+        validate_python_package_name(invalid_package)
+
+
+# NPM package validation tests
+
+
[email protected](
+    ("valid_npm",),
+    [
+        ("hello-world",),
+        ("data-explorer",),
+        ("myext",),
+        ("package-with-many-parts",),
+    ],
+)
+def test_validate_npm_package_name_valid(valid_npm):
+    """Test valid npm package names."""
+    # Should not raise exceptions
+    validate_npm_package_name(valid_npm)
+
+
[email protected](
+    ("reserved_name",),
+    [
+        ("node_modules",),
+        ("npm",),
+        ("yarn",),
+        ("package.json",),
+        ("localhost",),
+        ("favicon.ico",),
+    ],
+)
+def test_validate_npm_package_name_reserved(reserved_name):
+    """Test that npm reserved names are rejected."""
+    with pytest.raises(ExtensionNameError, match="reserved npm package name"):
+        validate_npm_package_name(reserved_name)
+
+
+# Publisher validation tests
+
+
[email protected](
+    ("valid_publisher",),
+    [
+        ("my-org",),
+        ("acme",),
+        ("apache-superset",),
+        ("test123",),
+        ("a",),  # Single character
+        ("publisher-with-many-parts",),
+    ],
+)
+def test_validate_publisher_valid(valid_publisher):
+    """Test valid publisher namespaces."""
+    # Should not raise exceptions
+    validate_publisher(valid_publisher)
+
+
[email protected](
+    ("invalid_publisher", "error_match"),
+    [
+        ("", "cannot be empty"),
+        ("My-Org", "must start with a letter and contain only lowercase 
letters"),
+        ("-publisher", "must start with a letter and contain only lowercase 
letters"),
+        ("publisher-", "must start with a letter and contain only lowercase 
letters"),
+        ("pub--lisher", "must start with a letter and contain only lowercase 
letters"),
+    ],
+)
+def test_validate_publisher_invalid(invalid_publisher, error_match):
+    """Test invalid publisher namespaces."""
+    with pytest.raises(ExtensionNameError, match=error_match):
+        validate_publisher(invalid_publisher)
+
+
+# Technical name validation tests
+
+
[email protected](
+    ("valid_name",),
+    [
+        ("dashboard-widgets",),
+        ("chart-builder",),
+        ("simple",),
+        ("api-client-v2",),
+        ("tool123",),
+    ],
+)
+def test_validate_technical_name_valid(valid_name):
+    """Test valid technical names."""
+    # Should not raise exceptions
+    validate_technical_name(valid_name)
+
+
[email protected](
+    ("invalid_name", "error_match"),
+    [
+        ("", "cannot be empty"),
+        (
+            "Dashboard-Widgets",
+            "must start with a letter and contain only lowercase letters",
+        ),
+        ("-name", "must start with a letter and contain only lowercase 
letters"),
+        ("name-", "must start with a letter and contain only lowercase 
letters"),
+        ("na--me", "must start with a letter and contain only lowercase 
letters"),
+    ],
+)
+def test_validate_technical_name_invalid(invalid_name, error_match):
+    """Test invalid technical names."""
+    with pytest.raises(ExtensionNameError, match=error_match):
+        validate_technical_name(invalid_name)
+
+
+# Name suggestion tests
+
+
[email protected](
+    ("display_name", "expected_technical"),
+    [
+        ("Dashboard Widgets", "dashboard-widgets"),
+        ("Chart Builder Pro!", "chart-builder-pro"),
+        ("My@Tool#123", "mytool123"),
+        ("  Spaced  Out  ", "spaced-out"),
+        ("API v2 Client", "api-v2-client"),
+    ],
+)
+def test_suggest_technical_name(display_name, expected_technical):
+    """Test technical name suggestion from display names."""
+    result = suggest_technical_name(display_name)
+    assert result == expected_technical
+
+
[email protected](
+    ("publisher", "name", "expected_mf"),
+    [
+        ("my-org", "dashboard-widgets", "myOrg_dashboardWidgets"),
+        ("acme", "chart-builder", "acme_chartBuilder"),
+        ("test-company", "simple", "testCompany_simple"),
+    ],
+)
+def test_get_module_federation_name(publisher, name, expected_mf):
+    """Test Module Federation name generation."""
+    result = get_module_federation_name(publisher, name)
+    assert result == expected_mf
+
+
+# Complete name generation tests
+
+
[email protected](
+    ("display_name", "expected_kebab", "expected_snake", "expected_camel"),
+    [
+        ("Hello World", "hello-world", "hello_world", "helloWorld"),
+        ("Data Explorer", "data-explorer", "data_explorer", "dataExplorer"),
+        ("My Extension v2", "my-extension-v2", "my_extension_v2", 
"myExtensionV2"),
+        ("Chart Tool", "chart-tool", "chart_tool", "chartTool"),
+        ("Simple", "simple", "simple", "simple"),
+        ("API v2 Client", "api-v2-client", "api_v2_client", "apiV2Client"),
+        (
+            "Dashboard Helper",
+            "dashboard-helper",
+            "dashboard_helper",
+            "dashboardHelper",
+        ),
+    ],
+)
+def test_generate_extension_names_complete_flow(
+    display_name, expected_kebab, expected_snake, expected_camel
+):
+    """Test complete name generation flow with publisher concept."""
+    publisher = "test-org"
+    names = generate_extension_names(display_name, publisher, expected_kebab)
+
+    # Test all transformations with publisher concept
+    assert names["display_name"] == display_name
+    assert names["publisher"] == publisher
+    assert names["name"] == expected_kebab  # Technical name
+    assert names["id"] == f"{publisher}.{expected_kebab}"  # Composite ID
+    assert names["npm_name"] == f"@{publisher}/{expected_kebab}"  # NPM scoped
+    assert (
+        names["mf_name"] == f"testOrg_{expected_camel}"
+    )  # Module Federation with publisher prefix
+    assert (
+        names["backend_package"] == f"{publisher.replace('-', 
'_')}-{expected_snake}"
+    )  # Collision-safe
+    assert (
+        names["backend_path"]
+        == f"superset_extensions.{publisher.replace('-', 
'_')}.{expected_snake}"
     )
-    def test_generate_extension_names_invalid(self, invalid_display):
-        """Test invalid name generation scenarios."""
-        with pytest.raises(ExtensionNameError):
-            generate_extension_names(invalid_display)
-
-    def test_generate_extension_names_unicode(self):
-        """Test handling of unicode characters."""
-        names = generate_extension_names("Café Extension")
-        assert "é" not in names["id"]
-        assert names["id"] == "caf-extension"
-        assert names["name"] == "Café Extension"  # Original preserved
-
-    def test_generate_extension_names_special_chars(self):
-        """Test name generation with special characters."""
-        names = generate_extension_names("My@Extension!")
-
-        assert names["name"] == "My@Extension!"
-        assert names["id"] == "myextension"
-        assert names["backend_name"] == "myextension"
-
-    def test_generate_extension_names_case_preservation(self):
-        """Test that display name case is preserved."""
-        names = generate_extension_names("CamelCase Extension")
-        assert names["name"] == "CamelCase Extension"
-        assert names["id"] == "camelcase-extension"
-
-
-class TestEdgeCases:
-    """Test edge cases and boundary conditions."""
-
-    @pytest.mark.parametrize(
-        "edge_case",
-        [
-            "",  # Empty string
-            "   ",  # Only spaces
-            "---",  # Only hyphens
-            "___",  # Only underscores
-        ],
+    assert (
+        names["backend_entry"]
+        == f"superset_extensions.{publisher.replace('-', 
'_')}.{expected_snake}.entrypoint"
     )
-    def test_empty_or_invalid_inputs(self, edge_case):
-        """Test inputs that become empty or invalid after processing."""
-        with pytest.raises(ExtensionNameError):
-            generate_extension_names(edge_case)
-
-    def test_minimal_valid_input(self):
-        """Test minimal valid input."""
-        names = generate_extension_names("A Extension")
-        assert names["id"] == "a-extension"
-        assert names["backend_name"] == "a_extension"
-
-    def test_numbers_handling(self):
-        """Test handling of numbers in names."""
-        names = generate_extension_names("Tool 123 v2")
-        assert names["id"] == "tool-123-v2"
-        assert names["backend_name"] == "tool_123_v2"
-
-    def test_id_based_name_generation(self):
-        """Test that technical names are derived from ID, not display name."""
-        # Simulate manual ExtensionNames construction with custom ID
-        display_name = "My Awesome Chart Builder Pro"
-        extension_id = "chart-builder"  # Much shorter than display name
-
-        # Create names using ID-based generation (new behavior)
-        from superset_extensions_cli.types import ExtensionNames
-
-        names = ExtensionNames(
-            name=display_name,
-            id=extension_id,
-            mf_name=kebab_to_camel_case(extension_id),  # From ID: 
"chartBuilder"
-            backend_name=kebab_to_snake_case(extension_id),  # From ID: 
"chart_builder"
-            
backend_package=f"superset_extensions.{kebab_to_snake_case(extension_id)}",
-            
backend_entry=f"superset_extensions.{kebab_to_snake_case(extension_id)}.entrypoint",
-        )
-
-        # Verify technical names come from ID, not display name
-        assert names["name"] == "My Awesome Chart Builder Pro"  # Display name 
preserved
-        assert names["id"] == "chart-builder"  # Extension ID
-        assert (
-            names["mf_name"] == "chartBuilder"
-        )  # From ID, not "myAwesomeChartBuilderPro"
-        assert (
-            names["backend_name"] == "chart_builder"
-        )  # From ID, not "my_awesome_chart_builder_pro"
-        assert names["backend_package"] == "superset_extensions.chart_builder"
-        assert names["backend_entry"] == 
"superset_extensions.chart_builder.entrypoint"
-
-    def test_generate_names_uses_id_based_technical_names(self):
-        """Test that generate_extension_names uses ID-based generation for 
technical names."""
-        display_name = "Hello World"
-
-        # Generated names should use ID-based technical name generation
-        names = generate_extension_names(display_name)
-
-        # Verify the ID was generated from display name
-        assert names["id"] == "hello-world"
-
-        # Verify technical names were generated from the ID, not original 
display name
-        assert names["mf_name"] == kebab_to_camel_case("hello-world")  # 
"helloWorld"
-        assert names["backend_name"] == kebab_to_snake_case(
-            "hello-world"
-        )  # "hello_world"
-
-        # For this simple case, the results are the same as before, but the 
path is different:
-        # Old path: Display Name -> camelCase directly
-        # New path: Display Name -> ID -> camelCase from ID
-        assert names["mf_name"] == "helloWorld"
-        assert names["backend_name"] == "hello_world"
+
+
[email protected](
+    ("invalid_display",),
+    [
+        ("Class Helper",),  # Would create 'class_helper' - reserved keyword
+        ("Import Tool",),  # Would create 'import_tool' - reserved keyword
+        ("@#$%",),  # All special chars - becomes empty
+        ("123 Tool",),  # Starts with number after kebab conversion
+    ],
+)
+def test_generate_extension_names_invalid(invalid_display):
+    """Test invalid name generation scenarios."""
+    with pytest.raises(ExtensionNameError):
+        generate_extension_names(invalid_display, "test-org")
+
+
+def test_generate_extension_names_unicode():
+    """Test handling of unicode characters."""
+    # Use a simpler approach - the display name validation now requires 
starting with letter
+    names = generate_extension_names("Cafe Extension", "test-org", 
"cafe-extension")
+    assert names["id"] == "test-org.cafe-extension"
+    assert names["display_name"] == "Cafe Extension"  # Original preserved
+
+
+def test_generate_extension_names_special_chars():
+    """Test name generation with special characters."""
+    # Use manual technical name since display validation is stricter
+    names = generate_extension_names("My Extension", "test-org", 
"my-extension")
+
+    assert names["display_name"] == "My Extension"
+    assert names["id"] == "test-org.my-extension"
+    assert names["backend_package"] == "test_org-my_extension"
+
+
+def test_generate_extension_names_case_preservation():
+    """Test that display name case is preserved."""
+    names = generate_extension_names("CamelCase Extension", "test-org")
+    assert names["display_name"] == "CamelCase Extension"
+    assert names["id"] == "test-org.camelcase-extension"
+
+
+# Edge case tests
+
+
[email protected](
+    ("edge_case",),
+    [
+        ("",),  # Empty string
+        ("   ",),  # Only spaces
+        ("---",),  # Only hyphens
+        ("___",),  # Only underscores
+    ],
+)
+def test_empty_or_invalid_inputs(edge_case):
+    """Test inputs that become empty or invalid after processing."""
+    with pytest.raises(ExtensionNameError):
+        generate_extension_names(edge_case, "test-org")
+
+
+def test_minimal_valid_input():
+    """Test minimal valid input."""
+    names = generate_extension_names("A Extension", "test-org")
+    assert names["id"] == "test-org.a-extension"
+    assert names["backend_package"] == "test_org-a_extension"
+
+
+def test_numbers_handling():
+    """Test handling of numbers in names."""
+    names = generate_extension_names("Tool 123 v2", "test-org")
+    assert names["id"] == "test-org.tool-123-v2"
+    assert names["backend_package"] == "test_org-tool_123_v2"
+
+
+def test_manual_technical_name_override():
+    """Test using manual technical name instead of auto-generated."""
+    display_name = "My Awesome Chart Builder Pro"
+    publisher = "acme"
+    technical_name = "chart-builder"  # Much shorter than display name
+
+    # Create names using manual technical name
+    names = generate_extension_names(display_name, publisher, technical_name)
+
+    # Verify technical names come from provided technical name, not display 
name
+    assert (
+        names["display_name"] == "My Awesome Chart Builder Pro"
+    )  # Display name preserved
+    assert names["publisher"] == "acme"
+    assert names["name"] == "chart-builder"  # Technical name used
+    assert names["id"] == "acme.chart-builder"  # Composite ID
+    assert names["mf_name"] == "acme_chartBuilder"  # Module Federation format
+    assert names["backend_package"] == "acme-chart_builder"  # Collision-safe
+    assert names["backend_path"] == "superset_extensions.acme.chart_builder"
+    assert names["backend_entry"] == 
"superset_extensions.acme.chart_builder.entrypoint"
+
+
+def test_generate_names_uses_suggested_technical_names():
+    """Test that generate_extension_names can auto-suggest technical names."""
+    display_name = "Hello World"
+    publisher = "test-org"
+
+    # Generated names should use suggested technical name generation
+    names = generate_extension_names(display_name, publisher)
+
+    # Verify the technical name was suggested from display name
+    assert names["name"] == "hello-world"
+    assert names["id"] == "test-org.hello-world"
+
+    # Verify other names were generated from the technical name and publisher
+    assert names["mf_name"] == get_module_federation_name(
+        "test-org", "hello-world"
+    )  # "testOrg_helloWorld"
+    assert names["backend_package"] == "test_org-hello_world"
+
+    # Module Federation name should use underscore format with camelCase
+    assert names["mf_name"] == "testOrg_helloWorld"
diff --git a/superset-extensions-cli/tests/test_templates.py 
b/superset-extensions-cli/tests/test_templates.py
index 30d2115be7..29858e494a 100644
--- a/superset-extensions-cli/tests/test_templates.py
+++ b/superset-extensions-cli/tests/test_templates.py
@@ -42,12 +42,15 @@ def jinja_env(templates_dir):
 def template_context():
     """Default template context for testing."""
     return {
-        "name": "Test Extension",
-        "id": "test-extension",
-        "mf_name": "testExtension",
-        "backend_name": "test_extension",
-        "backend_package": "superset_extensions.test_extension",
-        "backend_entry": "superset_extensions.test_extension.entrypoint",
+        "publisher": "test-org",
+        "name": "test-extension",
+        "display_name": "Test Extension",
+        "id": "test-org.test-extension",
+        "npm_name": "@test-org/test-extension",
+        "mf_name": "testOrg_testExtension",
+        "backend_package": "test_org-test_extension",
+        "backend_path": "superset_extensions.test_org.test_extension",
+        "backend_entry": 
"superset_extensions.test_org.test_extension.entrypoint",
         "version": "0.1.0",
         "license": "Apache-2.0",
         "include_frontend": True,
@@ -68,8 +71,9 @@ def 
test_extension_json_template_renders_with_both_frontend_and_backend(
     parsed = json.loads(rendered)
 
     # Verify basic fields
-    assert parsed["id"] == "test-extension"
-    assert parsed["name"] == "Test Extension"
+    assert parsed["publisher"] == "test-org"
+    assert parsed["name"] == "test-extension"
+    assert parsed["displayName"] == "Test Extension"
     assert parsed["version"] == "0.1.0"
     assert parsed["license"] == "Apache-2.0"
     assert parsed["permissions"] == []
@@ -79,18 +83,25 @@ def 
test_extension_json_template_renders_with_both_frontend_and_backend(
     frontend = parsed["frontend"]
     assert "contributions" in frontend
     assert "moduleFederation" in frontend
-    assert frontend["contributions"] == {"commands": [], "views": {}, "menus": 
{}}
+    assert frontend["contributions"] == {
+        "commands": [],
+        "views": {},
+        "menus": {},
+        "editors": [],
+    }
     assert frontend["moduleFederation"] == {
         "exposes": ["./index"],
-        "name": "testExtension",
+        "name": "testOrg_testExtension",
     }
 
     # Verify backend section exists
     assert "backend" in parsed
     backend = parsed["backend"]
-    assert backend["entryPoints"] == 
["superset_extensions.test_extension.entrypoint"]
+    assert backend["entryPoints"] == [
+        "superset_extensions.test_org.test_extension.entrypoint"
+    ]
     assert backend["files"] == [
-        "backend/src/superset_extensions/test_extension/**/*.py"
+        "backend/src/superset_extensions/test_org/test_extension/**/*.py"
     ]
 
 
@@ -136,7 +147,7 @@ def 
test_frontend_package_json_template_renders_correctly(jinja_env, template_co
     parsed = json.loads(rendered)
 
     # Verify basic package info
-    assert parsed["name"] == "test-extension"
+    assert parsed["name"] == "@test-org/test-extension"
     assert parsed["version"] == "0.1.0"
     assert parsed["license"] == "Apache-2.0"
     assert parsed["private"] is True
@@ -170,7 +181,7 @@ def 
test_backend_pyproject_toml_template_renders_correctly(jinja_env, template_c
     rendered = template.render(template_context)
 
     # Basic content verification (without full TOML parsing)
-    assert "test_extension" in rendered
+    assert "test_org-test_extension" in rendered
     assert "0.1.0" in rendered
     assert "Apache-2.0" in rendered
 
@@ -178,34 +189,36 @@ def 
test_backend_pyproject_toml_template_renders_correctly(jinja_env, template_c
 # Template Rendering with Different Parameters Tests
 @pytest.mark.unit
 @pytest.mark.parametrize(
-    "extension_id,name,backend_name",
+    "publisher,technical_name,display_name",
     [
-        ("simple-extension", "Simple Extension", "simple_extension"),
-        ("my-extension-123", "My Extension 123", "my_extension_123"),
-        (
-            "complex-extension-name-123",
-            "Complex Extension Name 123",
-            "complex_extension_name_123",
-        ),
-        ("ext", "Ext", "ext"),
+        ("test-org", "simple-extension", "Simple Extension"),
+        ("acme", "my-extension-123", "My Extension 123"),
+        ("company", "complex-extension-name-123", "Complex Extension Name 
123"),
+        ("pub", "ext", "Ext"),
     ],
 )
 def test_template_rendering_with_different_ids(
-    jinja_env, extension_id, name, backend_name
+    jinja_env, publisher, technical_name, display_name
 ):
-    """Test templates render correctly with various extension ids/names."""
-    # Generate camelCase name for webpack from extension ID (new ID-based 
approach)
-    from superset_extensions_cli.utils import kebab_to_camel_case
+    """Test templates render correctly with various publisher/name 
combinations."""
+    from superset_extensions_cli.utils import (
+        get_module_federation_name,
+        kebab_to_snake_case,
+    )
 
-    mf_name = kebab_to_camel_case(extension_id)
+    publisher_snake = kebab_to_snake_case(publisher)
+    name_snake = kebab_to_snake_case(technical_name)
 
     context = {
-        "id": extension_id,
-        "name": name,
-        "mf_name": mf_name,
-        "backend_name": backend_name,
-        "backend_package": f"superset_extensions.{backend_name}",
-        "backend_entry": f"superset_extensions.{backend_name}.entrypoint",
+        "publisher": publisher,
+        "name": technical_name,
+        "display_name": display_name,
+        "id": f"{publisher}.{technical_name}",
+        "npm_name": f"@{publisher}/{technical_name}",
+        "mf_name": get_module_federation_name(publisher, technical_name),
+        "backend_package": f"{publisher_snake}-{name_snake}",
+        "backend_path": f"superset_extensions.{publisher_snake}.{name_snake}",
+        "backend_entry": 
f"superset_extensions.{publisher_snake}.{name_snake}.entrypoint",
         "version": "1.0.0",
         "license": "MIT",
         "include_frontend": True,
@@ -217,13 +230,14 @@ def test_template_rendering_with_different_ids(
     rendered = template.render(context)
     parsed = json.loads(rendered)
 
-    assert parsed["id"] == extension_id
-    assert parsed["name"] == name
+    assert parsed["publisher"] == publisher
+    assert parsed["name"] == technical_name
+    assert parsed["displayName"] == display_name
     assert parsed["backend"]["entryPoints"] == [
-        f"superset_extensions.{backend_name}.entrypoint"
+        f"superset_extensions.{publisher_snake}.{name_snake}.entrypoint"
     ]
     assert parsed["backend"]["files"] == [
-        f"backend/src/superset_extensions/{backend_name}/**/*.py"
+        
f"backend/src/superset_extensions/{publisher_snake}/{name_snake}/**/*.py"
     ]
 
     # Test package.json template
@@ -231,13 +245,13 @@ def test_template_rendering_with_different_ids(
     rendered = template.render(context)
     parsed = json.loads(rendered)
 
-    assert parsed["name"] == extension_id
+    assert parsed["name"] == f"@{publisher}/{technical_name}"
 
     # Test pyproject.toml template
     template = jinja_env.get_template("backend/pyproject.toml.j2")
     rendered = template.render(context)
 
-    assert f"superset_extensions.{backend_name}" in rendered
+    assert f"{publisher_snake}-{name_snake}" in rendered
 
 
 @pytest.mark.unit
@@ -245,9 +259,12 @@ def test_template_rendering_with_different_ids(
 def test_template_rendering_with_different_versions(jinja_env, version):
     """Test templates render correctly with various version formats."""
     context = {
-        "id": "test_ext",
-        "name": "Test Extension",
-        "mf_name": "testExtension",
+        "publisher": "test-pub",
+        "name": "test-ext",
+        "display_name": "Test Extension",
+        "id": "test-pub.test-ext",
+        "npm_name": "@test-pub/test-ext",
+        "mf_name": "testPub_testExt",
         "version": version,
         "license": "Apache-2.0",
         "include_frontend": True,
@@ -275,12 +292,15 @@ def 
test_template_rendering_with_different_versions(jinja_env, version):
 def test_template_rendering_with_different_licenses(jinja_env, license_type):
     """Test templates render correctly with various license types."""
     context = {
-        "id": "test_ext",
-        "name": "Test Extension",
-        "mf_name": "testExtension",
-        "backend_name": "test_ext",
-        "backend_package": "superset_extensions.test_ext",
-        "backend_entry": "superset_extensions.test_ext.entrypoint",
+        "publisher": "test-pub",
+        "name": "test-ext",
+        "display_name": "Test Extension",
+        "id": "test-pub.test-ext",
+        "npm_name": "@test-pub/test-ext",
+        "mf_name": "testPub_testExt",
+        "backend_package": "test_pub-test_ext",
+        "backend_path": "superset_extensions.test_pub.test_ext",
+        "backend_entry": "superset_extensions.test_pub.test_ext.entrypoint",
         "version": "1.0.0",
         "license": license_type,
         "include_frontend": True,
@@ -345,12 +365,15 @@ def test_template_context_edge_cases(jinja_env):
     """Test template rendering with edge case contexts."""
     # Test with minimal context
     minimal_context = {
-        "id": "minimal",
-        "name": "Minimal",
-        "mf_name": "minimal",
-        "backend_name": "minimal",
-        "backend_package": "superset_extensions.minimal",
-        "backend_entry": "superset_extensions.minimal.entrypoint",
+        "publisher": "min",
+        "name": "minimal",
+        "display_name": "Minimal",
+        "id": "min.minimal",
+        "npm_name": "@min/minimal",
+        "mf_name": "min_minimal",
+        "backend_package": "min-minimal",
+        "backend_path": "superset_extensions.min.minimal",
+        "backend_entry": "superset_extensions.min.minimal.entrypoint",
         "version": "1.0.0",
         "license": "MIT",
         "include_frontend": False,
@@ -362,7 +385,8 @@ def test_template_context_edge_cases(jinja_env):
     parsed = json.loads(rendered)
 
     # Should still be valid JSON with basic fields
-    assert parsed["id"] == "minimal"
-    assert parsed["name"] == "Minimal"
+    assert parsed["publisher"] == "min"
+    assert parsed["name"] == "minimal"
+    assert parsed["displayName"] == "Minimal"
     assert "frontend" not in parsed
     assert "backend" not in parsed
diff --git a/superset/extensions/api.py b/superset/extensions/api.py
index 2368dce11b..fc386d60bf 100644
--- a/superset/extensions/api.py
+++ b/superset/extensions/api.py
@@ -117,17 +117,21 @@ class ExtensionsRestApi(BaseApi):
 
     @protect()
     @safe
-    @expose("/<id>", methods=("GET",))
-    def get(self, id: str, **kwargs: Any) -> Response:
-        """Get an extension by its id.
+    @expose("/<publisher>/<name>", methods=("GET",))
+    def get(self, publisher: str, name: str, **kwargs: Any) -> Response:
+        """Get an extension by its publisher and name.
         ---
         get:
-          summary: Get an extension by its id.
+          summary: Get an extension by its publisher and name.
           parameters:
           - in: path
             schema:
               type: string
-            name: id
+            name: publisher
+          - in: path
+            schema:
+              type: string
+            name: name
           responses:
             200:
               description: Extension details
@@ -154,8 +158,10 @@ class ExtensionsRestApi(BaseApi):
             500:
               $ref: '#/components/responses/500'
         """
+        # Reconstruct composite ID from publisher and name
+        composite_id = f"{publisher}.{name}"
         extensions = get_extensions()
-        extension = extensions.get(id)
+        extension = extensions.get(composite_id)
         if not extension:
             return self.response_404()
         extension_data = build_extension_data(extension)
@@ -163,8 +169,8 @@ class ExtensionsRestApi(BaseApi):
 
     @protect()
     @safe
-    @expose("/<id>/<file>", methods=("GET",))
-    def content(self, id: str, file: str) -> Response:
+    @expose("/<publisher>/<name>/<file>", methods=("GET",))
+    def content(self, publisher: str, name: str, file: str) -> Response:
         """Get a frontend chunk of an extension.
         ---
         get:
@@ -173,8 +179,13 @@ class ExtensionsRestApi(BaseApi):
           - in: path
             schema:
               type: string
-            name: id
-            description: id of the extension
+            name: publisher
+            description: publisher of the extension
+          - in: path
+            schema:
+              type: string
+            name: name
+            description: technical name of the extension
           - in: path
             schema:
               type: string
@@ -199,8 +210,10 @@ class ExtensionsRestApi(BaseApi):
             500:
               $ref: '#/components/responses/500'
         """
+        # Reconstruct composite ID from publisher and name
+        composite_id = f"{publisher}.{name}"
         extensions = get_extensions()
-        extension = extensions.get(id)
+        extension = extensions.get(composite_id)
         if not extension:
             return self.response_404()
 
diff --git a/superset/extensions/utils.py b/superset/extensions/utils.py
index 6449adae8e..7927009e83 100644
--- a/superset/extensions/utils.py
+++ b/superset/extensions/utils.py
@@ -94,8 +94,10 @@ class InMemoryFinder(importlib.abc.MetaPathFinder):
         for ns_name in namespace_packages:
             # Create a virtual __init__.py path for the namespace package
             if is_virtual_path:
-                ns_path = f"{source_base_path}/backend/src/"
-                f"{ns_name.replace('.', '/')}/__init__.py"
+                ns_path = (
+                    f"{source_base_path}/backend/src/"
+                    f"{ns_name.replace('.', '/')}/__init__.py"
+                )
             else:
                 ns_path = str(
                     Path(source_base_path)
@@ -240,7 +242,10 @@ def build_extension_data(extension: LoadedExtension) -> 
dict[str, Any]:
     if manifest.frontend:
         frontend = manifest.frontend
         module_federation = frontend.moduleFederation
-        remote_entry_url = 
f"/api/v1/extensions/{manifest.id}/{frontend.remoteEntry}"
+        remote_entry_url = (
+            f"/api/v1/extensions/{manifest.publisher}/"
+            f"{manifest.name}/{frontend.remoteEntry}"
+        )
         extension_data.update(
             {
                 "remoteEntry": remote_entry_url,
diff --git a/tests/unit_tests/extensions/test_types.py 
b/tests/unit_tests/extensions/test_types.py
index dea22ee19c..0df29ac362 100644
--- a/tests/unit_tests/extensions/test_types.py
+++ b/tests/unit_tests/extensions/test_types.py
@@ -38,12 +38,14 @@ def test_extension_config_minimal():
     """Test ExtensionConfig with minimal required fields."""
     config = ExtensionConfig.model_validate(
         {
-            "id": "my-extension",
-            "name": "My Extension",
+            "publisher": "my-org",
+            "name": "my-extension",
+            "displayName": "My Extension",
         }
     )
-    assert config.id == "my-extension"
-    assert config.name == "My Extension"
+    assert config.publisher == "my-org"
+    assert config.name == "my-extension"
+    assert config.displayName == "My Extension"
     assert config.version == "0.0.0"
     assert config.dependencies == []
     assert config.permissions == []
@@ -55,8 +57,9 @@ def test_extension_config_full():
     """Test ExtensionConfig with all fields populated."""
     config = ExtensionConfig.model_validate(
         {
-            "id": "query_insights",
-            "name": "Query Insights",
+            "publisher": "acme-corp",
+            "name": "query-insights",
+            "displayName": "Query Insights",
             "version": "1.0.0",
             "license": "Apache-2.0",
             "description": "A query insights extension",
@@ -83,8 +86,9 @@ def test_extension_config_full():
             },
         }
     )
-    assert config.id == "query_insights"
-    assert config.name == "Query Insights"
+    assert config.publisher == "acme-corp"
+    assert config.name == "query-insights"
+    assert config.displayName == "Query Insights"
     assert config.version == "1.0.0"
     assert config.license == "Apache-2.0"
     assert config.description == "A query insights extension"
@@ -97,32 +101,50 @@ def test_extension_config_full():
     assert config.backend.files == ["backend/src/query_insights/**/*.py"]
 
 
-def test_extension_config_missing_id():
-    """Test ExtensionConfig raises error when id is missing."""
+def test_extension_config_missing_publisher():
+    """Test ExtensionConfig raises error when publisher is missing."""
     with pytest.raises(ValidationError) as exc_info:
-        ExtensionConfig.model_validate({"name": "My Extension"})
-    assert "id" in str(exc_info.value)
+        ExtensionConfig.model_validate(
+            {"name": "my-extension", "displayName": "My Extension"}
+        )
+    assert "publisher" in str(exc_info.value)
 
 
 def test_extension_config_missing_name():
     """Test ExtensionConfig raises error when name is missing."""
     with pytest.raises(ValidationError) as exc_info:
-        ExtensionConfig.model_validate({"id": "my-extension"})
+        ExtensionConfig.model_validate(
+            {"publisher": "my-org", "displayName": "My Extension"}
+        )
     assert "name" in str(exc_info.value)
 
 
-def test_extension_config_empty_id():
-    """Test ExtensionConfig raises error when id is empty."""
+def test_extension_config_missing_display_name():
+    """Test ExtensionConfig raises error when displayName is missing."""
     with pytest.raises(ValidationError) as exc_info:
-        ExtensionConfig.model_validate({"id": "", "name": "My Extension"})
-    assert "id" in str(exc_info.value)
+        ExtensionConfig.model_validate({"publisher": "my-org", "name": 
"my-extension"})
+    assert "displayName" in str(exc_info.value)
+
+
+def test_extension_config_empty_publisher():
+    """Test ExtensionConfig raises error when publisher is empty."""
+    with pytest.raises(ValidationError) as exc_info:
+        ExtensionConfig.model_validate(
+            {"publisher": "", "name": "my-extension", "displayName": "My 
Extension"}
+        )
+    assert "publisher" in str(exc_info.value)
 
 
 def test_extension_config_invalid_version():
     """Test ExtensionConfig raises error for invalid version format."""
     with pytest.raises(ValidationError) as exc_info:
         ExtensionConfig.model_validate(
-            {"id": "my-extension", "name": "My Extension", "version": 
"invalid"}
+            {
+                "publisher": "my-org",
+                "name": "my-extension",
+                "displayName": "My Extension",
+                "version": "invalid",
+            }
         )
     assert "version" in str(exc_info.value)
 
@@ -131,7 +153,12 @@ def test_extension_config_valid_versions():
     """Test ExtensionConfig accepts valid semantic versions (major.minor.patch 
only)."""
     for version in ["1.0.0", "0.1.0", "10.20.30"]:
         config = ExtensionConfig.model_validate(
-            {"id": "my-extension", "name": "My Extension", "version": version}
+            {
+                "publisher": "my-org",
+                "name": "my-extension",
+                "displayName": "My Extension",
+                "version": version,
+            }
         )
         assert config.version == version
 
@@ -140,7 +167,12 @@ def test_extension_config_prerelease_version_rejected():
     """Test ExtensionConfig rejects prerelease versions."""
     with pytest.raises(ValidationError) as exc_info:
         ExtensionConfig.model_validate(
-            {"id": "my-extension", "name": "My Extension", "version": 
"1.0.0-beta"}
+            {
+                "publisher": "my-org",
+                "name": "my-extension",
+                "displayName": "My Extension",
+                "version": "1.0.0-beta",
+            }
         )
     assert "version" in str(exc_info.value)
 
@@ -154,12 +186,16 @@ def test_manifest_minimal():
     """Test Manifest with minimal required fields."""
     manifest = Manifest.model_validate(
         {
-            "id": "my-extension",
-            "name": "My Extension",
+            "id": "my-org.my-extension",
+            "publisher": "my-org",
+            "name": "my-extension",
+            "displayName": "My Extension",
         }
     )
-    assert manifest.id == "my-extension"
-    assert manifest.name == "My Extension"
+    assert manifest.id == "my-org.my-extension"
+    assert manifest.publisher == "my-org"
+    assert manifest.name == "my-extension"
+    assert manifest.displayName == "My Extension"
     assert manifest.frontend is None
     assert manifest.backend is None
 
@@ -168,8 +204,10 @@ def test_manifest_with_frontend():
     """Test Manifest with frontend section requires remoteEntry."""
     manifest = Manifest.model_validate(
         {
-            "id": "my-extension",
-            "name": "My Extension",
+            "id": "my-org.my-extension",
+            "publisher": "my-org",
+            "name": "my-extension",
+            "displayName": "My Extension",
             "frontend": {
                 "remoteEntry": "remoteEntry.abc123.js",
                 "contributions": {},
@@ -187,8 +225,10 @@ def test_manifest_frontend_missing_remote_entry():
     with pytest.raises(ValidationError) as exc_info:
         Manifest.model_validate(
             {
-                "id": "my-extension",
-                "name": "My Extension",
+                "id": "my-org.my-extension",
+                "publisher": "my-org",
+                "name": "my-extension",
+                "displayName": "My Extension",
                 "frontend": {"contributions": {}, "moduleFederation": {}},
             }
         )
@@ -199,8 +239,10 @@ def test_manifest_with_backend():
     """Test Manifest with backend section."""
     manifest = Manifest.model_validate(
         {
-            "id": "my-extension",
-            "name": "My Extension",
+            "id": "my-org.my-extension",
+            "publisher": "my-org",
+            "name": "my-extension",
+            "displayName": "My Extension",
             "backend": {"entryPoints": ["my_extension.entrypoint"]},
         }
     )
@@ -212,8 +254,10 @@ def test_manifest_backend_no_files_field():
     """Test ManifestBackend does not have files field (only in 
ExtensionConfig)."""
     manifest = Manifest.model_validate(
         {
-            "id": "my-extension",
-            "name": "My Extension",
+            "id": "my-org.my-extension",
+            "publisher": "my-org",
+            "name": "my-extension",
+            "displayName": "My Extension",
             "backend": {"entryPoints": ["my_extension.entrypoint"]},
         }
     )

Reply via email to