Repository: skupperproject/skupper
Pull request #1641 (2024-09-09 15:12:55)

Nonkube sites enhancement to use namespaces
* Using namespaces to isolate sites
* Container names are now prefixed with namespace name
* Systemd services are now prefixed with namespace name
* Bootstrap README file updated (cmd/bootstrap/README.md)
* Bootstrap can now re-initialize a given namespace
* Bootstrap now accepts bundle strategy flag (or SKUPPER_BUNDLE env variable)
* Bundle and Tarball strategies are no longer valid SKUPPER_PLATFORM values
* Bundles can be installed to different and multiple namespaces
* Bootstrap binary and script now use similar flags
* Router names now have a dynamic suffix
* Static status is now added to processed CRs
* Installation summary is clearer now


fgiorgetti: @c-kruse I have addressed the initial feedback.
Let me know if your thoughts.

Patch:
diff --git a/api/types/types.go b/api/types/types.go
index a2f11e5b..e326aa66 100644
--- a/api/types/types.go
+++ b/api/types/types.go
@@ -323,8 +323,6 @@ const (
 	PlatformPodman     Platform = "podman"
 	PlatformDocker     Platform = "docker"
 	PlatformSystemd    Platform = "systemd"
-	PlatformBundle     Platform = "bundle"
-	PlatformBundleTgz  Platform = "tarball"
 )
 
 func (p Platform) IsKubernetes() bool {
@@ -335,14 +333,6 @@ func (p Platform) IsContainerEngine() bool {
 	return p == PlatformDocker || p == PlatformPodman
 }
 
-func (p Platform) IsBundle() bool {
-	return p == PlatformBundle || p.IsTarball()
-}
-
-func (p Platform) IsTarball() bool {
-	return p == PlatformBundleTgz
-}
-
 type ConsoleAuthMode string
 
 const (
diff --git a/cmd/bootstrap/README.md b/cmd/bootstrap/README.md
index b08c21cf..b53f19ab 100644
--- a/cmd/bootstrap/README.md
+++ b/cmd/bootstrap/README.md
@@ -2,21 +2,27 @@
 
 In this current phase of the Skupper V2 implementation, non-kubernetes sites
 can be bootstrapped using a locally built binary, that can be produced by running
-`make build-bootstrap`, or using the (eventually outdated) quay.io/skupper/bootstrap
-container image by calling `./cmd/bootstrap/bootstrap.sh`.
+`make build-bootstrap`, or using the quay.io/skupper/bootstrap:v2-latest
+container image by calling `./cmd/bootstrap/bootstrap.sh` with the appropriate
+flags.
+
+It is important to mention that the bootstrap procedure executed by the provided
+`bootstrap` binary, will be incorporated into the Skupper CLI shortly.
 
 Non-kubernetes sites can be created using the standard V2 site declaration
 approach, which is based on the new set of Custom Resource Definitions (CRDs).
 
 The same CRDs used to create a V2 Skupper site on a Kubernetes cluster can be
 used to create non-kubernetes sites. Some particular options and fields are
-valid or required only on specific platforms, but overall the structure is the
+valid or required only on specific platforms, but the overall structure is the
 same.
 
 Differently than the current Kubernetes implementation, non-kubernetes sites
 are static. Meaning that you must have all your Custom Resources (CRs) ready
 at the time your site is bootstrapped and if something needs to change, you
-will need to remove and bootstrap your site again.
+will need to bootstrap it over. If you do that, the Certificate Authorities (CAs)
+are preserved, so if there is any eventual existing incoming link, it should
+be able to reconnect.
 
 ## Supported platforms
 
@@ -27,8 +33,6 @@ allowed values:
 * podman
 * docker
 * systemd
-* bundle
-* tarball
 
 ### Container engine platforms
 
@@ -41,43 +45,74 @@ unix socket will be used based on the current user and platform selected.
 The `systemd` platform actually requires that you have a local installation of
 the `skupper-router` (`skrouterd` binary must be available in your PATH).
 
-### Site bundle strategies
-
-If you do not want to run an actual site from your CRs, but instead, you just
-need to produce a bundle that can be installed somewhere else, then you can
-use `bundle` or `tarball` as the platform.
+## Bootstrap usage
 
-The `bundle` platform will produce a self-extracting shell archive that can
-be executed to install your local non-kubernetes site.
+### Bootstrap command and flags
 
-If you are not comfortable executing the produced script, you can also choose
-`tarball` as the platform. It produces a tar ball that contains a `install.sh`
-script, that basically performs the same procedure of the `bundle`.
-
-Both scripts accept flags:
-
-```
--h               help
--p <platform>    podman, docker, systemd
--x               remove
--d <directory>   dump static tokens
+```shell
+Skupper bootstrap
+
+Bootstraps a nonkube Skupper site base on the provided flags.
+
+When the path (-p) flag is provided, it will be used as the source
+directory containing the Skupper custom resources to be processed,
+generating a local Skupper site using the "default" namespace, unless
+a namespace is set in the custom resources, or if the namespace (-n)
+flag is provided.
+
+A namespace is just a directory in the file system where all site specific
+files are stored, like certificates, configurations, the original sources
+(original custom resources used to bootstrap the nonkube site) and
+the runtime files generated during initialization.
+
+Namespaces are stored under ${XDG_DATA_HOME}/.local/share/skupper/namespaces
+for regular users when XDG_DATA_HOME environment variable is set, or under
+${HOME}/.local/share/skupper/namespaces when it is not set.
+As the root user, namespaces are stored under: /usr/local/share/skupper/namespaces.
+
+In case the path (-p) flag is omitted, Skupper will try to process
+custom resources stored at the sources directory of the default namespace,
+or from the namespace provided through the namespace (-n) flag.
+
+If the respective namespace already exists and you want to bootstrap it
+over, you must provide the force (-f) flag. When you do that, the existing
+Certificate Authorities (CAs) are preserved, so eventual existing incoming
+links should be able to reconnect.
+
+To produce a bundle, instead of rendering a site, the bundle strategy (-b)
+flag must be set to "bundle" or "tarball".
+
+Usage:
+  bootstrap [options...]
+
+Flags:
+  -b string
+    	The bundle strategy to be produced: bundle or tarball
+  -f	Forces to overwrite an existing namespace
+  -n string
+    	The target namespace used for installation
+  -p string
+    	Custom resources location on the file system
+  -v	Report the version of the Skupper bootstrap command
 ```
 
-## Usage
-
 ### Bootstrapping and removing non-kubernetes sites
 
 Place all your Custom Resources (CRs) on a local directory.
 You must always provide a `Site` (CR), plus some other resource
 that makes your running site meaningful, like Listeners and/or Connectors.
 
-In case you want your local site to listen for incoming links from other sites,
+In case you want your local site to accept incoming links from other sites,
 at present, you have to explicitly provide a `RouterAccess` (CR).
 
 If you want your non-kubernetes site to establish links to other sites, make
 sure you also provide `AccessToken` or `Link` (CRs).
 
-***NOTE:** A V2 representation of the **"Hello world example"** is available below
+If the CRs have no namespace set, Skupper assumes "default" as the namespace
+to be used, otherwise it will use the namespace defined in the CRs, unless you
+force a specific namespace to be used through the `-n` flag.
+
+***NOTES:** A V2 representation of the **"Hello world example"** is available below
 to provide initial guidance.*
 
 Now that you have all your CRs placed on a local directory, just run:
@@ -87,39 +122,71 @@ Now that you have all your CRs placed on a local directory, just run:
 ```shell
 # To bootstrap your site using the binary
 export SKUPPER_PLATFORM=podman
-./bootstrap ./
+./bootstrap -p ./
 ```
 
+You can also use `-n=<name>` to override the namespace specified in the CRs.
+
 Alternatively you can also run the `bootstrap.sh` script:
 
 ```shell
 # To bootstrap your site using the shell script
 export SKUPPER_PLATFORM=podman
-./cmd/bootstrap/bootstrap.sh ./
+./cmd/bootstrap/bootstrap.sh -p ./
 ```
 
+Similarly to the binary, you can also use `-n <namespace>` to override the namespace
+defined in the source CRs.
+
 Remember that you can set the `SKUPPER_PLATFORM` environment variable to
 any of the platforms mentioned earlier.
 
+If you do not want to run an actual site from your CRs, but instead, you just
+need to produce a bundle that can be installed somewhere else, then you can
+use `bundle` or `tarball` as the bundle strategy (-b flag).
+
+The `bundle` strategy will produce a self-extracting shell archive that can
+be executed to install your local non-kubernetes site.
+
+If you are not comfortable executing the produced script, you can also choose
+`tarball` as the bundle strategy. It produces a tar ball that contains an
+`install.sh` script, that basically performs the same procedure of the
+self-extracting `bundle`.
+
 After the bootstrap procedure is completed, it will provide you some relevant
 information like:
 
-* Location where static tokens have been defined (when a `RouterAccess` is defined)
-* Location where the site bundle has been saved (when using `bundle` or `tarball`)
+* Location where static links have been defined (when a `RouterAccess` is provided)
+* Location where the site bundle has been saved (if bundle strategy flag `-b` set)
+* Namespace, Site name, Platform, Version and path to the sources
 
 #### Site bundles
 
 When a site bundle is produced, the bootstrap procedure will point you
 to the location where it has been saved.
 
+##### Bundle installation usage
+
+The bundle installation script accepts the following flags:
+
+```
+-h               help
+-p <platform>    podman, docker, systemd
+-n <namespace>   if not provided, the namespace defined in the bundle is used (if none, default is used)
+-x               remove site and namespace
+-d <directory>   dump static links into the provided directory 
+```
+
 #### Removing
 
-To remove your site, you can run a local script, that takes the site name
+To remove your site, you can run a local script, that takes the namespace
 as an argument. The platform is identified by the script so you don't need
 to export it.
 
+If the namespace is omitted, the "default" namespace is used. 
+
 ```shell
-./cmd/bootstrap/remove.sh <site-name>
+./cmd/bootstrap/remove.sh [namespace]
 ```
 
 ## Example
@@ -127,25 +194,26 @@ to export it.
 Here is a very basic demonstration on how you can run the `Hello World` example
 locally using just non-kubernetes sites.
 
-The example is assuming you are running both `west` and `east` sites on your
-machine. So if you want to run it on different machines, make sure to adjust
+The example assumes you are running both `west` and `east` sites on your
+machine. So if you want to run them on different machines, make sure to adjust
 IP addresses properly.
 
 ### Hello world
 
-This example basically runs a **frontend** application on the `west` site,
-which depends on a **backend** that is meant to run on the `east` site.
+This example basically runs a **frontend** application which depends on
+a **backend** service that is not initially accessible. The access will
+be provided by the Skupper Site running on the `east` namespace.
 
-To simulate it, the frontend application will be executed using podman,
-it will be exporting port 7070 in the loopback interface of the host machine,
-and we will tell it to expect that the backend service will be available
+To simulate it, the frontend application will be executed using podman.
+It exposes port 7070 in the loopback interface of the host machine,
+and must tell it to expect that the backend service will be available
 at `http://host.containers.internal:8080`, which from inside the container,
 means the host machine at port 8080.
 
-The backend will also run on the host machine with podman, and it will be
-exporting port 9090 to the loopback interface of the host machine. So after
-both `frontend` and `backend` containers are running, they won't be able
-to communicate.
+The backend also runs on the host machine with podman, and it
+exposes port 9090 to the loopback interface of the host machine.
+So after both `frontend` and `backend` containers are running,
+they won't be able to communicate.
 
 We will use two Skupper sites to resolve that. On the `west` site, Skupper
 will expose a `Listener` (CR), bound to port `8080` of the host machine.
@@ -154,8 +222,10 @@ On the `east` site, we will have a `Connector` (CR) that targets localhost
 at port 9090, in the host machine.
 
 The ideal scenario would be to run each component on different machines,
-where Skupper could add real value, but just for the purpose of making it
-simple to run, the example is defined to work at a single machine.
+where Skupper could add real value.
+But just with the purpose of making it simple to be executed locally,
+this example has been designed so that you can run everything using a
+single machine.
 
 #### Workloads
 
@@ -173,6 +243,8 @@ podman run --name backend -d --rm -p 127.0.0.1:9090:8080 quay.io/skupper/hello-w
 
 #### West
 
+##### Preparing the CRs
+
 The `west` site will be defined using the following CRs:
 
 ##### Site
@@ -212,11 +284,23 @@ spec:
   bindHost: 127.0.0.1
 ```
 
-Now that the `west` site has been created, copy the generated token into
+##### Bootstrap the west site
+
+Considering all your CRs have been saved to a directory named `west`, use:
+
+```shell
+./bootstrap -p ./west -n west
+```
+
+The CRs are defined without a namespace, that is why we have the `-n west` flag,
+to indicate we want this site to run under the `west` namespace.
+Otherwise, the `default` namespace would be used.
+
+Now that the `west` site has been created, copy the generated link into
 the `east` site local directory. Example:
 
 ```shell
-cp ${HOME}/.local/share/skupper/sites/west/runtime/token/link-go-west.yaml ./east/links/link-go-west.yaml
+cp ${HOME}/.local/share/skupper/namespaces/west/runtime/link/link-go-west.yaml ./east/link-go-west.yaml
 ```
 
 #### East
@@ -273,7 +357,101 @@ spec:
   tlsCredentials: link-go-west
 ```
 
+##### Bootstrap the east site
+
+Considering all your CRs have been saved to a directory named `east`, use:
+
+```shell
+./bootstrap -p ./east -n east
+```
+
 ### Testing the scenario
 
 Once both sites have been initialized, open **http://127.0.0.1:7070**
 in your browser and it should work.
+
+### Updating an existing installation
+
+Suppose modifications have been made to the `west` site CRs, directly at the
+namespace directory (i.e: ${HOME}/.local/share/skupper/namespaces/west/sources).
+
+To re-initialize the west site, run:
+
+```shell
+bootstrap -n west -f
+```
+
+The command above will reprocess all source CRs from the namespace path and
+restart the related components. The Certificate Authorities (CAs) are preserved,
+therefore eventual incoming links must still work.
+
+In case the CRs are located somewhere else, then a path must also be specified, as
+in the example below:
+
+```shell
+bootstrap -n west -p <path> -f
+```
+
+### Cleanup
+
+To remove both namespaces, run:
+
+```shell
+./cmd/bootstrap/remove.sh west
+./cmd/bootstrap/remove.sh east
+```
+
+### Producing site bundles
+
+As an alternative, you can also produce site bundles to try this example.
+
+#### Creating the west bundle
+
+```shell
+$ bootstrap -p ./west/ -b bundle
+Skupper nonkube bootstrap (version: main-release-161-g7c5100a2-modified)
+Site "west" has been created (as a distributable bundle)
+Installation bundle available at: /home/user/.local/share/skupper/bundles/skupper-install-west.sh
+Default namespace: default
+Default platform: docker
+```
+
+#### Extracting the static link
+
+To extract the link, run:
+
+```shell
+/home/user/.local/share/skupper/bundles/skupper-install-west.sh -d /tmp
+Static links for site "west" have been saved into /tmp/west
+```
+
+Copy the static Link to the `east` site definition.
+
+```shell
+cp /tmp/west/link-go-west.yaml ./east/link-go-west.yaml
+```
+
+#### Creating the east bundle
+
+```shell
+$ bootstrap -p ./east -b bundle
+Skupper nonkube bootstrap (version: main-release-161-g7c5100a2-modified)
+Site "east" has been created (as a distributable bundle)
+Installation bundle available at: /home/user/.local/share/skupper/bundles/skupper-install-east.sh
+Default namespace: default
+Default platform: podman
+```
+
+#### Installing both bundles
+
+```shell
+/home/user/.local/share/skupper/bundles/skupper-install-west.sh -n west
+/home/user/.local/share/skupper/bundles/skupper-install-east.sh -n east
+```
+
+#### Cleanup bundle installation
+
+```shell
+/home/user/.local/share/skupper/bundles/skupper-install-west.sh -x
+/home/user/.local/share/skupper/bundles/skupper-install-east.sh -x
+```
diff --git a/cmd/bootstrap/bootstrap.go b/cmd/bootstrap/bootstrap.go
index aa29e661..4992d067 100644
--- a/cmd/bootstrap/bootstrap.go
+++ b/cmd/bootstrap/bootstrap.go
@@ -6,53 +6,122 @@ import (
 	"os"
 	"os/exec"
 	"path"
+	"path/filepath"
 
 	"github.com/skupperproject/skupper/api/types"
+	internalbundle "github.com/skupperproject/skupper/internal/nonkube/bundle"
 	"github.com/skupperproject/skupper/pkg/config"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/nonkube/bundle"
 	"github.com/skupperproject/skupper/pkg/nonkube/common"
 	"github.com/skupperproject/skupper/pkg/nonkube/compat"
 	"github.com/skupperproject/skupper/pkg/nonkube/systemd"
+	"github.com/skupperproject/skupper/pkg/utils"
 	"github.com/skupperproject/skupper/pkg/version"
 )
 
 var (
-	platform = config.GetPlatform()
+	platform       = config.GetPlatform()
+	inputPath      string
+	userNamespace  string
+	bundleStrategy string
+)
+
+const (
+	description = `
+Bootstraps a nonkube Skupper site base on the provided flags.
+
+When the path (-p) flag is provided, it will be used as the source
+directory containing the Skupper custom resources to be processed,
+generating a local Skupper site using the "default" namespace, unless
+a namespace is set in the custom resources, or if the namespace (-n)
+flag is provided.
+
+A namespace is just a directory in the file system where all site specific
+files are stored, like certificates, configurations, the original sources
+(original custom resources used to bootstrap the nonkube site) and
+the runtime files generated during initialization.
+
+Namespaces are stored under ${XDG_DATA_HOME}/.local/share/skupper/namespaces
+for regular users when XDG_DATA_HOME environment variable is set, or under
+${HOME}/.local/share/skupper/namespaces when it is not set.
+As the root user, namespaces are stored under: /usr/local/share/skupper/namespaces.
+
+In case the path (-p) flag is omitted, Skupper will try to process
+custom resources stored at the sources directory of the default namespace,
+or from the namespace provided through the namespace (-n) flag.
+
+If the respective namespace already exists and you want to bootstrap it
+over, you must provide the force (-f) flag. When you do that, the existing
+Certificate Authorities (CAs) are preserved, so eventual existing incoming
+links should be able to reconnect.
+
+To produce a bundle, instead of rendering a site, the bundle strategy (-b)
+flag must be set to "bundle" or "tarball".
+`
 )
 
 func main() {
 	// if -version used, report and exit
-	isVersion := flag.Bool("version", false, "Report the version of the Skupper bootstrap command")
+	flag.Usage = func() {
+		fmt.Println("Skupper bootstrap")
+		fmt.Printf("%s\n", description)
+		fmt.Printf("Usage:\n  %s [options...]\n\n", os.Args[0])
+		fmt.Printf("Flags:\n")
+		flag.PrintDefaults()
+	}
+	flag.StringVar(&inputPath, "p", "", "Custom resources location on the file system")
+	flag.StringVar(&userNamespace, "n", "", "The target namespace used for installation")
+	flag.StringVar(&bundleStrategy, "b", "", "The bundle strategy to be produced: bundle or tarball")
+	force := flag.Bool("f", false, "Forces to overwrite an existing namespace")
+	isVersion := flag.Bool("v", false, "Report the version of the Skupper bootstrap command")
 	flag.Parse()
 	if *isVersion {
 		fmt.Println(version.Version)
 		os.Exit(0)
 	}
-
+	// if user overrides and force empty, use it as default
+	if inputPath != "" {
+		var err error
+		inputPath, err = filepath.Abs(inputPath)
+		if err != nil {
+			fmt.Printf("Unable to determine absolute path of %s: %v\n", inputPath, err)
+			os.Exit(1)
+		}
+	}
+	if bundleStrategy != "" {
+		if !internalbundle.IsValidBundle(bundleStrategy) {
+			fmt.Printf("Invalid bundle strategy: %s\n", bundleStrategy)
+			os.Exit(1)
+		}
+	}
 	//
 	// NOTE FOR CONTAINERS
 	// When running bootstrap process through a container
 	// the /input path must be mapped to a directory containing a site
-	// definition based on CR files.
-	// It also expects the /output path to be mapped to the
-	// Host's XDG_DATA_HOME/skupper or $HOME/.local/share/skupper (non-root)
+	// definition based on CR files, if an input path has been provided.
+	// The /output path must be mapped to the Host's XDG_DATA_HOME/skupper
+	// or $HOME/.local/share/skupper (non-root)
 	// and /usr/local/share/skupper (root).
 	//
 	fmt.Printf("Skupper nonkube bootstrap (version: %s)\n", version.Version)
 
-	var inputPath string
-	var outputPath string
-	inputPath = flag.Arg(0)
-
 	if platform.IsKubernetes() {
+		platform = types.PlatformPodman
 		// Bootstrap uses podman as the default platform
-		_ = os.Setenv(types.ENV_PLATFORM, "podman")
+		//_ = os.Setenv(types.ENV_PLATFORM, "podman")
 	}
-	if apis.IsRunningInContainer() {
-		inputPath = "/input"
-		outputPath = "/output"
-		for _, directory := range []string{inputPath, outputPath} {
+	isBundle := internalbundle.GetBundleStrategy(bundleStrategy) != ""
+	if api.IsRunningInContainer() {
+		requiredPaths := []string{"/output"}
+		if inputPath != "" {
+			if inputPath != "/input" {
+				fmt.Println("The input path must be set to /input when using a container to bootstrap")
+				os.Exit(1)
+			}
+			requiredPaths = append(requiredPaths, "/input")
+		}
+		for _, directory := range requiredPaths {
 			stat, err := os.Stat(directory)
 			if err != nil {
 				fmt.Printf("Failed to stat %s: %s\n", directory, err)
@@ -63,7 +132,7 @@ func main() {
 				os.Exit(1)
 			}
 		}
-	} else if !platform.IsBundle() {
+	} else if !isBundle {
 		binary := "podman"
 		if platform == types.PlatformSystemd {
 			binary = "skrouterd"
@@ -77,72 +146,143 @@ func main() {
 			os.Exit(1)
 		}
 	}
-	// TODO defined standard places for input path?
+	namespace := userNamespace
+	if namespace == "" {
+		namespace = "default"
+	}
 	if inputPath == "" {
-		fmt.Printf("No input path specified\n")
+		// when input path is empty, but a namespace is provided, try to reload an existing site definition
+		existingPath := api.GetInternalOutputPath(namespace, api.LoadedSiteStatePath)
+		if _, err := os.Stat(existingPath); err == nil {
+			inputPath = existingPath
+			fmt.Printf("Sources will consumed from namespace %q\n", namespace)
+		} else {
+			fmt.Printf("Input path has not been provided and namespace %s does not exist\n", namespace)
+			os.Exit(1)
+		}
+	}
+
+	// if namespace already exists, fail if force is not set
+	_, err := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimeSiteStatePath))
+	if !isBundle && err == nil && !*force {
+		fmt.Printf("Namespace already exists: %s\n", namespace)
 		os.Exit(1)
 	}
 
-	siteState, err := bootstrap(inputPath)
+	siteState, err := bootstrap(inputPath, userNamespace, internalbundle.GetBundleStrategy(bundleStrategy))
 	if err != nil {
 		fmt.Println("Failed to bootstrap:", err)
 		os.Exit(1)
 	}
 	var bundleSuffix string
-	if platform.IsBundle() {
+	if isBundle {
 		bundleSuffix = " (as a distributable bundle)"
+	} else {
+		bundleSuffix = fmt.Sprintf(" on namespace %q", siteState.GetNamespace())
+		// create bootstrap.out file
+		if api.IsRunningInContainer() {
+			outFile, err := os.Stat("/bootstrap.out")
+			if err == nil && !outFile.IsDir() {
+				err = os.WriteFile("/bootstrap.out", []byte(siteState.GetNamespace()), 0644)
+				if err != nil {
+					fmt.Println("Failed to write to bootstrap.out:", err)
+					fmt.Println("The systemd service will not be created.")
+				}
+			}
+		}
 	}
 	fmt.Printf("Site %q has been created%s\n", siteState.Site.Name, bundleSuffix)
-	if !platform.IsBundle() {
-		siteHome, err := apis.GetHostSiteHome(siteState.Site)
+	if !isBundle {
+		fmt.Printf("Platform: %s\n", platform)
+		tokenPath := api.GetInternalOutputPath(siteState.Site.Namespace, api.RuntimeTokenPath)
+		hostTokenPath, err := api.GetHostSiteInternalPath(siteState.Site, api.RuntimeTokenPath)
 		if err != nil {
-			fmt.Println("Failed to get site's home directory:", err)
-		} else {
-			tokenPath := path.Join(siteHome, common.RuntimeTokenPath)
-			hostTokenPath := tokenPath
-			if apis.IsRunningInContainer() {
-				tokenPath = path.Join("/output", "sites", siteState.Site.Name, common.RuntimeTokenPath)
-			}
-			tokens, _ := os.ReadDir(tokenPath)
-			for _, token := range tokens {
-				if !token.IsDir() {
-					fmt.Println("Static tokens have been defined at:", hostTokenPath)
-					break
-				}
+			fmt.Println("Failed to get site's static links path:", err)
+		}
+		tokens, _ := os.ReadDir(tokenPath)
+		for _, token := range tokens {
+			if !token.IsDir() {
+				fmt.Println("Static links have been defined at:", hostTokenPath)
+				break
 			}
 		}
+		sourcesPath, _ := api.GetHostSiteInternalPath(siteState.Site, api.LoadedSiteStatePath)
+		fmt.Printf("Definition is available at: %s\n", sourcesPath)
 	} else {
-		dataHome, err := apis.GetHostDataHome()
+		siteHome, err := api.GetHostBundlesPath()
 		if err != nil {
 			fmt.Println("Failed to get site bundle base directory:", err)
 		}
-		installationFile := path.Join(dataHome, "sites", fmt.Sprintf("skupper-install-%s.sh", siteState.Site.Name))
-		if platform.IsTarball() {
-			installationFile = path.Join(dataHome, "sites", fmt.Sprintf("skupper-install-%s.tar.gz", siteState.Site.Name))
+		installationFile := path.Join(siteHome, fmt.Sprintf("skupper-install-%s.sh", siteState.Site.Name))
+		if internalbundle.GetBundleStrategy(bundleStrategy) == string(internalbundle.BundleStrategyTarball) {
+			installationFile = path.Join(siteHome, fmt.Sprintf("skupper-install-%s.tar.gz", siteState.Site.Name))
 		}
 		fmt.Println("Installation bundle available at:", installationFile)
+		fmt.Println("Default namespace:", siteState.GetNamespace())
+		fmt.Println("Default platform:", utils.DefaultStr(string(platform), "podman"))
 	}
 }
 
-func bootstrap(inputPath string) (*apis.SiteState, error) {
-	var siteStateLoader apis.SiteStateLoader
+func bootstrap(inputPath string, namespace string, bundleStrategy string) (*api.SiteState, error) {
+	var siteStateLoader api.SiteStateLoader
+	var reloadExisting bool
+	isBundle := bundleStrategy != ""
+	sourcesPath := api.GetInternalOutputPath(namespace, api.LoadedSiteStatePath)
+	_, err := os.Stat(sourcesPath)
+	if !isBundle && err == nil {
+		reloadExisting = true
+		nsPlatformLoader := &common.NamespacePlatformLoader{}
+		nsPlatform, err := nsPlatformLoader.Load(namespace)
+		if err != nil {
+			_, runtimeStateErr := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimeSiteStatePath))
+			if runtimeStateErr == nil {
+				return nil, fmt.Errorf("unable to determine current platform used in namespace %q", namespace)
+			}
+			// platform.yaml not present, which is ok if a site is not yet rendered
+			nsPlatform = string(platform)
+			reloadExisting = false
+		}
+		currentPlatform := string(platform)
+		if platform.IsKubernetes() {
+			currentPlatform = "podman"
+		}
+		if nsPlatform != currentPlatform {
+			return nil, fmt.Errorf("existing namespace uses %q platform and it cannot change to %q", nsPlatform, currentPlatform)
+		}
+	}
 	siteStateLoader = &common.FileSystemSiteStateLoader{
 		Path:   inputPath,
-		Bundle: platform.IsBundle(),
+		Bundle: isBundle,
 	}
 	siteState, err := siteStateLoader.Load()
 	if err != nil {
 		return nil, fmt.Errorf("failed to load site state: %v", err)
 	}
-	var siteStateRenderer apis.StaticSiteStateRenderer
-	if platform == types.PlatformSystemd {
+	// if sources are being consume from namespace sources, they must be properly set
+	crNamespace := siteState.GetNamespace()
+	targetNamespace := utils.DefaultStr(namespace, "default")
+	if inputPath == sourcesPath {
+		if crNamespace != targetNamespace {
+			return nil, fmt.Errorf("namespace must be %q, but sources are defined using %q", targetNamespace, crNamespace)
+		}
+	} else if namespace != "" {
+		siteState.SetNamespace(namespace)
+	}
+
+	var siteStateRenderer api.StaticSiteStateRenderer
+	if isBundle {
+		siteStateRenderer = &bundle.SiteStateRenderer{
+			Strategy: internalbundle.BundleStrategy(bundleStrategy),
+			Platform: platform,
+		}
+	} else if platform == types.PlatformSystemd {
 		siteStateRenderer = &systemd.SiteStateRenderer{}
-	} else if platform.IsBundle() {
-		siteStateRenderer = &bundle.SiteStateRenderer{}
 	} else {
-		siteStateRenderer = &compat.SiteStateRenderer{}
+		siteStateRenderer = &compat.SiteStateRenderer{
+			Platform: platform,
+		}
 	}
-	err = siteStateRenderer.Render(siteState)
+	err = siteStateRenderer.Render(siteState, reloadExisting)
 	if err != nil {
 		return nil, fmt.Errorf("failed to render site state: %v", err)
 	}
diff --git a/cmd/bootstrap/bootstrap.sh b/cmd/bootstrap/bootstrap.sh
index ea973ab7..e6fc0da6 100755
--- a/cmd/bootstrap/bootstrap.sh
+++ b/cmd/bootstrap/bootstrap.sh
@@ -3,14 +3,17 @@
 set -Ceu
 
 IMAGE="quay.io/skupper/bootstrap:v2-latest"
-INPUT_PATH="${1:-${PWD}}"
+export INPUT_PATH=""
+export NAMESPACE=""
+export FORCE_FLAG=""
+export BUNDLE_STRATEGY=""
 SKUPPER_OUTPUT_PATH="${XDG_DATA_HOME:-${HOME}/.local/share}/skupper"
 SERVICE_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
 if [ -z "${UID:-}" ]; then
     UID="$(id -u)"
     export UID
 fi
-LOG_FILE="$(mktemp /tmp/skupper-bootstrap.XXXXX.log)"
+BOOTSTRAP_OUT="$(mktemp /tmp/skupper-bootstrap.XXXXX.out)"
 
 exit_error() {
     echo "$*"
@@ -37,7 +40,7 @@ is_container_platform() {
 }
 
 is_bundle_platform() {
-    if [ "${SKUPPER_PLATFORM}" = "bundle" ] || [ "${SKUPPER_PLATFORM}" = "tarball" ]; then
+    if [ -n "${BUNDLE_STRATEGY}" ]; then
         return 0
     fi
     return 1
@@ -97,13 +100,14 @@ create_service() {
     fi
 
     # generated service file
-    site_name="$(grep -E 'Site ".*" has been created' "${LOG_FILE}" | awk -F'"' '{print $2}')"
-    if [ -z "${site_name}" ]; then
-        echo "Unable to create SystemD service (site name could not be identified)"
+    namespace="$(cat "${BOOTSTRAP_OUT}")"
+    if [ -z "${namespace}" ]; then
+        # unable to create SystemD service (namespace could not be identified)
+        # possibly due to bootstrap failure
         return
     fi
-    service_name="skupper-site-${site_name}.service"
-    service_file="${SKUPPER_OUTPUT_PATH}/sites/${site_name}/runtime/scripts/${service_name}"
+    service_name="skupper-${namespace}.service"
+    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${namespace}/runtime/scripts/${service_name}"
     if [ ! -f "${service_file}" ]; then
         echo "SystemD service has not been defined"
         return
@@ -116,8 +120,7 @@ create_service() {
         systemctl daemon-reload
     else
         if [ ! -d "${SERVICE_DIR}" ]; then
-            echo "Unable to define path to SystemD service"
-            return
+            mkdir -p "${SERVICE_DIR}"
         fi
         cp -f "${service_file}" "${SERVICE_DIR}"
         systemctl --user enable --now "${service_name}"
@@ -125,10 +128,55 @@ create_service() {
     fi
 }
 
+usage() {
+    echo "Use: bootstrap.sh [-p <path>] [-n <namespace>] [-b strategy] [-f]"
+    echo "     -p Custom resources location on the file system"
+    echo "     -n The target namespace used for installation (overrides the namespace from custom resources when -p is provided)"
+    echo "     -b The bundle strategy to be produced: bundle or tarball"
+    echo "     -f Forces to overwrite an existing namespace"
+    exit 1
+}
+
+parse_opts() {
+    while getopts "p:n:b:f" opt; do
+        case "${opt}" in
+            p)
+                export INPUT_PATH="${OPTARG}"
+                if [ -z "${INPUT_PATH}" ] || [ ! -d "${INPUT_PATH}" ]; then
+                    echo "Invalid custom resources path (it must be a directory)"
+                    usage
+                fi
+                ;;
+            b)
+                bundle="${OPTARG}"
+                case "${bundle}" in
+                "bundle")
+                    export BUNDLE_STRATEGY="-b=bundle"
+                    ;;
+                "tarball")
+                    export BUNDLE_STRATEGY="-b=tarball"
+                    ;;
+                esac
+                ;;
+            n)
+                export NAMESPACE="${OPTARG}"
+                if ! echo "${NAMESPACE:?}" | grep -qE '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'; then
+                    echo "Invalid namespace"
+                    usage
+                fi
+                ;;
+            f)
+                export FORCE_FLAG="-f"
+                ;;
+            *)
+                usage
+                ;;
+        esac
+    done
+}
+
 main() {
-    if [ -z "${INPUT_PATH}" ] || [ ! -d "${INPUT_PATH}" ]; then
-        exit_error "Use: bootstrap.sh <local path to CRs>"
-    fi
+    parse_opts "$@"
 
     # Parse Skupper Platform and Container Engine settings
     container_env
@@ -142,9 +190,14 @@ main() {
         file_container_endpoint=$(echo "${CONTAINER_ENDPOINT}" | sed -e "s#unix://##g")
         MOUNTS="${MOUNTS} -v '${file_container_endpoint}:/${CONTAINER_ENGINE}.sock:z'"
     fi
-    MOUNTS="${MOUNTS} -v '${INPUT_PATH}:/input:z'"
+    INPUT_PATH_ARG=""
+    if [ -n "${INPUT_PATH}" ]; then
+        MOUNTS="${MOUNTS} -v '${INPUT_PATH}:/input:z'"
+        INPUT_PATH_ARG="/input"
+    fi
     MOUNTS="${MOUNTS} -v '${SKUPPER_OUTPUT_PATH}:/output:z'"
-    
+    MOUNTS="${MOUNTS} -v '${BOOTSTRAP_OUT}:/bootstrap.out:z'"
+
     # Env vars
     if is_container_platform; then
         if is_sock_endpoint; then
@@ -158,13 +211,13 @@ main() {
 
     # Running the bootstrap
     ${CONTAINER_ENGINE} pull ${IMAGE}
-    if eval "${CONTAINER_ENGINE}" run --rm --name skupper-bootstrap \
+    eval "${CONTAINER_ENGINE}" run --rm --name skupper-bootstrap \
         --network host --security-opt label=disable -u \""${RUNAS}"\" --userns=\""${USERNS}"\" \
         "${MOUNTS}" \
         "${ENV_VARS}" \
-        "${IMAGE}" 2>&1 | tee "${LOG_FILE}"; then
-        create_service
-    fi
+        "${IMAGE}" \
+        /app/bootstrap -p="${INPUT_PATH_ARG}" -n="${NAMESPACE}" ${BUNDLE_STRATEGY} ${FORCE_FLAG} 2>&1
+    create_service
 }
 
 main "$@"
diff --git a/cmd/bootstrap/remove.sh b/cmd/bootstrap/remove.sh
index 3d7f7065..62bd5a28 100755
--- a/cmd/bootstrap/remove.sh
+++ b/cmd/bootstrap/remove.sh
@@ -2,8 +2,9 @@
 
 set -Ceu
 
-if [ $# -eq 0 ] || [ -z "${1}" ]; then
-    echo "Use: ${0##*/} <site-name>"
+namespace=${1:-default}
+if [ -z "${namespace}" ]; then
+    echo "Use: ${0##*/} <namespace>"
     exit 1
 fi
 
@@ -11,45 +12,52 @@ if [ -z "${UID:-}" ]; then
     UID="$(id -u)"
     export UID
 fi
-site=$1
-sites_path="${HOME}/.local/share/skupper/sites"
-service_path="${HOME}/.config/systemd/user"
+namespaces_path="${XDG_DATA_HOME:-${HOME}/.local/share}/skupper/namespaces"
+service_path="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
 systemctl="systemctl --user"
 if [ "${UID}" -eq 0 ]; then
-    sites_path="/usr/local/share/skupper/sites"
+    namespaces_path="/usr/local/share/skupper/namespaces"
     service_path="/etc/systemd/system"
     systemctl="systemctl"
 fi
 
+usage() {
+    echo "Use: remove.sh <namespace>"
+}
+
 remove_definition() {
-    platform_file="${sites_path}/${site}/runtime/state/platform.yaml"
-    SKUPPER_PLATFORM=$(grep '^platform: ' "${platform_file}" | sed -e 's/.*: //g')
-    if [ "${SKUPPER_PLATFORM}" != "systemd" ]; then
-        ${SKUPPER_PLATFORM} rm -f "${site}-skupper-router"
+    platform_file="${namespaces_path}/${namespace}/runtime/state/platform.yaml"
+    if [ -f "${platform_file}" ]; then
+        SKUPPER_PLATFORM=$(grep '^platform: ' "${platform_file}" | sed -e 's/.*: //g')
+        if [ "${SKUPPER_PLATFORM}" = "podman" ] || [ "${SKUPPER_PLATFORM}" = "docker" ]; then
+            ${SKUPPER_PLATFORM} rm -f "${namespace}-skupper-router" > /dev/null
+        fi
     fi
-    rm -rf "${sites_path:?}/${site:?}/"
+    rm -rf "${namespaces_path:?}/${namespace:?}/"
 }
 
 remove_service() {
-    service="skupper-site-${site}.service"
+    service="skupper-${namespace}.service"
     ${systemctl} stop "${service}"
-    ${systemctl} disable "${service}"
+    ${systemctl} disable "${service}" > /dev/null 2>&1
     rm -f "${service_path:?}/${service:?}"
     ${systemctl} daemon-reload
     ${systemctl} reset-failed
 }
 
 main () {
-    if ! echo "${site:?}" | grep -E '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'; then
-        echo "Invalid site name"
+    if ! echo "${namespace:?}" | grep -qE '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'; then
+        echo "Invalid namespace"
+        usage
         exit 0
     fi
-    if [ ! -d "${sites_path:?}/${site:?}" ]; then
-        echo "Site does not exist"
+    if [ ! -d "${namespaces_path:?}/${namespace:?}" ]; then
+        echo "Namespace \"${namespace}\" does not exist"
         exit 0
     fi
     remove_definition
     remove_service
+    echo "Namespace \"${namespace}\" has been removed"
 }
 
-main "$@"
\ No newline at end of file
+main "$@"
diff --git a/internal/nonkube/bundle/common.go b/internal/nonkube/bundle/common.go
index 4c48dc82..cccd2237 100644
--- a/internal/nonkube/bundle/common.go
+++ b/internal/nonkube/bundle/common.go
@@ -2,6 +2,8 @@ package bundle
 
 import (
 	_ "embed"
+	"os"
+	"slices"
 
 	"github.com/skupperproject/skupper/internal/utils"
 )
@@ -9,15 +11,41 @@ import (
 const (
 	scriptExit = "\nexit 0\n"
 	shellDelim = "\n__TARBALL_CONTENT__\n"
+	BundleEnv  = "SKUPPER_BUNDLE"
 )
 
 var (
 	//go:embed install.sh.template
 	installScript string
 	//go:embed self_extract.sh.template
-	selfExtractPart string
+	selfExtractPart         string
+	allowedBundleStrategies = []string{string(BundleStrategyExtract), string(BundleStrategyTarball)}
 )
 
 type BundleGenerator interface {
-	Generate(tarball *utils.Tarball) error
+	Generate(tarball *utils.Tarball, defaultPlatform string) error
+}
+
+type BundleStrategy string
+
+var (
+	BundleStrategyExtract BundleStrategy = "bundle"
+	BundleStrategyTarball BundleStrategy = "tarball"
+)
+
+func IsValidBundle(strategy string) bool {
+	return slices.Contains(allowedBundleStrategies, strategy)
+}
+
+// GetBundleStrategy returns the provided strategy (argument) if it is
+// a valid strategy. If not, it returns the SKUPPER_BUNDLE environment
+// variable's value if it is valid, or empty otherwise.
+func GetBundleStrategy(strategyFlag string) string {
+	if IsValidBundle(strategyFlag) {
+		return strategyFlag
+	}
+	if envStrategy := os.Getenv(BundleEnv); IsValidBundle(envStrategy) {
+		return envStrategy
+	}
+	return ""
 }
diff --git a/internal/nonkube/bundle/extract.go b/internal/nonkube/bundle/extract.go
index 08912452..570773cb 100644
--- a/internal/nonkube/bundle/extract.go
+++ b/internal/nonkube/bundle/extract.go
@@ -9,10 +9,13 @@ import (
 	"text/template"
 
 	"github.com/skupperproject/skupper/internal/utils"
+	pkgutils "github.com/skupperproject/skupper/pkg/utils"
+	"github.com/skupperproject/skupper/pkg/version"
 )
 
 type SelfExtractingBundle struct {
 	SiteName   string
+	Namespace  string
 	OutputPath string
 }
 
@@ -20,7 +23,7 @@ func (s *SelfExtractingBundle) InstallFile() string {
 	return path.Join(s.OutputPath, fmt.Sprintf("skupper-install-%s.sh", s.SiteName))
 }
 
-func (s *SelfExtractingBundle) Generate(tarBall *utils.Tarball) error {
+func (s *SelfExtractingBundle) Generate(tarBall *utils.Tarball, defaultPlatform string) error {
 	var data = new(bytes.Buffer)
 	var err error
 
@@ -44,7 +47,10 @@ func (s *SelfExtractingBundle) Generate(tarBall *utils.Tarball) error {
 	var parsedInstallScript = new(bytes.Buffer)
 	err = installScriptTemplate.Execute(parsedInstallScript, map[string]interface{}{
 		"SiteName":        s.SiteName,
+		"Namespace":       s.Namespace,
+		"Platform":        pkgutils.DefaultStr(defaultPlatform, "podman"),
 		"SelfExtractPart": selfExtractPart,
+		"Version":         version.Version,
 	})
 	if err != nil {
 		return err
diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template
index dda9b08e..cb786b85 100644
--- a/internal/nonkube/bundle/install.sh.template
+++ b/internal/nonkube/bundle/install.sh.template
@@ -8,10 +8,14 @@ set -Ceu
 
 # site name to be provided by bundle generation
 export SITE_NAME="{{.SiteName}}"
-export SKUPPER_PLATFORM="podman"
+export SOURCE_NAMESPACE="{{.Namespace}}"
+export SOURCE_PLATFORM="{{.Platform}}"
+export NAMESPACE="${SOURCE_NAMESPACE}"
+export SKUPPER_PLATFORM="{{.Platform}}"
 export PLATFORM_COMMAND="podman"
 export REMOVE=false
 export DUMP_TOKENS=false
+export VERSION="{{.Version}}"
 
 # standard output directories
 if [ -z "${UID:-}" ]; then
@@ -33,8 +37,8 @@ if [ "${UID}" -eq 0 ]; then
     # shellcheck disable=SC2089
     export USERNS="''"
 fi
-export SITES_PATH="${SKUPPER_OUTPUT_PATH}/sites"
-export PLATFORM_FILE="${SITES_PATH}/${SITE_NAME}/runtime/state/platform.yaml"
+export NAMESPACES_PATH="${SKUPPER_OUTPUT_PATH}/namespaces"
+export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/state/platform.yaml"
 export USER="${USER:-$(id -un)}"
 SITE_ID="$(hostname -s)-${USER}-$(date +%s)"
 export SITE_ID
@@ -46,14 +50,15 @@ exit_error() {
 
 usage() {
     echo "Usage: $0 [-p <podman|docker|systemd>] [-x] [-d <output-dir>]" >&2
-    echo "    -p    platform to use (one of: podman, docker, systemd)" >&2
+    echo "    -p    the platform to use: podman, docker, systemd (default: ${SOURCE_PLATFORM})" >&2
+    echo "    -n    target namespace (default: ${SOURCE_NAMESPACE})" >&2
     echo "    -x    remove existing site definition" >&2
-    echo "    -d    dump tokens from bundle to the provide output directory" >&2
+    echo "    -d    dump static links from bundle into the provide output directory" >&2
     exit 1
 }
 
 parse_opts() {
-    while getopts "xhd:p:" opt; do
+    while getopts "xhd:p:n:" opt; do
         case "${opt}" in
             p)
                 valid_platforms="podman docker systemd"
@@ -69,17 +74,12 @@ parse_opts() {
                     echo "Invalid platform: ${SKUPPER_PLATFORM}"
                     usage
                 fi
-                if [ "${SKUPPER_PLATFORM}" = "docker" ]; then
-                    export PLATFORM_COMMAND="docker"
-                    if [ "${UID}" -ne 0 ]; then
-                        # shellcheck disable=SC2090
-                        export USERNS="host"
-                    fi
-                    docker_gid=$(getent group docker | cut -d: -f3)
-                    export RUNAS="${UID}:${docker_gid}"
-                elif [ "${SKUPPER_PLATFORM}" = "systemd" ]; then
-                    export PLATFORM_COMMAND="skrouterd"
+                ;;
+            n)
+                if [ -n "${OPTARG}" ]; then
+                    export NAMESPACE="${OPTARG}"
                 fi
+                export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/state/platform.yaml"
                 ;;
             d)
                 export DUMP_TOKENS=true
@@ -89,7 +89,7 @@ parse_opts() {
                     usage
                 fi
                 if [ ! -w "${SKUPPER_TOKEN_OUTPUT_PATH}" ]; then
-                    echo "No permission to write tokens to provided output directory"
+                    echo "No permission to write static links into the provided output directory"
                     usage
                 fi
                 ;;
@@ -104,15 +104,28 @@ parse_opts() {
                 ;;
         esac
     done
+
+    if [ "${SKUPPER_PLATFORM}" = "docker" ]; then
+        export PLATFORM_COMMAND="docker"
+        if [ "${UID}" -ne 0 ]; then
+            # shellcheck disable=SC2090
+            export USERNS="host"
+        fi
+        docker_gid=$(getent group docker | cut -d: -f3)
+        export RUNAS="${UID}:${docker_gid}"
+    elif [ "${SKUPPER_PLATFORM}" = "systemd" ]; then
+        export PLATFORM_COMMAND="skrouterd"
+    fi
+
 }
 
 create_service() {
     # if systemd is not available, skip it
     ${SYSTEMCTL} list-units > /dev/null 2>&1 || return
-    service_name="skupper-site-${SITE_NAME}.service"
+    service_name="skupper-${NAMESPACE}.service"
     service_file_suffix="container"
     [ "${SKUPPER_PLATFORM}" = "systemd" ] && service_file_suffix="systemd"
-    service_file="${SKUPPER_OUTPUT_PATH}/sites/${SITE_NAME}/runtime/scripts/${service_name}.${service_file_suffix}"
+    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${NAMESPACE}/runtime/scripts/skupper.service.${service_file_suffix}"
 
     if [ ! -f "${service_file}" ]; then
         echo "SystemD service has not been defined"
@@ -124,8 +137,7 @@ create_service() {
         cp "${service_file}" "/etc/systemd/system/${service_name}"
     else
         if [ ! -d "${SERVICE_DIR}" ]; then
-            echo "Unable to define path to SystemD service"
-            return
+            mkdir -p "${SERVICE_DIR}"
         fi
         cp "${service_file}" "${SERVICE_DIR}/${service_name}"
     fi
@@ -135,50 +147,56 @@ create_service() {
 }
 
 remove_service() {
-    service="skupper-site-${SITE_NAME}.service"
-    ${SYSTEMCTL} stop ${service}
-    ${SYSTEMCTL} disable ${service}
+    service="skupper-${NAMESPACE}.service"
+    ${SYSTEMCTL} stop "${service}"
+    ${SYSTEMCTL} disable "${service}"
     rm -f "${SERVICE_DIR:?}/${service}"
     ${SYSTEMCTL} daemon-reload
     ${SYSTEMCTL} reset-failed
 }
 
 remove_site() {
-    if [ ! -d "${SITES_PATH}/${SITE_NAME}" ]; then
-        echo "Site \"${SITE_NAME}\" does not exist"
+    if [ ! -d "${NAMESPACES_PATH}/${NAMESPACE}" ]; then
+        echo "Namespace \"${NAMESPACE}\" does not exist"
+        exit 0
+    fi
+
+    SITE_PATH="${NAMESPACES_PATH}/${NAMESPACE}/runtime/state/site/"
+    if ! grep -qr "name: ${SITE_NAME}\$" "${SITE_PATH}"; then
+        echo "Site \"${SITE_NAME}\" is not defined on namespace \"${NAMESPACE}\""
         exit 0
     fi
 
-    echo "Removing Skupper site definition for: ${SITE_NAME}"
+    echo "Removing Skupper site definition for ${SITE_NAME} from namespace ${NAMESPACE}"
     SKUPPER_PLATFORM=$(grep '^platform: ' "${PLATFORM_FILE}" | sed -e 's/.*: //g')
     if [ "${SKUPPER_PLATFORM}" != "systemd" ]; then
         # removing router container
-        ${SKUPPER_PLATFORM} rm -f ${SITE_NAME}-skupper-router
+        ${SKUPPER_PLATFORM} rm -f "${NAMESPACE}-skupper-router"
     fi
     # removing site definition
-    rm -rf "${SITES_PATH:?}/${SITE_NAME:?}/"
+    rm -rf "${NAMESPACES_PATH:?}/${NAMESPACE:?}/"
 }
 
 dump_tokens() {
-    tokens="$(ls "./${SITE_NAME}/runtime/token/")"
+    tokens="$(ls "./${SOURCE_NAMESPACE}/runtime/link/")"
     if [ -z "${tokens}" ]; then
-        echo "No tokens defined in this site bundle"
+        echo "No static links defined in this site bundle"
         return
     fi
     token_out_dir="${SKUPPER_TOKEN_OUTPUT_PATH:?}/${SITE_NAME:?}"
     mkdir -p "${token_out_dir}"
-    cp "./${SITE_NAME}/runtime/token/"*.yaml "${token_out_dir}"
-    echo "Tokens for site \"${SITE_NAME}\" have been saved into ${token_out_dir}"
+    cp "./${SOURCE_NAMESPACE}/runtime/link/"*.yaml "${token_out_dir}"
+    echo "Static links for site \"${SITE_NAME}\" have been saved into ${token_out_dir}"
 }
 
 set_router_access_port() {
-    config_file="${SITES_PATH}/${SITE_NAME}/config/router/skrouterd.json"
-    python "${SITES_PATH:?}/${SITE_NAME:?}/runtime/scripts/router_free_port.py" "${config_file}"
+    config_file="${NAMESPACES_PATH}/${NAMESPACE}/config/router/skrouterd.json"
+    python "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/scripts/router_free_port.py" "${config_file}"
 }
 
 create_sed_script() {
     ssl_profile_base_path="\${SSL_PROFILE_BASE_PATH}"
-    [ "${SKUPPER_PLATFORM}" = "systemd" ] && ssl_profile_base_path="${SITES_PATH}/${SITE_NAME}"
+    [ "${SKUPPER_PLATFORM}" = "systemd" ] && ssl_profile_base_path="${NAMESPACES_PATH}/${NAMESPACE}"
     container_engine="podman"
     [ "${SKUPPER_PLATFORM}" != "systemd" ] && container_engine="${SKUPPER_PLATFORM}"
     # populating sed script
@@ -187,53 +205,58 @@ create_sed_script() {
     # shellcheck disable=SC1083
     {
         echo "s#{{"{{"}}.SiteNameSuffix{{"}}"}}#${SITE_ID}#g"
+        echo "s#{{"{{"}}.Namespace{{"}}"}}#${NAMESPACE}#g"
         echo "s#{{"{{"}}.Platform{{"}}"}}#${SKUPPER_PLATFORM}#g"
         echo "s#{{"{{"}}.SslProfileBasePath{{"}}"}}#${ssl_profile_base_path}#g"
         echo "s#{{"{{"}}.ContainerEngine{{"}}"}}#${container_engine}#g"
         echo "s#{{"{{"}}.SiteId{{"}}"}}#${SITE_ID}#g"
-        echo "s#{{"{{"}}.SitesPath{{"}}"}}#${SITES_PATH}#g"
+        echo "s#{{"{{"}}.NamespacesPath{{"}}"}}#${NAMESPACES_PATH}#g"
         echo "s#{{"{{"}}.RuntimeDir{{"}}"}}#${RUNTIME_DIR}#g"
-        echo "s#{{"{{"}}.SiteScriptPath{{"}}"}}#${SITES_PATH}/${SITE_NAME}/runtime/scripts#g"
-        echo "s#{{"{{"}}.SiteConfigPath{{"}}"}}#${SITES_PATH}/${SITE_NAME}/config/router#g"
+        echo "s#{{"{{"}}.SiteScriptPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/runtime/scripts#g"
+        echo "s#{{"{{"}}.SiteConfigPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/config/router#g"
         echo "s#{{"{{"}}.RunAs{{"}}"}}#${RUNAS}#g"
         echo "s#{{"{{"}}.UserNamespace{{"}}"}}#${USERNS}#g"
     } >> script.sed
 }
 
 render_sed_templates() {
-    grep -rl '{{"{{"}}' "${SITES_PATH}/${SITE_NAME}" | xargs -r -i sed -f script.sed -i {}
+    grep -rl '{{"{{"}}' "${NAMESPACES_PATH}/${NAMESPACE}" | xargs -r -i sed -f script.sed -i {}
     rm -f script.sed
 }
 
 show_token_info() {
-    token_path="${SITES_PATH:?}/${SITE_NAME:?}/runtime/token"
+    token_path="${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/link"
     [ ! -d "${token_path}" ] && return
     tokens_found=$(find "${token_path}" -type f | wc -l)
     if [ "${tokens_found}" -gt 0 ]; then
-        echo "Static tokens have been defined at: ${token_path}"
+        echo "Static links have been defined at: ${token_path}"
     fi
 }
 
 create_containers() {
     [ "${SKUPPER_PLATFORM}" = "systemd" ] && return
-    "${SITES_PATH:?}/${SITE_NAME:?}/runtime/scripts/containers_create.sh"
+    "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/scripts/containers_create.sh"
 }
 
 create_site() {
-    if [ -d "${SITES_PATH}/${SITE_NAME}" ]; then
-        echo "Failed: site \"${SITE_NAME}\" is already defined"
+    if [ -d "${NAMESPACES_PATH}/${NAMESPACE}" ]; then
+        echo "Failed: namespace \"${NAMESPACE}\" is already defined"
+        echo "Location: ${NAMESPACES_PATH}/${NAMESPACE}"
         exit 1
     fi
 
     echo "Skupper site bundle installation"
-    echo "Site name: ${SITE_NAME}"
-    echo "Platform : ${SKUPPER_PLATFORM}"
+    echo "Namespace : ${NAMESPACE}"
+    echo "Site name : ${SITE_NAME}"
+    echo "Platform  : ${SKUPPER_PLATFORM}"
+    echo "Definition: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/sources"
+    echo "Version   : ${VERSION}"
 
     # Create base directory tree
-    mkdir -p "${SITES_PATH}"
+    mkdir -p "${NAMESPACES_PATH}"
 
     # Installing site definition files
-    cp -rf "./${SITE_NAME}" "${SITES_PATH}/${SITE_NAME}"
+    cp -rf "./${SOURCE_NAMESPACE}" "${NAMESPACES_PATH}/${NAMESPACE}"
 
     # Creating platform.yaml
     echo "platform: ${SKUPPER_PLATFORM}" > "${PLATFORM_FILE}"
@@ -254,11 +277,11 @@ create_site() {
     # Create systemd service
     create_service
 
-    echo "Site \"${SITE_NAME}\" is now running"
+    echo "Site \"${SITE_NAME}\" is now running on namespace \"${NAMESPACE}\""
 }
 
 sanity_check() {
-    required_fields="SITE_NAME SKUPPER_OUTPUT_PATH SERVICE_DIR SITES_PATH SKUPPER_PLATFORM"
+    required_fields="SITE_NAME SOURCE_NAMESPACE NAMESPACE SKUPPER_OUTPUT_PATH SERVICE_DIR NAMESPACES_PATH SKUPPER_PLATFORM"
     required_commands="python sed find grep xargs tar getent id ${PLATFORM_COMMAND}"
 
     for field_name in ${required_fields}; do
diff --git a/internal/nonkube/bundle/tarball.go b/internal/nonkube/bundle/tarball.go
index 9e4d5e7d..031bcb2e 100644
--- a/internal/nonkube/bundle/tarball.go
+++ b/internal/nonkube/bundle/tarball.go
@@ -8,24 +8,30 @@ import (
 	"text/template"
 
 	"github.com/skupperproject/skupper/internal/utils"
+	pkgutils "github.com/skupperproject/skupper/pkg/utils"
+	"github.com/skupperproject/skupper/pkg/version"
 )
 
 type TarballBundle struct {
 	SiteName   string
 	OutputPath string
+	Namespace  string
 }
 
 func (s *TarballBundle) InstallFile() string {
 	return path.Join(s.OutputPath, fmt.Sprintf("skupper-install-%s.tar.gz", s.SiteName))
 }
 
-func (s *TarballBundle) Generate(tarBall *utils.Tarball) error {
+func (s *TarballBundle) Generate(tarBall *utils.Tarball, defaultPlatform string) error {
 	var err error
 
 	installScriptTemplate := template.Must(template.New("install").Parse(installScript))
 	var parsedInstallScript = new(bytes.Buffer)
 	err = installScriptTemplate.Execute(parsedInstallScript, map[string]interface{}{
 		"SiteName":        s.SiteName,
+		"Namespace":       s.Namespace,
+		"Platform":        pkgutils.DefaultStr(defaultPlatform, "podman"),
+		"Version":         version.Version,
 		"SelfExtractPart": "",
 	})
 	if err != nil {
diff --git a/internal/nonkube/client/compat/client.go b/internal/nonkube/client/compat/client.go
index 4dec4795..28179c5b 100644
--- a/internal/nonkube/client/compat/client.go
+++ b/internal/nonkube/client/compat/client.go
@@ -205,8 +205,8 @@ func asSlice[T any](i interface{}) []T {
 	v := reflect.ValueOf(i)
 	s := make([]T, 0)
 	if v.Kind() == reflect.Slice {
-		for _, vv := range v.Interface().([]T) {
-			s = append(s, vv)
+		for _, vv := range v.Interface().([]interface{}) {
+			s = append(s, vv.(T))
 		}
 	}
 	return s
diff --git a/internal/nonkube/client/compat/container_mock.go b/internal/nonkube/client/compat/container_mock.go
index 8a918524..931c3206 100644
--- a/internal/nonkube/client/compat/container_mock.go
+++ b/internal/nonkube/client/compat/container_mock.go
@@ -177,7 +177,13 @@ func (r *RestClientMock) HandleContainerList(operation *runtime.ClientOperation,
 								"IPPrefixLen": json.Number(strconv.Itoa(containerNetwork.IPPrefixLen)),
 								"MacAddress":  containerNetwork.MacAddress,
 								"Gateway":     containerNetwork.Gateway,
-								"Aliases":     containerNetwork.Aliases,
+								"Aliases": func(aliases []string) []interface{} {
+									aliasesI := make([]interface{}, len(aliases))
+									for i, alias := range aliases {
+										aliasesI[i] = alias
+									}
+									return aliasesI
+								}(containerNetwork.Aliases),
 							}
 						}
 						return containerNetworks
diff --git a/internal/utils/files.go b/internal/utils/files.go
new file mode 100644
index 00000000..0be97bb2
--- /dev/null
+++ b/internal/utils/files.go
@@ -0,0 +1,43 @@
+package utils
+
+import (
+	"fmt"
+	"os"
+	"path"
+)
+
+type FilenameFilter func(string) bool
+type DirectoryReader struct{}
+
+func (f *DirectoryReader) ReadDir(dirname string, filter FilenameFilter) ([]string, error) {
+	dir, err := os.Open(dirname)
+	if err != nil {
+		return nil, err
+	}
+	dirInfo, err := dir.Stat()
+	if err != nil {
+		return nil, err
+	}
+	if !dirInfo.IsDir() {
+		return nil, fmt.Errorf("%s is not a directory", dirname)
+	}
+	files, err := dir.ReadDir(0)
+	if err != nil {
+		return nil, err
+	}
+	var fileNames []string
+	for _, file := range files {
+		if file.IsDir() {
+			recursiveFiles, err := f.ReadDir(path.Join(dirname, file.Name()), filter)
+			if err != nil {
+				return nil, err
+			}
+			fileNames = append(fileNames, recursiveFiles...)
+		} else {
+			if filter == nil || filter(file.Name()) {
+				fileNames = append(fileNames, path.Join(dirname, file.Name()))
+			}
+		}
+	}
+	return fileNames, nil
+}
diff --git a/internal/utils/tarball.go b/internal/utils/tarball.go
index 669ab0fb..27d03db6 100644
--- a/internal/utils/tarball.go
+++ b/internal/utils/tarball.go
@@ -175,8 +175,7 @@ func (t *Tarball) AddFileData(fileName string, mode int64, data []byte) error {
 	return nil
 }
 
-// Extract extracts the given tar.gz filename into the provided outputPath
-func (t *Tarball) Extract(fileName, outputPath string) error {
+func (t *Tarball) validateOutputPathoutputPath(outputPath string) error {
 	// Validating outputPath
 	if outputPath == "" {
 		return fmt.Errorf("outputPath is empty")
@@ -190,12 +189,33 @@ func (t *Tarball) Extract(fileName, outputPath string) error {
 	} else if !outputPathStat.Mode().IsDir() {
 		return fmt.Errorf("outputPath is not a directory")
 	}
+	return nil
+}
+
+func (t *Tarball) ExtractData(data []byte, outputPath string) error {
+	err := t.validateOutputPathoutputPath(outputPath)
+	if err != nil {
+		return err
+	}
+	reader := bytes.NewReader(data)
+	return t.extract(reader, outputPath)
+}
 
+// Extract extracts the given tar.gz filename into the provided outputPath
+func (t *Tarball) Extract(fileName, outputPath string) error {
+	err := t.validateOutputPathoutputPath(outputPath)
+	if err != nil {
+		return err
+	}
 	tgzFile, err := os.Open(fileName)
 	if err != nil {
 		return err
 	}
-	gzipReader, err := gzip.NewReader(tgzFile)
+	return t.extract(tgzFile, outputPath)
+}
+
+func (t *Tarball) extract(tgzReader io.Reader, outputPath string) error {
+	gzipReader, err := gzip.NewReader(tgzReader)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/config/platform.go b/pkg/config/platform.go
index e0f44cc1..67dc8909 100644
--- a/pkg/config/platform.go
+++ b/pkg/config/platform.go
@@ -132,13 +132,31 @@ var (
 	Platform string
 )
 
+// GetPlatform returns the runtime platform defined,
+// where the lookup goes through the following sequence:
+// - Platform variable,
+// - SKUPPER_PLATFORM environment variable
+// - Static platform defined by skupper switch
+// - Default platform "kubernetes" otherwise.
+// In case the defined platform is invalid, "kubernetes"
+// will be returned.
 func GetPlatform() types.Platform {
 	p := &PlatformInfo{}
 	_ = p.Load()
-	return types.Platform(utils.DefaultStr(Platform,
+	platform := types.Platform(utils.DefaultStr(Platform,
 		os.Getenv(types.ENV_PLATFORM),
 		string(p.Current),
 		string(types.PlatformKubernetes)))
+	switch platform {
+	case types.PlatformPodman:
+		return types.PlatformPodman
+	case types.PlatformDocker:
+		return types.PlatformDocker
+	case types.PlatformSystemd:
+		return types.PlatformSystemd
+	default:
+		return types.PlatformKubernetes
+	}
 }
 
 func getDataHome() string {
diff --git a/pkg/nonkube/api/environment.go b/pkg/nonkube/api/environment.go
new file mode 100644
index 00000000..ac41b26b
--- /dev/null
+++ b/pkg/nonkube/api/environment.go
@@ -0,0 +1,187 @@
+package api
+
+import (
+	"fmt"
+	"os"
+	"path"
+
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+)
+
+type InternalPath string
+type InternalPathProvider func(namespace string, internalPath InternalPath) string
+
+const (
+	ConfigRouterPath       InternalPath = "config/router"
+	CertificatesCaPath     InternalPath = "certificates/ca"
+	CertificatesClientPath InternalPath = "certificates/client"
+	CertificatesServerPath InternalPath = "certificates/server"
+	CertificatesLinkPath   InternalPath = "certificates/link"
+	LoadedSiteStatePath    InternalPath = "sources"
+	RuntimeSiteStatePath   InternalPath = "runtime/state"
+	RuntimeTokenPath       InternalPath = "runtime/link"
+	RuntimeScriptsPath     InternalPath = "runtime/scripts"
+)
+
+type IdGetter func() int
+
+var getUid IdGetter = os.Getuid
+
+func GetDataHome() string {
+	if getUid() == 0 {
+		return "/usr/local/share/skupper"
+	}
+	dataHome, ok := os.LookupEnv("XDG_DATA_HOME")
+	if !ok {
+		homeDir, _ := os.UserHomeDir()
+		dataHome = homeDir + "/.local/share"
+	}
+	return path.Join(dataHome, "skupper")
+}
+
+func GetConfigHome() string {
+	configHome, ok := os.LookupEnv("XDG_CONFIG_HOME")
+	if !ok {
+		homeDir, _ := os.UserHomeDir()
+		return homeDir + "/.config"
+	} else {
+		return configHome
+	}
+}
+
+func GetRuntimeDir() string {
+	if getUid() == 0 {
+		return "/run"
+	}
+	runtimeDir, ok := os.LookupEnv("XDG_RUNTIME_DIR")
+	if !ok {
+		runtimeDir = fmt.Sprintf("/run/user/%d", getUid())
+	}
+	return runtimeDir
+}
+
+// GetHostDataHome returns the value of the SKUPPER_OUTPUT_PATH environment
+// variable when running via container or the result of GetDataHome() otherwise.
+// This is only useful during the bootstrap process.
+func GetHostDataHome() (string, error) {
+	// If container provides SKUPPER_OUTPUT_PATH use it
+	if os.Getenv("SKUPPER_OUTPUT_PATH") != "" {
+		return os.Getenv("SKUPPER_OUTPUT_PATH"), nil
+	}
+	return GetDataHome(), nil
+}
+
+func GetHostSiteHome(site *v1alpha1.Site) (string, error) {
+	dataHome, err := GetHostDataHome()
+	if err != nil {
+		return "", err
+	}
+	ns := site.Namespace
+	if ns == "" {
+		ns = "default"
+	}
+	return path.Join(dataHome, "namespaces", ns), nil
+}
+
+func GetHostNamespacesPath() (string, error) {
+	return getHostPath("namespaces")
+}
+
+func GetHostBundlesPath() (string, error) {
+	return getHostPath("bundles")
+}
+
+func getHostPath(basePath string) (string, error) {
+	dataHome, err := GetHostDataHome()
+	if err != nil {
+		return "", err
+	}
+	return path.Join(dataHome, basePath), nil
+}
+
+func GetCustomSiteHome(site *v1alpha1.Site, customBaseDir string) string {
+	return getCustomSiteHome(site, customBaseDir, "namespaces")
+}
+
+func GetCustomBundleHome(site *v1alpha1.Site, customBaseDir string) string {
+	return getCustomSiteHome(site, customBaseDir, "bundles")
+}
+
+func getCustomSiteHome(site *v1alpha1.Site, customBaseDir string, basePath string) string {
+	ns := site.Namespace
+	if ns == "" {
+		ns = "default"
+	}
+	return path.Join(customBaseDir, basePath, ns)
+}
+
+func GetHostSiteInternalPath(site *v1alpha1.Site, internalPath InternalPath) (string, error) {
+	dataHome, err := GetHostSiteHome(site)
+	if err != nil {
+		return "", err
+	}
+	return path.Join(dataHome, string(internalPath)), nil
+}
+
+func IsRunningInContainer() bool {
+	// See: https://docs.podman.io/en/latest/markdown/podman-run.1.html
+	for _, file := range []string{"/run/.containerenv", "/.dockerenv"} {
+		if _, err := os.Stat(file); err == nil {
+			return true
+		}
+	}
+	return false
+}
+
+func GetDefaultOutputPath(namespace string) string {
+	return getDefaultOutputPath(namespace, false)
+}
+
+func GetDefaultBundleOutputPath(namespace string) string {
+	return getDefaultOutputPath(namespace, true)
+}
+
+func getDefaultOutputPath(namespace string, isBundle bool) string {
+	basePath := "namespaces"
+	if isBundle {
+		basePath = "bundles"
+	}
+	if namespace == "" {
+		namespace = "default"
+	}
+	if IsRunningInContainer() {
+		outputStat, err := os.Stat("/output")
+		if err == nil && outputStat.IsDir() {
+			return path.Join("/output", basePath, namespace)
+		}
+	}
+	return path.Join(GetDataHome(), basePath, namespace)
+}
+
+func GetDefaultOutputNamespacesPath() string {
+	if IsRunningInContainer() {
+		outputStat, err := os.Stat("/output")
+		if err == nil && outputStat.IsDir() {
+			return path.Join("/output", "namespaces")
+		}
+	}
+	return path.Join(GetDataHome(), "namespaces")
+}
+
+func GetDefaultOutputBundlesPath() string {
+	if IsRunningInContainer() {
+		outputStat, err := os.Stat("/output")
+		if err == nil && outputStat.IsDir() {
+			return path.Join("/output", "bundles")
+		}
+	}
+	return path.Join(GetDataHome(), "bundles")
+}
+
+func GetInternalOutputPath(namespace string, internalPath InternalPath) string {
+	return path.Join(GetDefaultOutputPath(namespace), string(internalPath))
+}
+
+func GetInternalBundleOutputPath(namespace string, internalPath InternalPath) string {
+	return path.Join(GetDefaultBundleOutputPath(namespace), string(internalPath))
+}
diff --git a/pkg/nonkube/apis/site_state.go b/pkg/nonkube/api/site_state.go
similarity index 86%
rename from pkg/nonkube/apis/site_state.go
rename to pkg/nonkube/api/site_state.go
index 94966211..386a00af 100644
--- a/pkg/nonkube/apis/site_state.go
+++ b/pkg/nonkube/api/site_state.go
@@ -1,10 +1,11 @@
-package apis
+package api
 
 import (
 	encodingjson "encoding/json"
 	"fmt"
 	"os"
 	"path"
+	"time"
 
 	"github.com/google/uuid"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
@@ -20,7 +21,7 @@ import (
 )
 
 type StaticSiteStateRenderer interface {
-	Render(state *SiteState) error
+	Render(state *SiteState, reload bool) error
 }
 
 type SiteState struct {
@@ -54,6 +55,14 @@ func NewSiteState(bundle bool) *SiteState {
 	}
 }
 
+func (s *SiteState) GetNamespace() string {
+	ns := s.Site.GetNamespace()
+	if ns == "" {
+		return "default"
+	}
+	return ns
+}
+
 func (s *SiteState) IsBundle() bool {
 	return s.bundle
 }
@@ -83,7 +92,8 @@ func (s *SiteState) CreateRouterAccess(name string, port int) {
 			Kind:       "RouterAccess",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: name,
+			Name:      name,
+			Namespace: s.GetNamespace(),
 		},
 		Spec: v1alpha1.RouterAccessSpec{
 			Roles: []v1alpha1.RouterAccessRole{
@@ -97,6 +107,7 @@ func (s *SiteState) CreateRouterAccess(name string, port int) {
 			Issuer:         tlsCaName,
 		},
 	}
+	s.RouterAccesses[name].SetConfigured(nil)
 	s.Certificates[tlsCaName] = s.newCertificate(tlsCaName, &v1alpha1.CertificateSpec{
 		Subject: tlsCaName,
 		Hosts:   []string{"127.0.0.1", "localhost"},
@@ -160,6 +171,7 @@ func (s *SiteState) CreateLinkAccessesCertificates() {
 			Subject: clientCertificateName,
 			Client:  true,
 		})
+		linkAccess.SetConfigured(nil)
 	}
 
 }
@@ -199,9 +211,15 @@ func (s *SiteState) newCertificate(name string, spec *v1alpha1.CertificateSpec)
 			Kind:       "Certificate",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: name,
+			Name:      name,
+			Namespace: s.GetNamespace(),
 		},
 		Spec: *spec,
+		Status: v1alpha1.CertificateStatus{
+			Status: v1alpha1.Status{
+				StatusMessage: v1alpha1.STATUS_OK,
+			},
+		},
 	}
 }
 
@@ -216,6 +234,7 @@ func (s *SiteState) linkMap(sslProfileBasePath string) site.LinkMap {
 	linkMap := site.LinkMap{}
 	for name, link := range s.Links {
 		siteLink := site.NewLink(name, path.Join(sslProfileBasePath, "certificates/link"))
+		link.SetConfigured(nil)
 		siteLink.Update(link)
 		linkMap[name] = siteLink
 	}
@@ -225,9 +244,11 @@ func (s *SiteState) linkMap(sslProfileBasePath string) site.LinkMap {
 func (s *SiteState) bindings() *site.Bindings {
 	b := site.NewBindings()
 	for name, connector := range s.Connectors {
+		connector.SetConfigured(nil)
 		_ = b.UpdateConnector(name, connector)
 	}
 	for name, listener := range s.Listeners {
+		listener.SetConfigured(nil)
 		_ = b.UpdateListener(name, listener)
 	}
 	return b
@@ -237,7 +258,11 @@ func (s *SiteState) ToRouterConfig(sslProfileBasePath string) qdr.RouterConfig {
 	if s.SiteId == "" {
 		s.SiteId = uuid.New().String()
 	}
-	routerConfig := qdr.InitialConfig(s.Site.Name, s.SiteId, version.Version, !s.IsInterior(), 3)
+	var routerName = s.Site.Name
+	if !s.bundle {
+		routerName = fmt.Sprintf("%s-%d", s.Site.Name, time.Now().Unix())
+	}
+	routerConfig := qdr.InitialConfig(routerName, s.SiteId, version.Version, !s.IsInterior(), 3)
 	// override metadata
 	if s.bundle {
 		routerConfig.Metadata.Id += "-{{.SiteNameSuffix}}"
@@ -260,6 +285,33 @@ func (s *SiteState) ToRouterConfig(sslProfileBasePath string) qdr.RouterConfig {
 
 	return routerConfig
 }
+
+func setNamespaceOnMap[T metav1.Object](objMap map[string]T, namespace string) {
+	for _, obj := range objMap {
+		obj.SetNamespace(namespace)
+	}
+}
+
+func (s *SiteState) SetNamespace(namespace string) {
+	if namespace == "" {
+		namespace = "default"
+	}
+	// equals
+	if s.GetNamespace() == namespace {
+		return
+	}
+	s.Site.SetNamespace(namespace)
+	setNamespaceOnMap(s.Listeners, namespace)
+	setNamespaceOnMap(s.Connectors, namespace)
+	setNamespaceOnMap(s.RouterAccesses, namespace)
+	setNamespaceOnMap(s.Grants, namespace)
+	setNamespaceOnMap(s.Links, namespace)
+	setNamespaceOnMap(s.Secrets, namespace)
+	setNamespaceOnMap(s.Claims, namespace)
+	setNamespaceOnMap(s.Certificates, namespace)
+	setNamespaceOnMap(s.SecuredAccesses, namespace)
+}
+
 func marshal(outputDirectory, resourceType, resourceName string, resource interface{}) error {
 	var err error
 	writeDirectory := path.Join(outputDirectory, resourceType)
diff --git a/pkg/nonkube/api/token.go b/pkg/nonkube/api/token.go
new file mode 100644
index 00000000..8dcf575a
--- /dev/null
+++ b/pkg/nonkube/api/token.go
@@ -0,0 +1,112 @@
+package api
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"strconv"
+
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/utils"
+	"k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/serializer/json"
+	"k8s.io/client-go/kubernetes/scheme"
+)
+
+type Token struct {
+	Links  []*v1alpha1.Link
+	Secret *v1.Secret
+}
+
+func (t *Token) Marshal() ([]byte, error) {
+	s := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme, json.SerializerOptions{Yaml: true})
+	buffer := new(bytes.Buffer)
+	writer := bufio.NewWriter(buffer)
+	_, _ = writer.Write([]byte("---\n"))
+	err := s.Encode(t.Secret, writer)
+	if err != nil {
+		return nil, err
+	}
+	for _, l := range t.Links {
+		_, _ = writer.Write([]byte("---\n"))
+		err = s.Encode(l, writer)
+		if err != nil {
+			return nil, err
+		}
+		if err = writer.Flush(); err != nil {
+			return nil, err
+		}
+	}
+	return buffer.Bytes(), nil
+}
+
+func CreateTokens(routerAccess v1alpha1.RouterAccess, secret v1.Secret) []*Token {
+	var tokens []*Token
+	interRouter := 0
+	edge := 0
+	for _, role := range routerAccess.Spec.Roles {
+		switch role.Name {
+		case "inter-router":
+			interRouter = role.Port
+		case "edge":
+			edge = role.Port
+		}
+	}
+	if interRouter == 0 && edge == 0 {
+		return nil
+	}
+	createToken := func(host string) *Token {
+		name := routerAccess.Name
+		linkName := fmt.Sprintf("link-%s", name)
+		// adjusting name to match the standard used by pkg/site/link.go
+		secret.Name = fmt.Sprintf("link-%s", name)
+		token := &Token{
+			Links: []*v1alpha1.Link{
+				{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "skupper.io/v1alpha1",
+						Kind:       "Link",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: linkName,
+					},
+					Spec: v1alpha1.LinkSpec{
+						TlsCredentials: secret.Name,
+						Cost:           1,
+					},
+				},
+			},
+			Secret: &secret,
+		}
+		var endpoints []v1alpha1.Endpoint
+		if interRouter > 0 {
+			endpoints = append(endpoints, v1alpha1.Endpoint{
+				Name:  "inter-router",
+				Host:  host,
+				Port:  strconv.Itoa(interRouter),
+				Group: "", // TODO ?
+			})
+		}
+		if edge > 0 {
+			endpoints = append(endpoints, v1alpha1.Endpoint{
+				Name:  "edge",
+				Host:  host,
+				Port:  strconv.Itoa(edge),
+				Group: "", // TODO ?
+			})
+		}
+		token.Links[0].Spec.Endpoints = endpoints
+		token.Secret.Namespace = ""
+		return token
+	}
+	var hosts []string
+	hosts = append(hosts, utils.DefaultStr(routerAccess.Spec.BindHost, "127.0.0.1"))
+	if len(routerAccess.Spec.SubjectAlternativeNames) > 0 {
+		hosts = append(hosts, routerAccess.Spec.SubjectAlternativeNames...)
+	}
+	for _, host := range hosts {
+		tokens = append(tokens, createToken(host))
+	}
+	return tokens
+}
diff --git a/pkg/nonkube/apis/environment.go b/pkg/nonkube/apis/environment.go
deleted file mode 100644
index bedf020a..00000000
--- a/pkg/nonkube/apis/environment.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package apis
-
-import (
-	"fmt"
-	"os"
-	"path"
-
-	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
-)
-
-type IdGetter func() int
-
-var getUid IdGetter = os.Getuid
-
-func GetDataHome() string {
-	if getUid() == 0 {
-		return "/usr/local/share/skupper"
-	}
-	dataHome, ok := os.LookupEnv("XDG_DATA_HOME")
-	if !ok {
-		homeDir, _ := os.UserHomeDir()
-		dataHome = homeDir + "/.local/share"
-	}
-	return path.Join(dataHome, "skupper")
-}
-
-func GetConfigHome() string {
-	configHome, ok := os.LookupEnv("XDG_CONFIG_HOME")
-	if !ok {
-		homeDir, _ := os.UserHomeDir()
-		return homeDir + "/.config"
-	} else {
-		return configHome
-	}
-}
-
-func GetRuntimeDir() string {
-	if getUid() == 0 {
-		return "/run"
-	}
-	runtimeDir, ok := os.LookupEnv("XDG_RUNTIME_DIR")
-	if !ok {
-		runtimeDir = fmt.Sprintf("/run/user/%d", getUid())
-	}
-	return runtimeDir
-}
-
-// GetHostDataHome returns the value of the SKUPPER_OUTPUT_PATH environment
-// variable when running via container or the result of GetDataHome() otherwise.
-// This is only useful during the bootstrap process.
-func GetHostDataHome() (string, error) {
-	// If container provides SKUPPER_OUTPUT_PATH use it
-	if os.Getenv("SKUPPER_OUTPUT_PATH") != "" {
-		return os.Getenv("SKUPPER_OUTPUT_PATH"), nil
-	}
-	return GetDataHome(), nil
-}
-
-func GetHostSiteHome(site *v1alpha1.Site) (string, error) {
-	dataHome, err := GetHostDataHome()
-	if err != nil {
-		return "", err
-	}
-	return path.Join(dataHome, "sites", site.Name), nil
-}
-
-func IsRunningInContainer() bool {
-	// See: https://docs.podman.io/en/latest/markdown/podman-run.1.html
-	for _, file := range []string{"/run/.containerenv", "/.dockerenv"} {
-		if _, err := os.Stat(file); err == nil {
-			return true
-		}
-	}
-	return false
-}
diff --git a/pkg/nonkube/apis/token.go b/pkg/nonkube/apis/token.go
deleted file mode 100644
index 0f8683f3..00000000
--- a/pkg/nonkube/apis/token.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package apis
-
-import (
-	"bufio"
-	"bytes"
-	"fmt"
-	"strconv"
-
-	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
-	"github.com/skupperproject/skupper/pkg/utils"
-	"k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime/serializer/json"
-	"k8s.io/client-go/kubernetes/scheme"
-)
-
-type Token struct {
-	Links  []*v1alpha1.Link
-	Secret *v1.Secret
-}
-
-func (t *Token) Marshal() ([]byte, error) {
-	s := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme, json.SerializerOptions{Yaml: true})
-	buffer := new(bytes.Buffer)
-	writer := bufio.NewWriter(buffer)
-	_, _ = writer.Write([]byte("---\n"))
-	err := s.Encode(t.Secret, writer)
-	if err != nil {
-		return nil, err
-	}
-	for _, l := range t.Links {
-		_, _ = writer.Write([]byte("---\n"))
-		err = s.Encode(l, writer)
-		if err != nil {
-			return nil, err
-		}
-		if err = writer.Flush(); err != nil {
-			return nil, err
-		}
-	}
-	return buffer.Bytes(), nil
-}
-
-func CreateToken(routerAccess *v1alpha1.RouterAccess, secret *v1.Secret) *Token {
-	interRouter := 0
-	edge := 0
-	for _, role := range routerAccess.Spec.Roles {
-		switch role.Name {
-		case "inter-router":
-			interRouter = role.Port
-		case "edge":
-			edge = role.Port
-		}
-	}
-	if interRouter == 0 && edge == 0 {
-		return nil
-	}
-	name := routerAccess.Name
-	linkName := fmt.Sprintf("link-%s", name)
-	// adjusting name to match the standard used by pkg/site/link.go
-	secret.Name = fmt.Sprintf("link-%s", name)
-	token := &Token{
-		Links: []*v1alpha1.Link{
-			{
-				TypeMeta: metav1.TypeMeta{
-					APIVersion: "skupper.io/v1alpha1",
-					Kind:       "Link",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name: linkName,
-				},
-				Spec: v1alpha1.LinkSpec{
-					TlsCredentials: secret.Name,
-					Cost:           1,
-				},
-			},
-		},
-		Secret: secret,
-	}
-	linkHost := utils.DefaultStr(routerAccess.Spec.BindHost, "127.0.0.1")
-	var endpoints []v1alpha1.Endpoint
-	if interRouter > 0 {
-		endpoints = append(endpoints, v1alpha1.Endpoint{
-			Name:  "inter-router",
-			Host:  linkHost,
-			Port:  strconv.Itoa(interRouter),
-			Group: "", // TODO ?
-		})
-	}
-	if edge > 0 {
-		endpoints = append(endpoints, v1alpha1.Endpoint{
-			Name:  "edge",
-			Host:  linkHost,
-			Port:  strconv.Itoa(edge),
-			Group: "", // TODO ?
-		})
-	}
-	token.Links[0].Spec.Endpoints = endpoints
-	return token
-}
diff --git a/pkg/nonkube/bundle/site_state_renderer.go b/pkg/nonkube/bundle/site_state_renderer.go
index f4bdea69..5cca695b 100644
--- a/pkg/nonkube/bundle/site_state_renderer.go
+++ b/pkg/nonkube/bundle/site_state_renderer.go
@@ -9,10 +9,9 @@ import (
 	"github.com/skupperproject/skupper/api/types"
 	internalbundle "github.com/skupperproject/skupper/internal/nonkube/bundle"
 	"github.com/skupperproject/skupper/internal/utils"
-	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/skupperproject/skupper/pkg/container"
 	"github.com/skupperproject/skupper/pkg/images"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/nonkube/common"
 )
 
@@ -22,15 +21,17 @@ var (
 )
 
 type SiteStateRenderer struct {
-	loadedSiteState *apis.SiteState
-	siteState       *apis.SiteState
+	loadedSiteState *api.SiteState
+	siteState       *api.SiteState
 	configRenderer  *common.FileSystemConfigurationRenderer
 	containers      map[string]container.Container
+	Strategy        internalbundle.BundleStrategy
+	Platform        types.Platform
 }
 
-func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
+func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool) error {
 	var err error
-	var validator apis.SiteStateValidator = &common.SiteStateValidator{}
+	var validator api.SiteStateValidator = &common.SiteStateValidator{}
 	err = validator.Validate(loadedSiteState)
 	if err != nil {
 		return err
@@ -53,7 +54,8 @@ func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
 	// rendering non-kube configuration files and certificates
 	s.configRenderer = &common.FileSystemConfigurationRenderer{
 		SslProfileBasePath: "{{.SslProfileBasePath}}",
-		Force:              false, // TODO discuss how this should be handled?
+		Platform:           string(s.Platform),
+		Bundle:             true,
 	}
 	err = s.configRenderer.Render(s.siteState)
 	if err != nil {
@@ -61,7 +63,7 @@ func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
 		os.Exit(1)
 	}
 	// Serializing loaded and runtime site states
-	if err = s.configRenderer.MarshalSiteStates(*s.loadedSiteState, *s.siteState); err != nil {
+	if err = s.configRenderer.MarshalSiteStates(s.loadedSiteState, s.siteState); err != nil {
 		return err
 	}
 
@@ -78,7 +80,7 @@ func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
 	if err = CreateSystemdServices(s.siteState); err != nil {
 		return err
 	}
-	if err = CreateStartupScripts(s.siteState); err != nil {
+	if err = CreateStartupScripts(s.siteState, s.Platform); err != nil {
 		return err
 	}
 	if err = s.createBundle(); err != nil {
@@ -93,7 +95,7 @@ func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
 func (s *SiteStateRenderer) prepareContainers() error {
 	s.containers = make(map[string]container.Container)
 	s.containers[types.RouterComponent] = container.Container{
-		Name:  fmt.Sprintf("%s-skupper-router", s.siteState.Site.Name),
+		Name:  "{{.Namespace}}-skupper-router",
 		Image: images.GetRouterImageName(),
 		Env: map[string]string{
 			"APPLICATION_NAME":      "skupper-router",
@@ -108,12 +110,12 @@ func (s *SiteStateRenderer) prepareContainers() error {
 		},
 		FileMounts: []container.FileMount{
 			{
-				Source:      path.Join("{{.SitesPath}}", s.siteState.Site.Name, "config/router"),
+				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", "config/router"),
 				Destination: "/etc/skupper-router/config",
 				Options:     []string{"z"},
 			},
 			{
-				Source:      path.Join("{{.SitesPath}}", s.siteState.Site.Name, "certificates"),
+				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", "certificates"),
 				Destination: "/etc/skupper-router/certificates",
 				Options:     []string{"z"},
 			},
@@ -128,16 +130,9 @@ func (s *SiteStateRenderer) prepareContainers() error {
 }
 
 func (s *SiteStateRenderer) createContainerScript() error {
-	siteHome, err := apis.GetHostSiteHome(s.siteState.Site)
-	if err != nil {
-		return err
-	}
-	scriptsPath := path.Join(siteHome, common.RuntimeScriptsPath)
-	if apis.IsRunningInContainer() {
-		scriptsPath = path.Join(common.GetDefaultOutputPath(s.siteState.Site.Name), common.RuntimeScriptsPath)
-	}
+	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.RuntimeScriptsPath)
 	scriptContent := containersToShell(s.containers)
-	err = os.WriteFile(path.Join(scriptsPath, "containers_create.sh"), scriptContent, 0755)
+	err := os.WriteFile(path.Join(scriptsPath, "containers_create.sh"), scriptContent, 0755)
 	if err != nil {
 		return fmt.Errorf("failed to create containers script: %v", err)
 	}
@@ -145,33 +140,29 @@ func (s *SiteStateRenderer) createContainerScript() error {
 }
 
 func (s *SiteStateRenderer) createBundle() error {
-	dataHome, err := apis.GetHostDataHome()
-	if err != nil {
-		return err
-	}
-	sitesHomeDir := path.Join(dataHome, "sites")
-	if apis.IsRunningInContainer() {
-		sitesHomeDir = path.Join("/output", "sites")
-	}
-	siteHomeDir := path.Join(sitesHomeDir, s.siteState.Site.Name)
+	bundlesHomeDir := api.GetDefaultOutputBundlesPath()
+	siteHomeDir := api.GetDefaultBundleOutputPath(s.siteState.Site.Namespace)
 	tarball := utils.NewTarball()
-	err = tarball.AddFiles(sitesHomeDir, s.siteState.Site.Name)
+	err := tarball.AddFiles(bundlesHomeDir, s.siteState.GetNamespace())
 	if err != nil {
 		return fmt.Errorf("failed to add files to tarball (%q): %v", siteHomeDir, err)
 	}
 	var generator internalbundle.BundleGenerator
-	if !config.GetPlatform().IsTarball() {
-		generator = &internalbundle.SelfExtractingBundle{
+	switch s.Strategy {
+	case internalbundle.BundleStrategyTarball:
+		generator = &internalbundle.TarballBundle{
 			SiteName:   s.siteState.Site.Name,
-			OutputPath: sitesHomeDir,
+			Namespace:  s.siteState.GetNamespace(),
+			OutputPath: bundlesHomeDir,
 		}
-	} else {
-		generator = &internalbundle.TarballBundle{
+	default:
+		generator = &internalbundle.SelfExtractingBundle{
 			SiteName:   s.siteState.Site.Name,
-			OutputPath: sitesHomeDir,
+			Namespace:  s.siteState.GetNamespace(),
+			OutputPath: bundlesHomeDir,
 		}
 	}
-	err = generator.Generate(tarball)
+	err = generator.Generate(tarball, string(s.Platform))
 	if err != nil {
 		return fmt.Errorf("failed to generate site bundle (%q): %v", s.siteState.Site.Name, err)
 	}
@@ -179,14 +170,8 @@ func (s *SiteStateRenderer) createBundle() error {
 }
 
 func (s *SiteStateRenderer) removeSiteFiles() error {
-	siteHomeDir, err := apis.GetHostSiteHome(s.siteState.Site)
-	if err != nil {
-		return err
-	}
-	if apis.IsRunningInContainer() {
-		siteHomeDir = path.Join("/output", "sites", s.siteState.Site.Name)
-	}
-	err = os.RemoveAll(siteHomeDir)
+	siteHomeDir := api.GetDefaultBundleOutputPath(s.siteState.Site.Namespace)
+	err := os.RemoveAll(siteHomeDir)
 	if err != nil {
 		return fmt.Errorf("file to remove temporary site directory %q: %v", siteHomeDir, err)
 	}
@@ -194,15 +179,8 @@ func (s *SiteStateRenderer) removeSiteFiles() error {
 }
 
 func (s *SiteStateRenderer) createFreePortScript() error {
-	siteHome, err := apis.GetHostSiteHome(s.siteState.Site)
-	if err != nil {
-		return err
-	}
-	scriptsPath := path.Join(siteHome, common.RuntimeScriptsPath)
-	if apis.IsRunningInContainer() {
-		scriptsPath = path.Join(common.GetDefaultOutputPath(s.siteState.Site.Name), common.RuntimeScriptsPath)
-	}
-	err = os.WriteFile(path.Join(scriptsPath, "router_free_port.py"), []byte(FreePortScript), 0755)
+	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.RuntimeScriptsPath)
+	err := os.WriteFile(path.Join(scriptsPath, "router_free_port.py"), []byte(FreePortScript), 0755)
 	if err != nil {
 		return fmt.Errorf("failed to create router_free_port.py script: %v", err)
 	}
diff --git a/pkg/nonkube/bundle/systemd.go b/pkg/nonkube/bundle/systemd.go
index 2f48314f..34b67396 100644
--- a/pkg/nonkube/bundle/systemd.go
+++ b/pkg/nonkube/bundle/systemd.go
@@ -8,30 +8,25 @@ import (
 	"path"
 	"text/template"
 
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/nonkube/common"
 )
 
-func CreateSystemdServices(siteState *apis.SiteState) error {
+func CreateSystemdServices(siteState *api.SiteState) error {
 	var err error
 	serviceTemplates := map[string]string{
 		"systemd":   common.SystemdServiceTemplate,
 		"container": common.SystemdContainerServiceTemplate,
 	}
-	siteHome, err := apis.GetHostSiteHome(siteState.Site)
-	if err != nil {
-		return err
-	}
-	scriptsPath := path.Join(siteHome, common.RuntimeScriptsPath)
-	if apis.IsRunningInContainer() {
-		scriptsPath = path.Join(common.GetDefaultOutputPath(siteState.Site.Name), common.RuntimeScriptsPath)
-	}
+	scriptsPath := api.GetInternalBundleOutputPath(siteState.Site.Namespace, api.RuntimeScriptsPath)
 	for platform, serviceTemplate := range serviceTemplates {
 		var buf = new(bytes.Buffer)
 		parsedTemplate := template.Must(template.New("service").Parse(serviceTemplate))
 		parsedTemplate.Option()
 		err = parsedTemplate.Execute(buf, map[string]interface{}{
 			"Site":           siteState.Site,
+			"Namespace":      "{{.Namespace}}",
 			"RuntimeDir":     "{{.RuntimeDir}}",
 			"SiteScriptPath": "{{.SiteScriptPath}}",
 			"SiteConfigPath": "{{.SiteConfigPath}}",
@@ -39,7 +34,7 @@ func CreateSystemdServices(siteState *apis.SiteState) error {
 		if err != nil {
 			return fmt.Errorf("failed to execute %s service template: %w", platform, err)
 		}
-		serviceFile := path.Join(scriptsPath, fmt.Sprintf("skupper-site-%s.service.%s", siteState.Site.Name, platform))
+		serviceFile := path.Join(scriptsPath, fmt.Sprintf("skupper.service.%s", platform))
 		err = os.WriteFile(serviceFile, buf.Bytes(), 0644)
 		if err != nil {
 			return fmt.Errorf("failed to write %s service file: %w", platform, err)
@@ -48,9 +43,15 @@ func CreateSystemdServices(siteState *apis.SiteState) error {
 	return nil
 }
 
-func CreateStartupScripts(siteState *apis.SiteState) error {
+func CreateStartupScripts(siteState *api.SiteState, platform types.Platform) error {
 	// Creating startup scripts first
-	scripts, err := common.GetStartupScripts(siteState.Site, "{{.SiteId}}")
+	startupArgs := common.StartupScriptsArgs{
+		Namespace: siteState.GetNamespace(),
+		SiteId:    "{{.SiteId}}",
+		Platform:  platform,
+		Bundle:    true,
+	}
+	scripts, err := common.GetStartupScripts(startupArgs, api.GetInternalBundleOutputPath)
 	if err != nil {
 		return fmt.Errorf("error getting startup scripts: %w", err)
 	}
diff --git a/pkg/nonkube/common/claim_redeemer.go b/pkg/nonkube/common/claim_redeemer.go
index d6b49fd9..45e84a35 100644
--- a/pkg/nonkube/common/claim_redeemer.go
+++ b/pkg/nonkube/common/claim_redeemer.go
@@ -10,7 +10,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 
 	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
 
@@ -18,7 +18,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/yaml"
 )
 
-func RedeemClaims(siteState *apis.SiteState) error {
+func RedeemClaims(siteState *api.SiteState) error {
 	var errs []error
 
 	for name, claim := range siteState.Claims {
@@ -37,7 +37,7 @@ func RedeemClaims(siteState *apis.SiteState) error {
 }
 
 // Redeem logic that populates siteState.Secrets and siteState.Links
-func redeemAccessToken(claim *skupperv1alpha1.AccessToken, siteState *apis.SiteState) error {
+func redeemAccessToken(claim *skupperv1alpha1.AccessToken, siteState *api.SiteState) error {
 	transport := &http.Transport{}
 	if claim.Spec.Ca != "" {
 		caPool := x509.NewCertPool()
diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go
index 744208ff..109fb345 100644
--- a/pkg/nonkube/common/fs_config_renderer.go
+++ b/pkg/nonkube/common/fs_config_renderer.go
@@ -10,37 +10,35 @@ import (
 	"strings"
 	"syscall"
 
+	"github.com/skupperproject/skupper/internal/utils"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
 	"github.com/skupperproject/skupper/pkg/certs"
-	"github.com/skupperproject/skupper/pkg/config"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/qdr"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-const (
-	ConfigRouterPath       = "config/router"
-	CertificatesCaPath     = "certificates/ca"
-	CertificatesClientPath = "certificates/client"
-	CertificatesServerPath = "certificates/server"
-	CertificatesLinkPath   = "certificates/link"
-	LoadedSiteStatePath    = "loaded/state"
-	RuntimeSiteStatePath   = "runtime/state"
-	RuntimeTokenPath       = "runtime/token"
-	RuntimeScriptsPath     = "runtime/scripts"
-)
-
 var (
-	configurationDirectories = []string{
-		ConfigRouterPath,
-		CertificatesCaPath,
-		CertificatesClientPath,
-		CertificatesServerPath,
-		CertificatesLinkPath,
-		LoadedSiteStatePath,
-		RuntimeSiteStatePath,
-		RuntimeTokenPath,
-		RuntimeScriptsPath,
+	configurationDirectories = []api.InternalPath{
+		api.ConfigRouterPath,
+		api.CertificatesCaPath,
+		api.CertificatesClientPath,
+		api.CertificatesServerPath,
+		api.CertificatesLinkPath,
+		api.LoadedSiteStatePath,
+		api.RuntimeSiteStatePath,
+		api.RuntimeTokenPath,
+		api.RuntimeScriptsPath,
+	}
+	reloadDirectories = []api.InternalPath{
+		api.ConfigRouterPath,
+		api.CertificatesClientPath,
+		api.CertificatesServerPath,
+		api.CertificatesLinkPath,
+		api.RuntimeSiteStatePath,
+		api.RuntimeTokenPath,
+		api.RuntimeScriptsPath,
 	}
 )
 
@@ -48,68 +46,46 @@ const (
 	DefaultSslProfileBasePath = "${SSL_PROFILE_BASE_PATH}"
 )
 
-func GetDefaultOutputPath(siteName string) string {
-	if apis.IsRunningInContainer() {
-		outputStat, err := os.Stat("/output")
-		if err == nil && outputStat.IsDir() {
-			return path.Join("/output", "sites", siteName)
-		}
-	}
-	return path.Join(apis.GetDataHome(), "sites", siteName)
-}
-
 type FileSystemConfigurationRenderer struct {
-	// OutputPath destination directory where configuration will be rendered into
-	OutputPath string
 	// SslProfileBasePath path where configuration will be read from in runtime
 	SslProfileBasePath string
-	// Force creation even if directory already exists
-	Force        bool
-	RouterConfig qdr.RouterConfig
+	RouterConfig       qdr.RouterConfig
+	Platform           string
+	Bundle             bool
+	customOutputPath   string
+}
+
+func NewFileSystemConfigurationRenderer(outputPath string) *FileSystemConfigurationRenderer {
+	return &FileSystemConfigurationRenderer{}
 }
 
 // Render simply renders the given site state as configuration files.
-func (c *FileSystemConfigurationRenderer) Render(siteState *apis.SiteState) error {
+func (c *FileSystemConfigurationRenderer) Render(siteState *api.SiteState) error {
 	var err error
-	// Set the default output path
-	if c.OutputPath == "" {
-		c.OutputPath = GetDefaultOutputPath(siteState.Site.Name)
-	} else {
-		_ = os.Setenv("SKUPPER_OUTPUT_PATH", c.OutputPath)
-	}
+	outputPath := c.GetOutputPath(siteState)
 	if c.SslProfileBasePath == "" {
 		c.SslProfileBasePath = DefaultSslProfileBasePath
 	}
 	// Proceed only if output path does not exist
-	outputDir, err := os.Open(c.OutputPath)
+	outputDir, err := os.Open(outputPath)
 	if err == nil {
 		defer outputDir.Close()
-		if !c.Force {
-			siteConfigPath, err := apis.GetHostSiteHome(siteState.Site)
-			if err != nil {
-				return err
-			}
-			_, err = os.Stat(siteConfigPath)
-			if err == nil {
-				return fmt.Errorf("site %s already exists (path: %s)", siteState.Site.Name, siteConfigPath)
-			}
-		}
 		outputDirStat, err := outputDir.Stat()
 		if err != nil {
-			return fmt.Errorf("failed to check if output directory exists (%s): %w", c.OutputPath, err)
+			return fmt.Errorf("failed to check if output directory exists (%s): %w", outputPath, err)
 		}
 		if !outputDirStat.IsDir() {
-			return fmt.Errorf("output path must be a directory (%s)", c.OutputPath)
+			return fmt.Errorf("output path must be a directory (%s)", outputPath)
 		}
 	} else {
 		var pathErr *os.PathError
 		if ok := errors.As(err, &pathErr); ok && !errors.Is(pathErr.Err, syscall.ENOENT) {
-			return fmt.Errorf("unable to use output path %s: %v", c.OutputPath, err)
+			return fmt.Errorf("unable to use output path %s: %v", outputPath, err)
 		}
 	}
 	// Creating internal configuration directories
 	for _, dir := range configurationDirectories {
-		configDir := path.Join(c.OutputPath, dir)
+		configDir := path.Join(outputPath, string(dir))
 		err := os.MkdirAll(configDir, 0755)
 		if err != nil && !os.IsExist(err) {
 			return fmt.Errorf("unable to create configuration directory %s: %v", configDir, err)
@@ -134,33 +110,124 @@ func (c *FileSystemConfigurationRenderer) Render(siteState *apis.SiteState) erro
 	}
 
 	// Saving runtime platform
-	platform := config.GetPlatform()
-	if platform == "kubernetes" {
-		platform = "podman"
-	}
-	if !platform.IsBundle() {
-		content := fmt.Sprintf("platform: %s\n", string(platform))
-		err = os.WriteFile(path.Join(c.OutputPath, RuntimeSiteStatePath, "platform.yaml"), []byte(content), 0644)
+	if !c.Bundle {
+		content := fmt.Sprintf("platform: %s\n", c.Platform)
+		err = os.WriteFile(path.Join(outputPath, string(api.RuntimeSiteStatePath), "platform.yaml"), []byte(content), 0644)
 		if err != nil {
 			return fmt.Errorf("failed to write runtime platform: %w", err)
 		}
 	}
-
+	siteState.Site.SetConfigured(nil)
+	endpoints := make([]v1alpha1.Endpoint, 0)
+	for raName, ra := range siteState.RouterAccesses {
+		for _, role := range ra.Spec.Roles {
+			if role.Name != "normal" {
+				endpoints = append(endpoints, v1alpha1.Endpoint{
+					Name: fmt.Sprintf("%s-%s", raName, role.Name),
+					Host: ra.Spec.BindHost,
+					Port: strconv.Itoa(role.Port),
+				})
+			}
+		}
+	}
+	siteState.Site.Status.Endpoints = endpoints
 	return nil
 }
 
-func (c *FileSystemConfigurationRenderer) MarshalSiteStates(loadedSiteState, runtimeSiteState apis.SiteState) error {
-	if err := apis.MarshalSiteState(loadedSiteState, path.Join(c.OutputPath, LoadedSiteStatePath)); err != nil {
-		return err
+func (c *FileSystemConfigurationRenderer) GetOutputPath(siteState *api.SiteState) string {
+	var customSiteHomeProvider = api.GetCustomSiteHome
+	var defaultOutputPathProvider = api.GetDefaultOutputPath
+	if siteState.IsBundle() {
+		customSiteHomeProvider = api.GetCustomBundleHome
+		defaultOutputPathProvider = api.GetDefaultBundleOutputPath
 	}
-	if err := apis.MarshalSiteState(runtimeSiteState, path.Join(c.OutputPath, RuntimeSiteStatePath)); err != nil {
+	if c.customOutputPath != "" {
+		return customSiteHomeProvider(siteState.Site, c.customOutputPath)
+	}
+	return defaultOutputPathProvider(siteState.Site.Namespace)
+}
+
+func (c *FileSystemConfigurationRenderer) MarshalSiteStates(loadedSiteState, runtimeSiteState *api.SiteState) error {
+	if loadedSiteState != nil {
+		outputPath := c.GetOutputPath(loadedSiteState)
+		sourcesPath := path.Join(outputPath, string(api.LoadedSiteStatePath))
+		existingSources, _ := new(utils.DirectoryReader).ReadDir(sourcesPath, nil)
+		// when sources are already defined, we back them up
+		if len(existingSources) > 0 {
+			tb := utils.NewTarball()
+			err := tb.AddFiles(sourcesPath)
+			if err != nil {
+				return fmt.Errorf("unable to backup existing sources: %s", err)
+			}
+			tbFile := path.Join(outputPath, "sources.backup.tar.gz")
+			err = tb.Save(tbFile)
+			if err != nil {
+				return fmt.Errorf("unable to backup sources.backup.tar.gz: %s", err)
+			}
+			err = os.RemoveAll(sourcesPath)
+			if err != nil {
+				return fmt.Errorf("unable to remove old sources: %s", err)
+			}
+			if err = os.Mkdir(sourcesPath, 0755); err != nil {
+				defer func() {
+					if err != nil {
+						if restoreErr := tb.Extract(tbFile, outputPath); restoreErr != nil {
+							fmt.Printf("unable to restore sources.backup.tar.gz: %s", restoreErr)
+							return
+						}
+					}
+				}()
+				return fmt.Errorf("unable to recreate sources directory %s: %s", sourcesPath, err)
+			}
+		}
+		if err := api.MarshalSiteState(*loadedSiteState, sourcesPath); err != nil {
+			return err
+		}
+	}
+	outputPath := c.GetOutputPath(runtimeSiteState)
+	runtimeStatePath := path.Join(outputPath, string(api.RuntimeSiteStatePath))
+	if err := api.MarshalSiteState(*runtimeSiteState, runtimeStatePath); err != nil {
 		return err
 	}
 	return nil
 }
 
-func (c *FileSystemConfigurationRenderer) createTokens(siteState *apis.SiteState) error {
-	tokens := make([]apis.Token, 0)
+func CleanupNamespaceForReload(namespace string) error {
+	// Re-create internal configuration directories
+	for _, dir := range reloadDirectories {
+		outputPath := api.GetInternalOutputPath(namespace, dir)
+		if err := os.RemoveAll(outputPath); err != nil {
+			return fmt.Errorf("failed to remove directory %s: %w", outputPath, err)
+		}
+		err := os.MkdirAll(outputPath, 0755)
+		if err != nil && !os.IsExist(err) {
+			return fmt.Errorf("unable to create configuration directory %s: %v", outputPath, err)
+		}
+	}
+	return nil
+}
+
+func BackupNamespace(namespace string) ([]byte, error) {
+	namespacePath := api.GetDefaultOutputNamespacesPath()
+	tb := utils.NewTarball()
+	err := tb.AddFiles(namespacePath, namespace)
+	if err != nil {
+		return nil, fmt.Errorf("failed to add files to tarball: %w", err)
+	}
+	return tb.SaveData()
+}
+
+func RestoreNamespaceData(data []byte) error {
+	tb := utils.NewTarball()
+	err := tb.ExtractData(data, api.GetDefaultOutputNamespacesPath())
+	if err != nil {
+		return fmt.Errorf("error restoring namespace files: %w", err)
+	}
+	return nil
+}
+
+func (c *FileSystemConfigurationRenderer) createTokens(siteState *api.SiteState) error {
+	tokens := make([]*api.Token, 0)
 	for name, linkAccess := range siteState.RouterAccesses {
 		noInterRouterRole := linkAccess.FindRole("inter-router") == nil
 		noEdgeRole := linkAccess.FindRole("edge") == nil
@@ -172,19 +239,20 @@ func (c *FileSystemConfigurationRenderer) createTokens(siteState *apis.SiteState
 			certName = linkAccess.Spec.TlsCredentials
 		}
 		secretName := fmt.Sprintf("client-%s", certName)
-		secret, err := c.loadClientSecret(secretName)
+		secret, err := c.loadClientSecret(siteState, secretName)
 		if err != nil {
 			return fmt.Errorf("unable to load client secret %s: %v", secretName, err)
 		}
-		token := apis.CreateToken(linkAccess, secret)
-		// routerAccess is not valid (no inter-router or edge endpoints)
-		if token == nil {
-			continue
+		routerTokens := api.CreateTokens(*linkAccess, *secret)
+		// routerAccess is valid (inter-router and edge endpoints defined)
+		if len(routerTokens) > 0 {
+			tokens = append(tokens, routerTokens...)
 		}
-		tokens = append(tokens, *token)
 	}
+	outputPath := c.GetOutputPath(siteState)
 	for _, token := range tokens {
-		tokenPath := path.Join(c.OutputPath, RuntimeTokenPath, fmt.Sprintf("%s.yaml", token.Links[0].Name))
+		tokenFileName := fmt.Sprintf("%s-%s.yaml", token.Links[0].Name, token.Links[0].Spec.Endpoints[0].Host)
+		tokenPath := path.Join(outputPath, string(api.RuntimeTokenPath), tokenFileName)
 		tokenYaml, err := token.Marshal()
 		if err != nil {
 			return fmt.Errorf("unable to marshal token: %v", err)
@@ -196,7 +264,7 @@ func (c *FileSystemConfigurationRenderer) createTokens(siteState *apis.SiteState
 	return nil
 }
 
-func (c *FileSystemConfigurationRenderer) createRouterConfig(siteState *apis.SiteState) error {
+func (c *FileSystemConfigurationRenderer) createRouterConfig(siteState *api.SiteState) error {
 	c.RouterConfig = siteState.ToRouterConfig(c.SslProfileBasePath)
 
 	// Saving router config
@@ -204,7 +272,8 @@ func (c *FileSystemConfigurationRenderer) createRouterConfig(siteState *apis.Sit
 	if err != nil {
 		return fmt.Errorf("unable to marshal router config: %v", err)
 	}
-	routerConfigFileName := path.Join(c.OutputPath, "config/router", "skrouterd.json")
+	outputPath := c.GetOutputPath(siteState)
+	routerConfigFileName := path.Join(outputPath, string(api.ConfigRouterPath), "skrouterd.json")
 	err = os.WriteFile(routerConfigFileName, []byte(routerConfigJson), 0644)
 	if err != nil {
 		return fmt.Errorf("unable to write router config file: %v", err)
@@ -212,9 +281,9 @@ func (c *FileSystemConfigurationRenderer) createRouterConfig(siteState *apis.Sit
 	return nil
 }
 
-func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *apis.SiteState) error {
+func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *api.SiteState) error {
 	var err error
-	writeSecretFiles := func(basePath string, secret *corev1.Secret) error {
+	writeSecretFilesIgnore := func(basePath string, secret *corev1.Secret, ignoreExisting bool) error {
 		baseDir, err := os.Open(basePath)
 		if err != nil {
 			if os.IsNotExist(err) {
@@ -238,8 +307,10 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *apis.
 			if certFile, err := os.Open(certFileName); err == nil {
 				// ignoring existing certificate
 				_ = certFile.Close()
-				log.Printf("warning: %s already existing (ignoring)", certFileName)
-				continue
+				if ignoreExisting {
+					log.Printf("warning: %s will not be overwritten", certFileName)
+					continue
+				}
 			}
 			err = os.WriteFile(certFileName, data, 0640)
 			if err != nil {
@@ -248,14 +319,18 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *apis.
 		}
 		return nil
 	}
+	writeSecretFiles := func(basePath string, secret *corev1.Secret) error {
+		return writeSecretFilesIgnore(basePath, secret, false)
+	}
 	// create certificate authorities first
+	outputPath := c.GetOutputPath(siteState)
 	for name, certificate := range siteState.Certificates {
 		if certificate.Spec.Signing == false {
 			continue
 		}
 		secret := certs.GenerateCASecret(name, certificate.Spec.Subject)
-		caPath := path.Join(c.OutputPath, "certificates/ca", name)
-		err = writeSecretFiles(caPath, &secret)
+		caPath := path.Join(outputPath, string(api.CertificatesCaPath), name)
+		err = writeSecretFilesIgnore(caPath, &secret, true)
 		if err != nil {
 			return err
 		}
@@ -266,7 +341,7 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *apis.
 		var secret corev1.Secret
 		var caSecret *corev1.Secret
 		if certificate.Spec.Ca != "" {
-			caSecret, err = c.loadCASecret(certificate.Spec.Ca)
+			caSecret, err = c.loadCASecret(siteState, certificate.Spec.Ca)
 			if err != nil {
 				return fmt.Errorf("unable to load CA secret %s: %v", certificate.Spec.Ca, err)
 			}
@@ -284,7 +359,7 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *apis.
 		} else {
 			continue
 		}
-		certPath := path.Join(c.OutputPath, "certificates", purpose, name)
+		certPath := path.Join(outputPath, "certificates", purpose, name)
 		err = writeSecretFiles(certPath, &secret)
 		if err != nil {
 			return err
@@ -297,7 +372,7 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *apis.
 		if !ok {
 			return fmt.Errorf("secret %s not found", secretName)
 		}
-		certPath := path.Join(c.OutputPath, "certificates", "link", secretName+"-profile")
+		certPath := path.Join(outputPath, "certificates", "link", secretName+"-profile")
 		err = writeSecretFiles(certPath, secret)
 		if err != nil {
 			return err
@@ -306,7 +381,7 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *apis.
 	return nil
 }
 
-func (c *FileSystemConfigurationRenderer) connectJson(siteState *apis.SiteState) *string {
+func (c *FileSystemConfigurationRenderer) connectJson(siteState *api.SiteState) *string {
 	var host string
 	port := 0
 	for _, la := range siteState.RouterAccesses {
@@ -339,16 +414,17 @@ func (c *FileSystemConfigurationRenderer) connectJson(siteState *apis.SiteState)
 	return &content
 }
 
-func (c *FileSystemConfigurationRenderer) loadCASecret(name string) (*corev1.Secret, error) {
-	return c.loadCertAsSecret("ca", name)
+func (c *FileSystemConfigurationRenderer) loadCASecret(siteState *api.SiteState, name string) (*corev1.Secret, error) {
+	return c.loadCertAsSecret(siteState, "ca", name)
 }
 
-func (c *FileSystemConfigurationRenderer) loadClientSecret(name string) (*corev1.Secret, error) {
-	return c.loadCertAsSecret("client", name)
+func (c *FileSystemConfigurationRenderer) loadClientSecret(siteState *api.SiteState, name string) (*corev1.Secret, error) {
+	return c.loadCertAsSecret(siteState, "client", name)
 }
 
-func (c *FileSystemConfigurationRenderer) loadCertAsSecret(purpose, name string) (*corev1.Secret, error) {
-	certPath := path.Join(c.OutputPath, fmt.Sprintf("certificates/%s", purpose), name)
+func (c *FileSystemConfigurationRenderer) loadCertAsSecret(siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) {
+	outputPath := c.GetOutputPath(siteState)
+	certPath := path.Join(outputPath, fmt.Sprintf("certificates/%s", purpose), name)
 	var secret *corev1.Secret
 	certDir, err := os.Open(certPath)
 	if err != nil {
@@ -372,7 +448,8 @@ func (c *FileSystemConfigurationRenderer) loadCertAsSecret(purpose, name string)
 			APIVersion: "v1",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: name,
+			Name:      name,
+			Namespace: siteState.GetNamespace(),
 		},
 		Data: map[string][]byte{},
 	}
diff --git a/pkg/nonkube/common/platform.go b/pkg/nonkube/common/platform.go
new file mode 100644
index 00000000..e998016f
--- /dev/null
+++ b/pkg/nonkube/common/platform.go
@@ -0,0 +1,38 @@
+package common
+
+import (
+	"fmt"
+	"os"
+	"path"
+
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"sigs.k8s.io/yaml"
+)
+
+type NamespacePlatformLoader struct {
+	PathProvider api.InternalPathProvider
+	Platform     string `yaml:"platform"`
+}
+
+func (s *NamespacePlatformLoader) GetPathProvider() api.InternalPathProvider {
+	if s.PathProvider == nil {
+		return api.GetInternalOutputPath
+	}
+	return s.PathProvider
+}
+
+func (s *NamespacePlatformLoader) Load(namespace string) (string, error) {
+	if namespace == "" {
+		namespace = "default"
+	}
+	pathProvider := s.GetPathProvider()
+	runtimePath := pathProvider(namespace, api.RuntimeSiteStatePath)
+	platformFile, err := os.ReadFile(path.Join(runtimePath, "platform.yaml"))
+	if err != nil {
+		return "", fmt.Errorf("failed to read platform config file for namespace %s: %w", namespace, err)
+	}
+	if err = yaml.Unmarshal(platformFile, s); err != nil {
+		return "", fmt.Errorf("failed to unmarshal platform config file for namespace %s: %w", namespace, err)
+	}
+	return s.Platform, nil
+}
diff --git a/pkg/nonkube/common/site_state_loader.go b/pkg/nonkube/common/site_state_loader.go
index e0d21bf7..182ca5e9 100644
--- a/pkg/nonkube/common/site_state_loader.go
+++ b/pkg/nonkube/common/site_state_loader.go
@@ -8,9 +8,12 @@ import (
 	"path"
 	"strings"
 
+	"github.com/skupperproject/skupper/internal/utils"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/skupperproject/skupper/pkg/qdr"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
@@ -22,12 +25,12 @@ type FileSystemSiteStateLoader struct {
 	Bundle bool
 }
 
-func (f *FileSystemSiteStateLoader) Load() (*apis.SiteState, error) {
-	var siteState = apis.NewSiteState(f.Bundle)
+func (f *FileSystemSiteStateLoader) Load() (*api.SiteState, error) {
+	var siteState = api.NewSiteState(f.Bundle)
 	filter := func(filename string) bool {
 		return strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml")
 	}
-	dirReader := new(DirectoryReader)
+	dirReader := new(utils.DirectoryReader)
 	yamlFileNames, err := dirReader.ReadDir(f.Path, filter)
 	if err != nil {
 		return nil, err
@@ -41,13 +44,48 @@ func (f *FileSystemSiteStateLoader) Load() (*apis.SiteState, error) {
 		reader := bufio.NewReader(yamlFile)
 		err = LoadIntoSiteState(reader, siteState)
 		if err != nil {
-			return siteState, err
+			return siteState, fmt.Errorf("error loading %q: %v", yamlFileName, err)
 		}
 	}
+	namespacesFound := GetNamespacesFound(siteState)
+	if len(namespacesFound) > 1 {
+		return nil, fmt.Errorf("multiple namespaces found, but only a unique namespace must be used across all "+
+			"resources - namespaces found: %v", namespacesFound)
+	}
 	return siteState, nil
 }
 
-func LoadIntoSiteState(reader *bufio.Reader, siteState *apis.SiteState) error {
+func addNamespacesFromMap[T metav1.Object](objMap map[string]T, nsMap map[string]bool) {
+	for _, obj := range objMap {
+		ns := obj.GetNamespace()
+		if ns == "" {
+			ns = "default"
+		}
+		nsMap[ns] = true
+	}
+}
+
+func GetNamespacesFound(s *api.SiteState) []string {
+	var namespaces []string
+	var nsMap = make(map[string]bool)
+	siteNamespace := s.GetNamespace()
+	nsMap[siteNamespace] = true
+	addNamespacesFromMap(s.Listeners, nsMap)
+	addNamespacesFromMap(s.Connectors, nsMap)
+	addNamespacesFromMap(s.RouterAccesses, nsMap)
+	addNamespacesFromMap(s.Grants, nsMap)
+	addNamespacesFromMap(s.Links, nsMap)
+	addNamespacesFromMap(s.Secrets, nsMap)
+	addNamespacesFromMap(s.Claims, nsMap)
+	addNamespacesFromMap(s.Certificates, nsMap)
+	addNamespacesFromMap(s.SecuredAccesses, nsMap)
+	for ns := range nsMap {
+		namespaces = append(namespaces, ns)
+	}
+	return namespaces
+}
+
+func LoadIntoSiteState(reader *bufio.Reader, siteState *api.SiteState) error {
 	var err error
 	yamlDecoder := yamlutil.NewYAMLOrJSONDecoder(reader, 1024)
 	// allow reading multiple-document yaml
@@ -118,38 +156,16 @@ func LoadIntoSiteState(reader *bufio.Reader, siteState *apis.SiteState) error {
 	return nil
 }
 
-type FilenameFilter func(string) bool
-type DirectoryReader struct{}
-
-func (f *DirectoryReader) ReadDir(dirname string, filter FilenameFilter) ([]string, error) {
-	dir, err := os.Open(dirname)
+func LoadRouterConfig(namespace string) (*qdr.RouterConfig, error) {
+	routerConfigPath := api.GetInternalOutputPath(namespace, api.ConfigRouterPath)
+	routerConfigFile := path.Join(routerConfigPath, "skrouterd.json")
+	routerConfigData, err := os.ReadFile(routerConfigFile)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("unable to load router configuration: %w", err)
 	}
-	dirInfo, err := dir.Stat()
+	routerConfig, err := qdr.UnmarshalRouterConfig(string(routerConfigData))
 	if err != nil {
-		return nil, err
-	}
-	if !dirInfo.IsDir() {
-		return nil, fmt.Errorf("%s is not a directory", dirname)
-	}
-	files, err := dir.ReadDir(0)
-	if err != nil {
-		return nil, err
-	}
-	var fileNames []string
-	for _, file := range files {
-		if file.IsDir() {
-			recursiveFiles, err := f.ReadDir(path.Join(dirname, file.Name()), filter)
-			if err != nil {
-				return nil, err
-			}
-			fileNames = append(fileNames, recursiveFiles...)
-		} else {
-			if filter == nil || filter(file.Name()) {
-				fileNames = append(fileNames, path.Join(dirname, file.Name()))
-			}
-		}
+		return nil, fmt.Errorf("unable to parse router configuration: %w", err)
 	}
-	return fileNames, nil
+	return &routerConfig, nil
 }
diff --git a/pkg/nonkube/common/site_state_renderer_common.go b/pkg/nonkube/common/site_state_renderer_common.go
index 9c8785f2..41ceca0e 100644
--- a/pkg/nonkube/common/site_state_renderer_common.go
+++ b/pkg/nonkube/common/site_state_renderer_common.go
@@ -2,30 +2,67 @@ package common
 
 import (
 	"fmt"
-	"maps"
 
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/utils"
+	corev1 "k8s.io/api/core/v1"
 )
 
-func CopySiteState(siteState *apis.SiteState) *apis.SiteState {
+func CopySiteState(siteState *api.SiteState) *api.SiteState {
 	// Preserving loaded state
-	var activeSiteState = apis.NewSiteState(siteState.IsBundle())
+	var activeSiteState = api.NewSiteState(siteState.IsBundle())
 	siteState.Site.DeepCopyInto(activeSiteState.Site)
 	activeSiteState.SiteId = siteState.SiteId
-	activeSiteState.Listeners = maps.Clone(siteState.Listeners)
-	activeSiteState.Connectors = maps.Clone(siteState.Connectors)
-	activeSiteState.RouterAccesses = maps.Clone(siteState.RouterAccesses)
-	activeSiteState.Claims = maps.Clone(siteState.Claims)
-	activeSiteState.Links = maps.Clone(siteState.Links)
-	activeSiteState.Grants = maps.Clone(siteState.Grants)
-	activeSiteState.SecuredAccesses = maps.Clone(siteState.SecuredAccesses)
-	activeSiteState.Certificates = maps.Clone(siteState.Certificates)
-	activeSiteState.Secrets = maps.Clone(siteState.Secrets)
+	activeSiteState.Listeners = copySiteStateMap(siteState.Listeners)
+	activeSiteState.Connectors = copySiteStateMap(siteState.Connectors)
+	activeSiteState.RouterAccesses = copySiteStateMap(siteState.RouterAccesses)
+	activeSiteState.Claims = copySiteStateMap(siteState.Claims)
+	activeSiteState.Links = copySiteStateMap(siteState.Links)
+	activeSiteState.Grants = copySiteStateMap(siteState.Grants)
+	activeSiteState.SecuredAccesses = copySiteStateMap(siteState.SecuredAccesses)
+	activeSiteState.Certificates = copySiteStateMap(siteState.Certificates)
+	activeSiteState.Secrets = copySiteStateMap(siteState.Secrets)
 	return activeSiteState
 }
 
-func CreateRouterAccess(siteState *apis.SiteState) error {
+func copySiteStateMap[T any](m map[string]T) map[string]T {
+	if m == nil {
+		return nil
+	}
+	newMap := make(map[string]T, len(m))
+	for k, v := range m {
+		var c any
+		switch vv := any(v).(type) {
+		case *v1alpha1.Listener:
+			c = vv.DeepCopy()
+		case *v1alpha1.Connector:
+			c = vv.DeepCopy()
+		case *v1alpha1.RouterAccess:
+			c = vv.DeepCopy()
+		case *v1alpha1.AccessGrant:
+			c = vv.DeepCopy()
+		case *v1alpha1.Link:
+			c = vv.DeepCopy()
+		case *v1alpha1.AccessToken:
+			c = vv.DeepCopy()
+		case *v1alpha1.Certificate:
+			c = vv.DeepCopy()
+		case *v1alpha1.SecuredAccess:
+			c = vv.DeepCopy()
+		case *corev1.Secret:
+			c = vv.DeepCopy()
+		}
+		if c != nil {
+			newMap[k] = c.(T)
+		} else {
+			newMap[k] = v
+		}
+	}
+	return newMap
+}
+
+func CreateRouterAccess(siteState *api.SiteState) error {
 	if !siteState.HasRouterAccess() {
 		name := fmt.Sprintf("skupper-local")
 		var port = 5671
diff --git a/pkg/nonkube/common/site_state_validator.go b/pkg/nonkube/common/site_state_validator.go
index 28990ac5..96fe2172 100644
--- a/pkg/nonkube/common/site_state_validator.go
+++ b/pkg/nonkube/common/site_state_validator.go
@@ -6,7 +6,7 @@ import (
 	"regexp"
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/utils"
 	corev1 "k8s.io/api/core/v1"
 )
@@ -21,15 +21,14 @@ const (
 	rfc1123Error = `a lowercase RFC 1123 name must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name',  or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')`
 )
 
-type SiteStateValidator struct {
-}
+type SiteStateValidator struct{}
 
 // Validate provides a common validation for non-kubernetes sites
 // which do not benefit from the Kubernetes API. The goal is not
 // to validate the site state against the spec (CRD), but to a more
 // basic level, like to ensure that mandatory fields for each resource are
 // populated and users will be able to operate the non-k8s site.
-func (s *SiteStateValidator) Validate(siteState *apis.SiteState) error {
+func (s *SiteStateValidator) Validate(siteState *api.SiteState) error {
 	var err error
 	if err = s.validateSite(siteState.Site); err != nil {
 		return err
@@ -57,6 +56,12 @@ func (s *SiteStateValidator) Validate(siteState *apis.SiteState) error {
 }
 
 func (s *SiteStateValidator) validateSite(site *v1alpha1.Site) error {
+	namespace := site.Namespace
+	if namespace != "" {
+		if err := ValidateName(namespace); err != nil {
+			return fmt.Errorf("invalid namespace %q: %w", namespace, err)
+		}
+	}
 	if err := ValidateName(site.Name); err != nil {
 		return fmt.Errorf("invalid site name: %w", err)
 	}
@@ -135,10 +140,12 @@ func (s *SiteStateValidator) validateListeners(listeners map[string]*v1alpha1.Li
 		if ip := net.ParseIP(listener.Spec.Host); ip == nil {
 			return fmt.Errorf("invalid listener host: %s - a valid IP address is expected", listener.Spec.Host)
 		}
-
 		if utils.IntSliceContains(hostPorts[listener.Spec.Host], listener.Spec.Port) {
 			return fmt.Errorf("port %d is already mapped for host %q (listener: %q)", listener.Spec.Port, listener.Spec.Host, name)
 		}
+		if listener.Spec.RoutingKey == "" {
+			return fmt.Errorf("routingKey is missing for listener: %s", listener.Name)
+		}
 		hostPorts[listener.Spec.Host] = append(hostPorts[listener.Spec.Host], listener.Spec.Port)
 	}
 	return nil
@@ -157,6 +164,9 @@ func (s *SiteStateValidator) validateConnectors(connectors map[string]*v1alpha1.
 		if ip == nil && !validHostname {
 			return fmt.Errorf("invalid connector host: %s - a valid IP address or hostname is expected", connector.Spec.Host)
 		}
+		if connector.Spec.RoutingKey == "" {
+			return fmt.Errorf("routingKey is missing for connector: %s", connector.Name)
+		}
 	}
 	return nil
 }
diff --git a/pkg/nonkube/common/startup.go b/pkg/nonkube/common/startup.go
index bb62d5cb..8ba6293a 100644
--- a/pkg/nonkube/common/startup.go
+++ b/pkg/nonkube/common/startup.go
@@ -8,9 +8,8 @@ import (
 	"path"
 	"text/template"
 
-	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
-	"github.com/skupperproject/skupper/pkg/config"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 )
 
 var (
@@ -32,39 +31,41 @@ type StartupScript interface {
 type startupScripts struct {
 	StartScript     string
 	StopScript      string
-	Site            *v1alpha1.Site
+	Namespace       string
 	SiteId          string
 	SkupperPlatform string
 	ContainerEngine string
 	path            string
 }
 
-func GetStartupScripts(site *v1alpha1.Site, siteId string) (StartupScript, error) {
+type StartupScriptsArgs struct {
+	Namespace string
+	SiteId    string
+	Platform  types.Platform
+	Bundle    bool
+}
+
+func GetStartupScripts(args StartupScriptsArgs, pathProvider api.InternalPathProvider) (StartupScript, error) {
 	scripts := &startupScripts{
 		StartScript:     StartScriptContainerTemplate,
 		StopScript:      StopScriptContainerTemplate,
-		Site:            site,
-		SiteId:          siteId,
+		Namespace:       args.Namespace,
+		SiteId:          args.SiteId,
 		SkupperPlatform: "podman",
 	}
 
-	platform := config.GetPlatform()
-	if !platform.IsContainerEngine() && !platform.IsBundle() {
+	if !args.Platform.IsContainerEngine() && !args.Bundle {
 		return nil, fmt.Errorf("startup scripts can only be used with podman or docker platforms")
 	}
-	scripts.SkupperPlatform = string(platform)
+	scripts.SkupperPlatform = string(args.Platform)
 	scripts.ContainerEngine = scripts.SkupperPlatform
-	if platform.IsBundle() {
+	if args.Bundle {
 		scripts.ContainerEngine = "{{.ContainerEngine}}"
 	}
-	siteHome, err := apis.GetHostSiteHome(site)
-	if err != nil {
-		return nil, err
-	}
-	scripts.path = path.Join(siteHome, RuntimeScriptsPath)
-	if apis.IsRunningInContainer() {
-		scripts.path = path.Join(GetDefaultOutputPath(site.Name), RuntimeScriptsPath)
+	if pathProvider == nil {
+		pathProvider = api.GetInternalOutputPath
 	}
+	scripts.path = pathProvider(args.Namespace, api.RuntimeScriptsPath)
 	return scripts, nil
 }
 
diff --git a/pkg/nonkube/common/systemd.go b/pkg/nonkube/common/systemd.go
index b95daa2c..d1b87dd3 100644
--- a/pkg/nonkube/common/systemd.go
+++ b/pkg/nonkube/common/systemd.go
@@ -12,8 +12,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
-	"github.com/skupperproject/skupper/pkg/config"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 )
 
 var (
@@ -37,39 +36,47 @@ type CommandExecutor func(name string, arg ...string) *exec.Cmd
 
 type systemdServiceInfo struct {
 	Site                *v1alpha1.Site
+	Namespace           string
 	SiteScriptPath      string
 	SiteConfigPath      string
 	SiteHomePath        string
 	RuntimeDir          string
-	getUid              apis.IdGetter
+	getUid              api.IdGetter
 	command             CommandExecutor
 	rootSystemdBasePath string
+	platform            string
 }
 
-func NewSystemdServiceInfo(site *v1alpha1.Site) (SystemdService, error) {
-	siteHomePath, err := apis.GetHostSiteHome(site)
+func NewSystemdServiceInfo(site *v1alpha1.Site, platform string) (SystemdService, error) {
+	siteHomePath, err := api.GetHostSiteHome(site)
 	if err != nil {
 		return nil, err
 	}
-	siteScriptPath := path.Join(siteHomePath, RuntimeScriptsPath)
-	siteConfigPath := path.Join(siteHomePath, ConfigRouterPath)
+	siteScriptPath := path.Join(siteHomePath, string(api.RuntimeScriptsPath))
+	siteConfigPath := path.Join(siteHomePath, string(api.ConfigRouterPath))
+	namespace := site.Namespace
+	if namespace == "" {
+		namespace = "default"
+	}
 	return &systemdServiceInfo{
 		Site:                site,
+		Namespace:           namespace,
 		SiteScriptPath:      siteScriptPath,
 		SiteConfigPath:      siteConfigPath,
-		RuntimeDir:          apis.GetRuntimeDir(),
+		RuntimeDir:          api.GetRuntimeDir(),
 		getUid:              os.Getuid,
 		command:             exec.Command,
 		rootSystemdBasePath: rootSystemdBasePath,
+		platform:            platform,
 	}, nil
 }
 
 func (s *systemdServiceInfo) GetServiceName() string {
-	return fmt.Sprintf("skupper-site-%s.service", s.Site.Name)
+	return fmt.Sprintf("skupper-%s.service", s.Namespace)
 }
 
 func (s *systemdServiceInfo) Create() error {
-	if !apis.IsRunningInContainer() && !s.isSystemdEnabled() {
+	if !api.IsRunningInContainer() && !s.isSystemdEnabled() {
 		msg := "SystemD is not enabled"
 		if s.getUid() != 0 {
 			msg += " at user level"
@@ -77,10 +84,9 @@ func (s *systemdServiceInfo) Create() error {
 		return fmt.Errorf(msg)
 	}
 
-	platform := config.GetPlatform()
 	var buf = new(bytes.Buffer)
 	var service *template.Template
-	if platform == types.PlatformSystemd {
+	if s.platform == string(types.PlatformSystemd) {
 		service = template.Must(template.New(s.GetServiceName()).Parse(SystemdServiceTemplate))
 	} else {
 		service = template.Must(template.New(s.GetServiceName()).Parse(SystemdContainerServiceTemplate))
@@ -106,7 +112,7 @@ func (s *systemdServiceInfo) Create() error {
 	}
 
 	// Only enable when running locally
-	if !apis.IsRunningInContainer() {
+	if !api.IsRunningInContainer() {
 		return s.enableService(serviceName)
 	}
 
@@ -114,22 +120,22 @@ func (s *systemdServiceInfo) Create() error {
 }
 
 func (s *systemdServiceInfo) getServiceFile() string {
-	if apis.IsRunningInContainer() {
-		return path.Join(GetDefaultOutputPath(s.Site.Name), RuntimeScriptsPath, s.GetServiceName())
+	if api.IsRunningInContainer() {
+		return path.Join(api.GetInternalOutputPath(s.Site.Namespace, api.RuntimeScriptsPath), s.GetServiceName())
 	}
 	if s.getUid() == 0 {
 		return path.Join(s.rootSystemdBasePath, s.GetServiceName())
 	}
-	return path.Join(apis.GetConfigHome(), "systemd/user", s.GetServiceName())
+	return path.Join(api.GetConfigHome(), "systemd/user", s.GetServiceName())
 }
 
 func (s *systemdServiceInfo) Remove() error {
-	if !apis.IsRunningInContainer() && !s.isSystemdEnabled() {
+	if !api.IsRunningInContainer() && !s.isSystemdEnabled() {
 		return fmt.Errorf("SystemD is not enabled at user level")
 	}
 
 	// Stopping systemd user service
-	if !apis.IsRunningInContainer() {
+	if !api.IsRunningInContainer() {
 		cmd := s.getCmdStopSystemdService(s.GetServiceName())
 		_ = cmd.Run()
 
@@ -142,7 +148,7 @@ func (s *systemdServiceInfo) Remove() error {
 	_ = os.Remove(s.getServiceFile())
 
 	// Reloading systemd user daemon
-	if !apis.IsRunningInContainer() {
+	if !api.IsRunningInContainer() {
 		cmd := s.getCmdReloadSystemdDaemon()
 		_ = cmd.Run()
 
diff --git a/pkg/nonkube/common/systemd_container_service.template b/pkg/nonkube/common/systemd_container_service.template
index 5a6d0548..6eff9d2b 100644
--- a/pkg/nonkube/common/systemd_container_service.template
+++ b/pkg/nonkube/common/systemd_container_service.template
@@ -1,5 +1,5 @@
 [Unit]
-Description=skupper-site-{{.Site.Name}}.service
+Description=skupper-{{.Namespace}}.service
 Wants=network-online.target
 After=network-online.target
 RequiresMountsFor={{.RuntimeDir}}/containers
diff --git a/pkg/nonkube/common/systemd_service.template b/pkg/nonkube/common/systemd_service.template
index 65b36e0a..49d38f03 100644
--- a/pkg/nonkube/common/systemd_service.template
+++ b/pkg/nonkube/common/systemd_service.template
@@ -1,5 +1,5 @@
 [Unit]
-Description=skupper-site-{{.Site.Name}}.service
+Description=skupper-{{.Namespace}}.service
 Wants=network-online.target
 After=network-online.target
 
diff --git a/pkg/nonkube/compat/site_state_renderer.go b/pkg/nonkube/compat/site_state_renderer.go
index 47962b82..28a0b1d9 100644
--- a/pkg/nonkube/compat/site_state_renderer.go
+++ b/pkg/nonkube/compat/site_state_renderer.go
@@ -11,22 +11,24 @@ import (
 	internalclient "github.com/skupperproject/skupper/internal/nonkube/client/compat"
 	"github.com/skupperproject/skupper/pkg/container"
 	"github.com/skupperproject/skupper/pkg/images"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/nonkube/common"
 	"github.com/skupperproject/skupper/pkg/utils"
 )
 
 type SiteStateRenderer struct {
-	loadedSiteState *apis.SiteState
-	siteState       *apis.SiteState
-	configRenderer  *common.FileSystemConfigurationRenderer
-	containers      map[string]container.Container
-	cli             *internalclient.CompatClient
+	loadedSiteState   *api.SiteState
+	siteState         *api.SiteState
+	configRenderer    *common.FileSystemConfigurationRenderer
+	containers        map[string]container.Container
+	stoppedContainers map[string]string
+	Platform          types.Platform
+	cli               *internalclient.CompatClient
 }
 
-func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
+func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool) error {
 	var err error
-	var validator apis.SiteStateValidator = &common.SiteStateValidator{}
+	var validator api.SiteStateValidator = &common.SiteStateValidator{}
 	err = validator.Validate(loadedSiteState)
 	if err != nil {
 		return err
@@ -36,21 +38,74 @@ func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
 	if err != nil {
 		return fmt.Errorf("failed to create container client: %v", err)
 	}
+	var backupData []byte
+	// Restore namespace data if reload fail and backupData is not nil
+	defer func() {
+		if !reload {
+			return
+		}
+		// when reload is successful, backupData must be nil
+		if backupData == nil {
+			for _, temporaryName := range s.stoppedContainers {
+				err = s.cli.ContainerRemove(temporaryName)
+				if err != nil {
+					fmt.Printf("Failed to remove temporary container %s: %v\n", temporaryName, err)
+				}
+			}
+			return
+		}
+		fmt.Println("Bootstrap failed, restoring previous state")
+		err := common.RestoreNamespaceData(backupData)
+		if err != nil {
+			fmt.Printf("Error restoring namespace data for %q - %s\n", loadedSiteState.GetNamespace(), err)
+			return
+		}
+		for originalName, temporaryName := range s.stoppedContainers {
+			if temporaryName != originalName {
+				err = s.cli.ContainerRename(temporaryName, originalName)
+				if err != nil {
+					fmt.Printf("Error restoring container name from %q to %q - %s\n", temporaryName, originalName, err)
+				}
+			}
+			err = s.cli.ContainerStart(originalName)
+			if err != nil {
+				fmt.Printf("Error starting container %q - %s\n", originalName, err)
+			}
+		}
+	}()
+
+	if reload {
+		backupData, err = common.BackupNamespace(loadedSiteState.GetNamespace())
+		if err != nil {
+			return fmt.Errorf("failed to backup namespace: %v", err)
+		}
+		err = s.loadExistingSiteId(loadedSiteState)
+		if err != nil {
+			return err
+		}
+		err = s.cleanupExistingNamespace(loadedSiteState)
+		if err != nil {
+			return err
+		}
+	}
 	// active (runtime) SiteState
 	s.siteState = common.CopySiteState(s.loadedSiteState)
 	err = common.RedeemClaims(s.siteState)
 	if err != nil {
 		return fmt.Errorf("failed to redeem claims: %v", err)
 	}
-	// TODO verify if needed in phase 0
 	if err = common.CreateRouterAccess(s.siteState); err != nil {
 		return err
 	}
 	s.siteState.CreateLinkAccessesCertificates()
 	s.siteState.CreateBridgeCertificates()
 	// rendering non-kube configuration files and certificates
+	platform := types.PlatformPodman
+	if s.Platform == types.PlatformDocker {
+		platform = types.PlatformDocker
+	}
 	s.configRenderer = &common.FileSystemConfigurationRenderer{
-		Force: false, // TODO discuss how this should be handled?
+		Platform: string(platform),
 	}
 	err = s.configRenderer.Render(s.siteState)
 	if err != nil {
@@ -58,7 +113,7 @@ func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
 		os.Exit(1)
 	}
 	// Serializing loaded and runtime site states
-	if err = s.configRenderer.MarshalSiteStates(*s.loadedSiteState, *s.siteState); err != nil {
+	if err = s.configRenderer.MarshalSiteStates(loadedSiteState, s.siteState); err != nil {
 		return err
 	}
 
@@ -81,17 +136,54 @@ func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
 	if err = s.createSystemdService(); err != nil {
 		return err
 	}
+	// no need to restore anything
+	backupData = nil
 	return nil
 }
 
+func (s *SiteStateRenderer) loadExistingSiteId(siteState *api.SiteState) error {
+	routerConfig, err := common.LoadRouterConfig(siteState.GetNamespace())
+	if err != nil {
+		return err
+	}
+	// loading site id
+	siteState.SiteId = routerConfig.GetSiteMetadata().Id
+	return nil
+}
+
+func (s *SiteStateRenderer) cleanupExistingNamespace(siteState *api.SiteState) error {
+	// stopping containers
+	containers, err := s.cli.ContainerList()
+	if err != nil {
+		return fmt.Errorf("failed to list containers: %v", err)
+	}
+	s.stoppedContainers = map[string]string{}
+	for _, stopContainer := range containers {
+		if siteId, ok := stopContainer.Labels[types.SiteId]; ok && siteId == siteState.SiteId {
+			err = s.cli.ContainerStop(stopContainer.Name)
+			if err != nil {
+				return fmt.Errorf("failed to stop container: %v", err)
+			}
+			s.stoppedContainers[stopContainer.Name] = stopContainer.Name
+			temporaryName := fmt.Sprintf("%s-backup", stopContainer.Name)
+			err = s.cli.ContainerRename(stopContainer.Name, temporaryName)
+			if err != nil {
+				return fmt.Errorf("failed to rename container %q to %q: %v", stopContainer.Name, temporaryName, err)
+			}
+			s.stoppedContainers[stopContainer.Name] = temporaryName
+		}
+	}
+	return common.CleanupNamespaceForReload(siteState.GetNamespace())
+}
+
 func (s *SiteStateRenderer) prepareContainers() error {
-	siteConfigPath, err := apis.GetHostSiteHome(s.siteState.Site)
+	siteConfigPath, err := api.GetHostSiteHome(s.siteState.Site)
 	if err != nil {
 		return err
 	}
 	s.containers = make(map[string]container.Container)
 	s.containers[types.RouterComponent] = container.Container{
-		Name:  fmt.Sprintf("%s-skupper-router", s.siteState.Site.Name),
+		Name:  fmt.Sprintf("%s-skupper-router", s.siteState.GetNamespace()),
 		Image: images.GetRouterImageName(),
 		Env: map[string]string{
 			"APPLICATION_NAME":      "skupper-router",
@@ -179,7 +271,16 @@ func (s *SiteStateRenderer) startContainers() error {
 
 func (s *SiteStateRenderer) createSystemdService() error {
 	// Creating startup scripts first
-	scripts, err := common.GetStartupScripts(s.siteState.Site, s.configRenderer.RouterConfig.GetSiteMetadata().Id)
+	platform := types.PlatformPodman
+	if s.Platform == types.PlatformDocker {
+		platform = types.PlatformDocker
+	}
+	startupArgs := common.StartupScriptsArgs{
+		Namespace: s.siteState.GetNamespace(),
+		SiteId:    s.configRenderer.RouterConfig.GetSiteMetadata().Id,
+		Platform:  platform,
+	}
+	scripts, err := common.GetStartupScripts(startupArgs, api.GetInternalOutputPath)
 	if err != nil {
 		return fmt.Errorf("error getting startup scripts: %w", err)
 	}
@@ -189,7 +290,7 @@ func (s *SiteStateRenderer) createSystemdService() error {
 	}
 
 	// Creating systemd user service
-	systemd, err := common.NewSystemdServiceInfo(s.siteState.Site)
+	systemd, err := common.NewSystemdServiceInfo(s.siteState.Site, string(s.Platform))
 	if err != nil {
 		return err
 	}
@@ -198,7 +299,7 @@ func (s *SiteStateRenderer) createSystemdService() error {
 	}
 
 	// Validate if lingering is enabled for current user
-	if !apis.IsRunningInContainer() {
+	if !api.IsRunningInContainer() {
 		username := utils.ReadUsername()
 		if os.Getuid() != 0 && !common.IsLingeringEnabled(username) {
 			fmt.Printf("It is recommended to enable lingering for %s, otherwise Skupper may not start on boot.\n", username)
diff --git a/pkg/nonkube/systemd/site_state_renderer.go b/pkg/nonkube/systemd/site_state_renderer.go
index 5dcf641a..8c17d76d 100644
--- a/pkg/nonkube/systemd/site_state_renderer.go
+++ b/pkg/nonkube/systemd/site_state_renderer.go
@@ -4,45 +4,86 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/nonkube/common"
 	"github.com/skupperproject/skupper/pkg/utils"
 )
 
 type SiteStateRenderer struct {
-	loadedSiteState *apis.SiteState
-	siteState       *apis.SiteState
+	loadedSiteState *api.SiteState
+	siteState       *api.SiteState
 	configRenderer  *common.FileSystemConfigurationRenderer
 }
 
-func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
+func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool) error {
 	var err error
-	var validator apis.SiteStateValidator = &common.SiteStateValidator{}
+	var validator api.SiteStateValidator = &common.SiteStateValidator{}
 	err = validator.Validate(loadedSiteState)
 	if err != nil {
 		return err
 	}
 	s.loadedSiteState = loadedSiteState
+	var backupData []byte
+	// Restore namespace data if reload fail and backupData is not nil
+	defer func() {
+		if !reload {
+			return
+		}
+		// when reload is successful, backupData must be nil
+		if backupData == nil {
+			return
+		}
+		fmt.Println("Bootstrap failed, restoring previous state")
+		err := common.RestoreNamespaceData(backupData)
+		if err != nil {
+			fmt.Printf("Error restoring namespace data for %q - %s\n", loadedSiteState.GetNamespace(), err)
+			return
+		}
+		err = s.createSystemdService()
+		if err != nil {
+			fmt.Printf("Error recovering systemd service info - %s\n", err)
+		}
+	}()
+
+	if reload {
+		backupData, err = common.BackupNamespace(loadedSiteState.GetNamespace())
+		if err != nil {
+			return fmt.Errorf("failed to backup namespace: %v", err)
+		}
+		err = s.loadExistingSiteId(loadedSiteState)
+		if err != nil {
+			return err
+		}
+		err = s.removeSystemdService()
+		if err != nil {
+			return err
+		}
+		err = common.CleanupNamespaceForReload(loadedSiteState.GetNamespace())
+		if err != nil {
+			return err
+		}
+	}
+
 	// active (runtime) SiteState
 	s.siteState = common.CopySiteState(s.loadedSiteState)
 	err = common.RedeemClaims(s.siteState)
 	if err != nil {
 		return fmt.Errorf("failed to redeem claims: %v", err)
 	}
-	// TODO verify if needed in phase 0
 	if err = common.CreateRouterAccess(s.siteState); err != nil {
 		return err
 	}
 	s.siteState.CreateLinkAccessesCertificates()
 	s.siteState.CreateBridgeCertificates()
 	// rendering non-kube configuration files and certificates
-	siteHome, err := apis.GetHostSiteHome(s.siteState.Site)
+	siteHome, err := api.GetHostSiteHome(s.siteState.Site)
 	if err != nil {
 		return fmt.Errorf("failed to get site home: %w", err)
 	}
 	s.configRenderer = &common.FileSystemConfigurationRenderer{
-		Force:              false, // TODO discuss how this should be handled?
 		SslProfileBasePath: siteHome,
+		Platform:           string(types.PlatformSystemd),
 	}
 	err = s.configRenderer.Render(s.siteState)
 	if err != nil {
@@ -50,20 +91,31 @@ func (s *SiteStateRenderer) Render(loadedSiteState *apis.SiteState) error {
 		os.Exit(1)
 	}
 	// Serializing loaded and runtime site states
-	if err = s.configRenderer.MarshalSiteStates(*s.loadedSiteState, *s.siteState); err != nil {
+	if err = s.configRenderer.MarshalSiteStates(loadedSiteState, s.siteState); err != nil {
 		return err
 	}
-
 	// Create systemd service
 	if err = s.createSystemdService(); err != nil {
 		return err
 	}
+	// no need to restore anything
+	backupData = nil
+	return nil
+}
+
+func (s *SiteStateRenderer) loadExistingSiteId(siteState *api.SiteState) error {
+	routerConfig, err := common.LoadRouterConfig(siteState.GetNamespace())
+	if err != nil {
+		return err
+	}
+	// loading site id
+	siteState.SiteId = routerConfig.GetSiteMetadata().Id
 	return nil
 }
 
 func (s *SiteStateRenderer) createSystemdService() error {
 	// Creating systemd user service
-	systemd, err := common.NewSystemdServiceInfo(s.siteState.Site)
+	systemd, err := common.NewSystemdServiceInfo(s.loadedSiteState.Site, string(types.PlatformSystemd))
 	if err != nil {
 		return err
 	}
@@ -72,7 +124,7 @@ func (s *SiteStateRenderer) createSystemdService() error {
 	}
 
 	// Validate if lingering is enabled for current user
-	if !apis.IsRunningInContainer() {
+	if !api.IsRunningInContainer() {
 		username := utils.ReadUsername()
 		if os.Getuid() != 0 && !common.IsLingeringEnabled(username) {
 			fmt.Printf("It is recommended to enable lingering for %s, otherwise Skupper may not start on boot.\n", username)
@@ -81,3 +133,15 @@ func (s *SiteStateRenderer) createSystemdService() error {
 
 	return nil
 }
+
+func (s *SiteStateRenderer) removeSystemdService() error {
+	// Removing systemd user service
+	systemd, err := common.NewSystemdServiceInfo(s.loadedSiteState.Site, string(types.PlatformSystemd))
+	if err != nil {
+		return err
+	}
+	if err = systemd.Remove(); err != nil {
+		return fmt.Errorf("unable to remove startup service %q - %v\n", systemd.GetServiceName(), err)
+	}
+	return nil
+}


Test patch:
diff --git a/internal/nonkube/bundle/common_test.go b/internal/nonkube/bundle/common_test.go
new file mode 100644
index 00000000..fe2afe38
--- /dev/null
+++ b/internal/nonkube/bundle/common_test.go
@@ -0,0 +1,116 @@
+package bundle
+
+import (
+	"testing"
+
+	"gotest.tools/assert"
+)
+
+func TestIsValidBundle(t *testing.T) {
+	tests := []struct {
+		name     string
+		strategy string
+		expect   bool
+	}{
+		{
+			name:   "empty-strategy",
+			expect: false,
+		},
+		{
+			name:     "invalid-strategy",
+			strategy: "podman",
+			expect:   false,
+		},
+		{
+			name:     "bundle-strategy",
+			strategy: "bundle",
+			expect:   true,
+		},
+		{
+			name:     "tarball-strategy",
+			strategy: "tarball",
+			expect:   true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			assert.Equal(t, test.expect, IsValidBundle(test.strategy))
+		})
+	}
+}
+
+func TestGetBundleStrategy(t *testing.T) {
+	tests := []struct {
+		name   string
+		env    string
+		flag   string
+		expect string
+	}{
+		{
+			name:   "empty-strategy",
+			expect: "",
+		},
+		{
+			name:   "invalid-strategy-env",
+			env:    "podman",
+			expect: "",
+		},
+		{
+			name:   "invalid-strategy-flag",
+			flag:   "podman",
+			expect: "",
+		},
+		{
+			name:   "invalid-strategy",
+			env:    "podman",
+			flag:   "podman",
+			expect: "",
+		},
+		{
+			name:   "bundle-strategy-env",
+			env:    "bundle",
+			expect: "bundle",
+		},
+		{
+			name:   "bundle-strategy-flag",
+			flag:   "bundle",
+			expect: "bundle",
+		},
+		{
+			name:   "bundle-strategy-override",
+			flag:   "bundle",
+			env:    "tarball",
+			expect: "bundle",
+		},
+		{
+			name:   "tarball-strategy-env",
+			env:    "tarball",
+			expect: "tarball",
+		},
+		{
+			name:   "tarball-strategy-flag",
+			flag:   "tarball",
+			expect: "tarball",
+		},
+		{
+			name:   "tarball-strategy-override",
+			flag:   "tarball",
+			env:    "bundle",
+			expect: "tarball",
+		},
+		{
+			name:   "tarball-strategy-env-invalid-flag",
+			flag:   "invalid",
+			env:    "tarball",
+			expect: "tarball",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			t.Setenv("SKUPPER_BUNDLE", test.env)
+			assert.Equal(t, test.expect, GetBundleStrategy(test.flag))
+		})
+	}
+}
diff --git a/internal/nonkube/bundle/extract_test.go b/internal/nonkube/bundle/extract_test.go
index f9d17372..15d6001c 100644
--- a/internal/nonkube/bundle/extract_test.go
+++ b/internal/nonkube/bundle/extract_test.go
@@ -10,7 +10,7 @@ import (
 
 	"github.com/skupperproject/skupper/internal/utils"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"gotest.tools/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
@@ -58,7 +58,7 @@ func TestSelfExtractingBundle_Generate(t *testing.T) {
 	t.Run("generate-self-extracting-bundle", func(t *testing.T) {
 		tb := utils.NewTarball()
 		assert.Assert(t, tb.AddFiles(sitePath))
-		assert.Assert(t, b.Generate(tb))
+		assert.Assert(t, b.Generate(tb, ""))
 		cleanupPaths = append(cleanupPaths, b.InstallFile())
 	})
 
@@ -81,7 +81,7 @@ func fakeSiteCrs(routerAccess bool) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	ss := &apis.SiteState{
+	ss := &api.SiteState{
 		Site: &v1alpha1.Site{
 			TypeMeta: metav1.TypeMeta{
 				APIVersion: "skupper.io/v1alpha1",
@@ -159,6 +159,6 @@ func fakeSiteCrs(routerAccess bool) (string, error) {
 	if err != nil {
 		return tempDir, err
 	}
-	err = apis.MarshalSiteState(*ss, siteDir)
+	err = api.MarshalSiteState(*ss, siteDir)
 	return tempDir, err
 }
diff --git a/internal/nonkube/bundle/tarball_test.go b/internal/nonkube/bundle/tarball_test.go
index b77ba74f..83469697 100644
--- a/internal/nonkube/bundle/tarball_test.go
+++ b/internal/nonkube/bundle/tarball_test.go
@@ -57,7 +57,7 @@ func TestTarballBundle_Generate(t *testing.T) {
 
 	t.Run("generate-tarball-bundle", func(t *testing.T) {
 		assert.Assert(t, tb.AddFiles(sitePath))
-		assert.Assert(t, b.Generate(tb))
+		assert.Assert(t, b.Generate(tb, ""))
 		cleanupPaths = append(cleanupPaths, b.InstallFile())
 	})
 
diff --git a/internal/utils/files_test.go b/internal/utils/files_test.go
new file mode 100644
index 00000000..b9c20c76
--- /dev/null
+++ b/internal/utils/files_test.go
@@ -0,0 +1,108 @@
+package utils
+
+import (
+	"os"
+	"path"
+	"slices"
+	"strings"
+	"testing"
+
+	"gotest.tools/assert"
+)
+
+func TestDirectoryReader(t *testing.T) {
+	for _, test := range []struct {
+		description   string
+		files         int
+		directories   int
+		levels        int
+		filter        func(string) bool
+		expectedFiles []string
+	}{
+		{
+			description:   "empty-directory",
+			files:         0,
+			directories:   0,
+			levels:        0,
+			expectedFiles: []string{},
+		},
+		{
+			description: "empty-directory-tree",
+			files:       0,
+			directories: 3,
+			levels:      3,
+		},
+		{
+			description: "full-directory",
+			files:       3,
+			directories: 0,
+			levels:      1,
+			expectedFiles: []string{
+				"file.1",
+				"file.2",
+				"file.3",
+			},
+		},
+		{
+			description: "full-directory-tree",
+			files:       3,
+			directories: 3,
+			levels:      1,
+			expectedFiles: []string{
+				"file.1",
+				"file.2",
+				"file.3",
+				"dir1/file.1",
+				"dir1/file.2",
+				"dir1/file.3",
+				"dir2/file.1",
+				"dir2/file.2",
+				"dir2/file.3",
+				"dir3/file.1",
+				"dir3/file.2",
+				"dir3/file.3",
+			},
+		},
+		{
+			description: "full-directory-tree-filtered",
+			files:       3,
+			directories: 3,
+			levels:      1,
+			filter: func(name string) bool {
+				return strings.HasSuffix(name, ".3")
+			},
+			expectedFiles: []string{
+				"file.3",
+				"dir1/file.3",
+				"dir2/file.3",
+				"dir3/file.3",
+			},
+		},
+	} {
+		t.Run(test.description, func(t *testing.T) {
+			baseDir, err := os.MkdirTemp("", "testdirreader.*")
+			assert.Assert(t, err)
+			defer func() {
+				assert.Assert(t, os.RemoveAll(baseDir))
+			}()
+			// Generating files and asserting generation was successful
+			tree := generateDirectoryTree(test.directories, test.levels)
+			err = createFiles(baseDir, test.files, []byte("sample data"), tree)
+			assert.Assert(t, err, "unable to create files")
+
+			r := new(DirectoryReader)
+			files, err := r.ReadDir(baseDir, test.filter)
+			assert.NilError(t, err, "error reading directory")
+			if test.filter == nil {
+				assert.Equal(t, len(files), expectedCreatedFiles(test.directories, test.levels, test.files))
+			} else {
+				if len(test.expectedFiles) > 0 {
+					assert.Equal(t, len(files), len(test.expectedFiles))
+				}
+			}
+			for _, expectedFile := range test.expectedFiles {
+				assert.Assert(t, slices.Contains(files, path.Join(baseDir, expectedFile)))
+			}
+		})
+	}
+}
diff --git a/internal/utils/tarball_test.go b/internal/utils/tarball_test.go
index 023a52cd..f48a8bc1 100644
--- a/internal/utils/tarball_test.go
+++ b/internal/utils/tarball_test.go
@@ -8,7 +8,6 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/skupperproject/skupper/pkg/nonkube/common"
 	"gotest.tools/assert"
 )
 
@@ -55,7 +54,7 @@ func TestTarball(t *testing.T) {
 			err = createFiles(baseDir, tc.files, []byte(testFileContent), tree)
 			assert.Assert(t, err, "unable to create files")
 			generatedFilesExpected := tc.files * (len(tree) + 1)
-			dirReader := new(common.DirectoryReader)
+			dirReader := new(DirectoryReader)
 			filesFound, err := dirReader.ReadDir(baseDir, nil)
 			assert.Assert(t, err)
 			assert.Equal(t, generatedFilesExpected, len(filesFound))
@@ -127,6 +126,21 @@ func TestTarball(t *testing.T) {
 					assert.Equal(t, string(data), testFileContent)
 				}
 			})
+			t.Run(tc.description+"-UncompressData", func(t *testing.T) {
+				baseDirCopy := baseDir + ".data"
+				tb := NewTarball()
+				err = tb.ExtractData(savedData, baseDirCopy)
+				assert.Assert(t, err)
+				cleanupList = append(cleanupList, baseDirCopy)
+				filesFoundCopy, err := dirReader.ReadDir(baseDirCopy, nil)
+				assert.Assert(t, err)
+				assert.Equal(t, generatedFilesExpected, len(filesFoundCopy))
+				for _, file := range filesFoundCopy {
+					data, err := os.ReadFile(file)
+					assert.Assert(t, err)
+					assert.Equal(t, string(data), testFileContent)
+				}
+			})
 			t.Run(tc.description+"-UncompressExtra", func(t *testing.T) {
 				baseDirExtra := baseDir + ".extra"
 				tb := NewTarball()
@@ -172,6 +186,22 @@ func createFiles(baseDir string, files int, content []byte, tree []string) error
 	return nil
 }
 
+// expectedCreatedFiles returns the amount of tiles expected
+// to be created based on the provided arguments.
+func expectedCreatedFiles(dirs, levels, files int) int {
+	/*
+		total of files is: (sum(dirs^!levels))*files
+		i.e: dir=3,levels=4,files=10
+		(3^4 + 3^3 + 3^2 + 3^1) * 10
+	*/
+	sum := 0
+	for level := 1; level <= levels; level++ {
+		sum += int(math.Pow(float64(dirs), float64(level)))
+	}
+	sum *= files
+	return sum + files
+}
+
 func generateDirectoryTree(dirs, levels int) []string {
 	var tree []string
 	indexSum := func(level int) int {
diff --git a/pkg/nonkube/apis/environment_test.go b/pkg/nonkube/api/environment_test.go
similarity index 92%
rename from pkg/nonkube/apis/environment_test.go
rename to pkg/nonkube/api/environment_test.go
index 29583107..649e36ee 100644
--- a/pkg/nonkube/apis/environment_test.go
+++ b/pkg/nonkube/api/environment_test.go
@@ -1,4 +1,4 @@
-package apis
+package api
 
 import (
 	"bytes"
@@ -45,14 +45,14 @@ func TestGetConfigHome(t *testing.T) {
 			var homeOrig = os.Getenv(HOME)
 
 			if tt.xdgConfigHome != "" {
-				_ = os.Setenv(XDG_CONFIG_HOME, tt.xdgConfigHome)
+				t.Setenv(XDG_CONFIG_HOME, tt.xdgConfigHome)
 			}
 			if tt.homeDir != "" {
-				_ = os.Setenv(HOME, tt.homeDir)
+				t.Setenv(HOME, tt.homeDir)
 			}
 			got := GetConfigHome()
-			_ = os.Setenv(XDG_CONFIG_HOME, xdgConfigHomeOrig)
-			_ = os.Setenv(HOME, homeOrig)
+			t.Setenv(XDG_CONFIG_HOME, xdgConfigHomeOrig)
+			t.Setenv(HOME, homeOrig)
 			if got != tt.want {
 				t.Errorf("GetConfigHome() = %v, want %v", got, tt.want)
 			}
@@ -89,14 +89,14 @@ func TestGetDataHome(t *testing.T) {
 			var homeOrig = os.Getenv(HOME)
 
 			if tt.xdgDataHome != "" {
-				_ = os.Setenv(XDG_DATA_HOME, tt.xdgDataHome)
+				t.Setenv(XDG_DATA_HOME, tt.xdgDataHome)
 			}
 			if tt.homeDir != "" {
-				_ = os.Setenv(HOME, tt.homeDir)
+				t.Setenv(HOME, tt.homeDir)
 			}
 			got := GetDataHome()
-			_ = os.Setenv(XDG_DATA_HOME, xdgDataHomeOrig)
-			_ = os.Setenv(HOME, homeOrig)
+			t.Setenv(XDG_DATA_HOME, xdgDataHomeOrig)
+			t.Setenv(HOME, homeOrig)
 			if got != tt.want {
 				t.Errorf("GetDataHome() = %v, want %v", got, tt.want)
 			}
@@ -162,13 +162,13 @@ func TestGetPlatform(t *testing.T) {
 	// Restore original values
 	defer func() {
 		config.Platform = cliOrig
-		_ = os.Setenv(types.ENV_PLATFORM, envOrig)
+		t.Setenv(types.ENV_PLATFORM, envOrig)
 		config.PlatformConfigFile = cfgFileOrig
 	}()
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			config.Platform = tt.cliVar
-			_ = os.Setenv(types.ENV_PLATFORM, tt.envVar)
+			t.Setenv(types.ENV_PLATFORM, tt.envVar)
 
 			// creating a temporary platform file
 			f, err := os.CreateTemp(os.TempDir(), "platform-*.yaml")
@@ -386,14 +386,14 @@ func TestGetHostSiteHome(t *testing.T) {
 	}
 	homeDir, err := os.UserHomeDir()
 	assert.Assert(t, err)
-	defaultSiteHome := path.Join(homeDir, ".local/share/skupper/sites/my-test-site")
+	defaultSiteHome := path.Join(homeDir, ".local/share/skupper/namespaces/default")
 	const fakeXdgDataHome = "/fake/xdg/home"
-	xdgSiteHome := path.Join(fakeXdgDataHome, "/skupper/sites/my-test-site")
+	xdgSiteHome := path.Join(fakeXdgDataHome, "/skupper/namespaces/default")
 
 	envXdgDataHome := "XDG_DATA_HOME"
 	originalXdgDataHome := os.Getenv(envXdgDataHome)
 	defer func() {
-		_ = os.Setenv(envXdgDataHome, originalXdgDataHome)
+		t.Setenv(envXdgDataHome, originalXdgDataHome)
 	}()
 	for _, scenario := range []struct {
 		expectedSiteHome string
@@ -408,10 +408,8 @@ func TestGetHostSiteHome(t *testing.T) {
 			useXdgDataHome:   true,
 		},
 	} {
-		if !scenario.useXdgDataHome {
-			assert.Assert(t, os.Unsetenv(envXdgDataHome))
-		} else {
-			assert.Assert(t, os.Setenv(envXdgDataHome, fakeXdgDataHome))
+		if scenario.useXdgDataHome {
+			t.Setenv(envXdgDataHome, fakeXdgDataHome)
 		}
 		siteHome, err := GetHostSiteHome(fakeSite)
 		assert.Assert(t, err)
diff --git a/pkg/nonkube/apis/site_state_test.go b/pkg/nonkube/api/site_state_test.go
similarity index 78%
rename from pkg/nonkube/apis/site_state_test.go
rename to pkg/nonkube/api/site_state_test.go
index 938f9742..e0b5d7a4 100644
--- a/pkg/nonkube/apis/site_state_test.go
+++ b/pkg/nonkube/api/site_state_test.go
@@ -1,4 +1,4 @@
-package apis
+package api
 
 import (
 	"os"
@@ -286,3 +286,85 @@ func fakeSiteState() *SiteState {
 		SecuredAccesses: make(map[string]*v1alpha1.SecuredAccess),
 	}
 }
+
+func TestSetNamespace(t *testing.T) {
+	ss := fakeSiteState()
+
+	for _, test := range []struct {
+		description   string
+		curNamespace  string
+		newNamespace  string
+		expectChanged bool
+	}{
+		{
+			description:   "empty-to-default",
+			curNamespace:  "",
+			newNamespace:  "default",
+			expectChanged: false,
+		},
+		{
+			description:   "default-to-my-namespace",
+			curNamespace:  "default",
+			newNamespace:  "my-namespace",
+			expectChanged: true,
+		},
+		{
+			description:   "my-namespace-to-other-namespace",
+			curNamespace:  "my-namespace",
+			newNamespace:  "other-namespace",
+			expectChanged: true,
+		},
+		{
+			description:   "other-namespace-to-other-namespace",
+			curNamespace:  "other-namespace",
+			newNamespace:  "other-namespace",
+			expectChanged: false,
+		},
+		{
+			description:   "other-namespace-to-default",
+			curNamespace:  "other-namespace",
+			newNamespace:  "default",
+			expectChanged: true,
+		},
+	} {
+		t.Run(test.description, func(t *testing.T) {
+			assertNamespaceOnSiteState(t, ss, test.curNamespace)
+			ss.SetNamespace(test.newNamespace)
+			if test.expectChanged {
+				assertNamespaceOnSiteState(t, ss, test.newNamespace)
+			} else {
+				assertNamespaceOnSiteState(t, ss, test.curNamespace)
+			}
+		})
+	}
+}
+
+func assertNamespaceOnSiteState(t *testing.T, ss *SiteState, namespace string) {
+	t.Helper()
+	assert.Equal(t, ss.GetNamespace(), getDefaultNs(namespace))
+	assert.Assert(t, assertNamespaceOnMap(ss.Listeners, namespace))
+	assert.Assert(t, assertNamespaceOnMap(ss.Connectors, namespace))
+	assert.Assert(t, assertNamespaceOnMap(ss.RouterAccesses, namespace))
+	assert.Assert(t, assertNamespaceOnMap(ss.Grants, namespace))
+	assert.Assert(t, assertNamespaceOnMap(ss.Links, namespace))
+	assert.Assert(t, assertNamespaceOnMap(ss.Secrets, namespace))
+	assert.Assert(t, assertNamespaceOnMap(ss.Claims, namespace))
+	assert.Assert(t, assertNamespaceOnMap(ss.Certificates, namespace))
+	assert.Assert(t, assertNamespaceOnMap(ss.SecuredAccesses, namespace))
+}
+
+func assertNamespaceOnMap[T metav1.Object](objMap map[string]T, namespace string) bool {
+	for _, obj := range objMap {
+		if getDefaultNs(obj.GetNamespace()) != getDefaultNs(namespace) {
+			return false
+		}
+	}
+	return true
+}
+
+func getDefaultNs(ns string) string {
+	if ns == "" {
+		return "default"
+	}
+	return ns
+}
diff --git a/pkg/nonkube/common/fs_config_renderer_test.go b/pkg/nonkube/common/fs_config_renderer_test.go
index 98f30c71..3c9791c7 100644
--- a/pkg/nonkube/common/fs_config_renderer_test.go
+++ b/pkg/nonkube/common/fs_config_renderer_test.go
@@ -5,8 +5,9 @@ import (
 	"path"
 	"testing"
 
+	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"gotest.tools/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -16,22 +17,24 @@ func TestFileSystemConfigurationRenderer_Render(t *testing.T) {
 	ss := fakeSiteState()
 	ss.CreateLinkAccessesCertificates()
 	ss.CreateBridgeCertificates()
-	outputPath, err := os.MkdirTemp("", "fs-config-renderer-*")
+	customOutputPath, err := os.MkdirTemp("", "fs-config-renderer-*")
 	assert.Assert(t, err)
 	defer func() {
-		err := os.RemoveAll(outputPath)
+		err := os.RemoveAll(customOutputPath)
 		assert.Assert(t, err)
 	}()
-	fsConfigRenderer := &FileSystemConfigurationRenderer{
-		OutputPath: outputPath,
-	}
+	fsConfigRenderer := new(FileSystemConfigurationRenderer)
+	fsConfigRenderer.customOutputPath = customOutputPath
 	assert.Assert(t, fsConfigRenderer.Render(ss))
-	for _, dirName := range []string{"certificates", "config", "loaded", "runtime"} {
-		file, err := os.Stat(path.Join(outputPath, dirName))
+	customOutputPath = fsConfigRenderer.GetOutputPath(ss)
+	for _, dirName := range []string{"certificates", "config", "sources", "runtime"} {
+		file, err := os.Stat(path.Join(customOutputPath, dirName))
 		assert.Assert(t, err)
 		assert.Assert(t, file.IsDir())
 	}
-
+	if envPlatform := os.Getenv(types.ENV_PLATFORM); envPlatform != "" {
+		t.Skipf("The %s environment variable is set to: %s", types.ENV_PLATFORM, envPlatform)
+	}
 	expectedFiles := []string{
 		"config/router/skrouterd.json",
 		"certificates/ca/skupper-site-ca/tls.crt",
@@ -57,18 +60,19 @@ func TestFileSystemConfigurationRenderer_Render(t *testing.T) {
 		"certificates/link/link-one-profile/tls.crt",
 		"certificates/link/link-one-profile/tls.key",
 		"runtime/state/platform.yaml",
-		"runtime/token/link-link-access-one.yaml",
+		"runtime/link/link-link-access-one-127.0.0.1.yaml",
+		"runtime/link/link-link-access-one-localhost.yaml",
 	}
 	for _, fileName := range expectedFiles {
-		fs, err := os.Stat(path.Join(outputPath, fileName))
+		fs, err := os.Stat(path.Join(customOutputPath, fileName))
 		assert.Assert(t, err)
 		assert.Assert(t, fs.Mode().IsRegular())
 		assert.Assert(t, fs.Size() > 0)
 	}
 }
 
-func fakeSiteState() *apis.SiteState {
-	return &apis.SiteState{
+func fakeSiteState() *api.SiteState {
+	return &api.SiteState{
 		SiteId: "site-id",
 		Site: &v1alpha1.Site{
 			TypeMeta: metav1.TypeMeta{
diff --git a/pkg/nonkube/common/site_state_loader_test.go b/pkg/nonkube/common/site_state_loader_test.go
index 60826292..a9a28de3 100644
--- a/pkg/nonkube/common/site_state_loader_test.go
+++ b/pkg/nonkube/common/site_state_loader_test.go
@@ -5,7 +5,7 @@ import (
 	"path"
 	"testing"
 
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"gotest.tools/assert"
 )
 
@@ -19,7 +19,7 @@ func TestFileSystemSiteStateLoder(t *testing.T) {
 	ss := fakeSiteState()
 	ss.CreateBridgeCertificates()
 	ss.CreateLinkAccessesCertificates()
-	assert.Assert(t, apis.MarshalSiteState(*ss, outputPath))
+	assert.Assert(t, api.MarshalSiteState(*ss, outputPath))
 
 	fsStateLoader := &FileSystemSiteStateLoader{
 		Path: path.Join(outputPath),
diff --git a/pkg/nonkube/common/site_state_renderer_common_test.go b/pkg/nonkube/common/site_state_renderer_common_test.go
new file mode 100644
index 00000000..b1deaa50
--- /dev/null
+++ b/pkg/nonkube/common/site_state_renderer_common_test.go
@@ -0,0 +1,46 @@
+package common
+
+import (
+	"reflect"
+	"testing"
+
+	"gotest.tools/assert"
+)
+
+func TestCopySiteState(t *testing.T) {
+	ss := fakeSiteState()
+	newSs := CopySiteState(ss)
+	assert.DeepEqual(t, ss.Site, newSs.Site)
+	assert.Assert(t, ss.Site != newSs.Site)
+	assert.DeepEqual(t, ss.Listeners, newSs.Listeners)
+	assert.DeepEqual(t, ss.Connectors, newSs.Connectors)
+	assert.DeepEqual(t, ss.RouterAccesses, newSs.RouterAccesses)
+	assert.DeepEqual(t, ss.Grants, newSs.Grants)
+	assert.DeepEqual(t, ss.Links, newSs.Links)
+	assert.DeepEqual(t, ss.Secrets, newSs.Secrets)
+	assert.DeepEqual(t, ss.Claims, newSs.Claims)
+	assert.DeepEqual(t, ss.Certificates, newSs.Certificates)
+	assert.DeepEqual(t, ss.SecuredAccesses, newSs.SecuredAccesses)
+	assert.Assert(t, equalsButNotShallowCopy(ss.Listeners, newSs.Listeners))
+	assert.Assert(t, equalsButNotShallowCopy(ss.Connectors, newSs.Connectors))
+	assert.Assert(t, equalsButNotShallowCopy(ss.RouterAccesses, newSs.RouterAccesses))
+	assert.Assert(t, equalsButNotShallowCopy(ss.Grants, newSs.Grants))
+	assert.Assert(t, equalsButNotShallowCopy(ss.Links, newSs.Links))
+	assert.Assert(t, equalsButNotShallowCopy(ss.Secrets, newSs.Secrets))
+	assert.Assert(t, equalsButNotShallowCopy(ss.Claims, newSs.Claims))
+	assert.Assert(t, equalsButNotShallowCopy(ss.Certificates, newSs.Certificates))
+	assert.Assert(t, equalsButNotShallowCopy(ss.SecuredAccesses, newSs.SecuredAccesses))
+}
+
+func equalsButNotShallowCopy[T comparable](oldMap, newMap map[string]T) bool {
+	for k, v := range oldMap {
+		newV := newMap[k]
+		if v == newV {
+			return false
+		}
+		if !reflect.DeepEqual(v, newV) {
+			return false
+		}
+	}
+	return true
+}
diff --git a/pkg/nonkube/common/site_state_validator_test.go b/pkg/nonkube/common/site_state_validator_test.go
index 96f7246e..d6f049bc 100644
--- a/pkg/nonkube/common/site_state_validator_test.go
+++ b/pkg/nonkube/common/site_state_validator_test.go
@@ -4,7 +4,7 @@ import (
 	"testing"
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
-	"github.com/skupperproject/skupper/pkg/nonkube/apis"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"gotest.tools/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -13,13 +13,13 @@ import (
 func TestSiteStateValidator_Validate(t *testing.T) {
 	tests := []struct {
 		info          string
-		siteState     *apis.SiteState
+		siteState     *api.SiteState
 		valid         bool
 		errorContains string
 	}{
 		{
 			info: "invalid-site-name",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				siteState.Site.Name = "bad_name"
 			}),
 			valid:         false,
@@ -27,7 +27,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-link-access-name",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, la := range siteState.RouterAccesses {
 					la.Name = "bad_name"
 					break
@@ -38,7 +38,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-link-access-tlsCredentials-required",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, la := range siteState.RouterAccesses {
 					la.Spec.TlsCredentials = ""
 					break
@@ -48,7 +48,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-link-access-tlsCredentials-bad-name",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, la := range siteState.RouterAccesses {
 					la.Spec.TlsCredentials = "bad_name"
 					break
@@ -59,7 +59,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-link-access-roles-required",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, la := range siteState.RouterAccesses {
 					la.Spec.Roles = make([]v1alpha1.RouterAccessRole, 0)
 					break
@@ -70,7 +70,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-link-access-roles-invalid-role",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, la := range siteState.RouterAccesses {
 					la.Spec.Roles[0].Name = "bad-role"
 					break
@@ -81,7 +81,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-links-no-secrets",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				siteState.Secrets = make(map[string]*corev1.Secret)
 			}),
 			valid:         false,
@@ -89,7 +89,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-links-name",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, link := range siteState.Links {
 					link.Name = "bad_name"
 					break
@@ -100,7 +100,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-links-secret-not-found",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, link := range siteState.Links {
 					link.Spec.TlsCredentials = "invalid"
 					break
@@ -111,7 +111,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-listener-name",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, listener := range siteState.Listeners {
 					listener.Name = "bad_name"
 				}
@@ -121,7 +121,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-listener-host",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, listener := range siteState.Listeners {
 					listener.Spec.Host = ""
 				}
@@ -131,7 +131,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-listener-host",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, listener := range siteState.Listeners {
 					listener.Spec.Host = "1.2.3.4"
 					listener.Spec.Port = 0
@@ -142,7 +142,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-listener-host-invalid-ip",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, listener := range siteState.Listeners {
 					listener.Spec.Host = "invalid_hostname"
 				}
@@ -152,7 +152,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-listener-port-already-mapped",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, listener := range siteState.Listeners {
 					listener.Spec.Host = "1.2.3.4"
 				}
@@ -162,7 +162,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-connector-name",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, connector := range siteState.Connectors {
 					connector.Name = "bad_name"
 				}
@@ -172,7 +172,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-connector-host",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, connector := range siteState.Connectors {
 					connector.Spec.Host = ""
 				}
@@ -182,7 +182,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-connector-host",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, connector := range siteState.Connectors {
 					connector.Spec.Host = "1.2.3.4"
 					connector.Spec.Port = 0
@@ -193,7 +193,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-connector-host-invalid-ip",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				for _, connector := range siteState.Connectors {
 					connector.Spec.Host = "invalid_hostname"
 				}
@@ -203,7 +203,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-claim-name",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				siteState.Claims["bad_name"] = &v1alpha1.AccessToken{
 					TypeMeta: metav1.TypeMeta{
 						Kind:       "Claim",
@@ -219,7 +219,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 		},
 		{
 			info: "invalid-grant-name",
-			siteState: customize(func(siteState *apis.SiteState) {
+			siteState: customize(func(siteState *api.SiteState) {
 				siteState.Grants["bad_name"] = &v1alpha1.AccessGrant{
 					TypeMeta: metav1.TypeMeta{
 						Kind:       "Grant",
@@ -251,7 +251,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 	}
 }
 
-func customize(fn func(siteState *apis.SiteState)) *apis.SiteState {
+func customize(fn func(siteState *api.SiteState)) *api.SiteState {
 	siteState := fakeSiteState()
 	fn(siteState)
 	return siteState
diff --git a/pkg/nonkube/common/startup_test.go b/pkg/nonkube/common/startup_test.go
index f748b0c0..4b83e4cb 100644
--- a/pkg/nonkube/common/startup_test.go
+++ b/pkg/nonkube/common/startup_test.go
@@ -7,6 +7,8 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"gotest.tools/assert"
 )
 
@@ -14,18 +16,24 @@ func TestGetStartupScripts(t *testing.T) {
 	xdgDataHomeOrig := os.Getenv("XDG_DATA_HOME")
 	skupperPlatformOrig := os.Getenv("SKUPPER_PLATFORM")
 	defer func() {
-		_ = os.Setenv("XDG_DATA_HOME", xdgDataHomeOrig)
-		_ = os.Setenv("SKUPPER_PLATFORM", skupperPlatformOrig)
+		t.Setenv("XDG_DATA_HOME", xdgDataHomeOrig)
+		t.Setenv("SKUPPER_PLATFORM", skupperPlatformOrig)
 	}()
 
 	siteState := fakeSiteState()
 	tempDir := t.TempDir()
-	os.Setenv("XDG_DATA_HOME", tempDir)
+	t.Setenv("XDG_DATA_HOME", tempDir)
 
 	for _, platform := range []string{"podman", "docker"} {
 		t.Run("platform-"+platform, func(t *testing.T) {
-			os.Setenv("SKUPPER_PLATFORM", platform)
-			scripts, err := GetStartupScripts(siteState.Site, siteState.SiteId)
+			t.Setenv("SKUPPER_PLATFORM", platform)
+			startupArgs := StartupScriptsArgs{
+				Namespace: siteState.GetNamespace(),
+				SiteId:    siteState.SiteId,
+				Platform:  types.Platform(platform),
+				Bundle:    false,
+			}
+			scripts, err := GetStartupScripts(startupArgs, api.GetInternalOutputPath)
 			assert.Assert(t, err)
 			assert.Assert(t, scripts != nil)
 			assert.Assert(t, os.MkdirAll(scripts.GetPath(), 0755))
@@ -47,8 +55,12 @@ func TestGetStartupScripts(t *testing.T) {
 	}
 
 	t.Run("invalid-platform", func(t *testing.T) {
-		os.Setenv("SKUPPER_PLATFORM", "kubernetes")
-		scripts, err := GetStartupScripts(siteState.Site, siteState.SiteId)
+		startupArgs := StartupScriptsArgs{
+			Namespace: siteState.GetNamespace(),
+			SiteId:    siteState.SiteId,
+			Platform:  types.PlatformKubernetes,
+		}
+		scripts, err := GetStartupScripts(startupArgs, api.GetInternalOutputPath)
 		assert.ErrorContains(t, err, "startup scripts can only be used with podman or docker platforms")
 		assert.Assert(t, scripts == nil)
 	})
diff --git a/pkg/nonkube/common/systemd_test.go b/pkg/nonkube/common/systemd_test.go
index c9e96494..8e5139b2 100644
--- a/pkg/nonkube/common/systemd_test.go
+++ b/pkg/nonkube/common/systemd_test.go
@@ -17,26 +17,26 @@ func TestSystemdService(t *testing.T) {
 	xdgConfigHomeOrig := os.Getenv("XDG_CONFIG_HOME")
 	platformOrig := os.Getenv("SKUPPER_PLATFORM")
 	defer func() {
-		_ = os.Setenv("SKUPPER_OUTPUT_PATH", outputPathOrig)
-		_ = os.Setenv("XDG_CONFIG_HOME", xdgConfigHomeOrig)
-		_ = os.Setenv("SKUPPER_PLATFORM", platformOrig)
+		t.Setenv("SKUPPER_OUTPUT_PATH", outputPathOrig)
+		t.Setenv("XDG_CONFIG_HOME", xdgConfigHomeOrig)
+		t.Setenv("SKUPPER_PLATFORM", platformOrig)
 	}()
 	siteState := fakeSiteState()
 
 	outputPath := t.TempDir()
 	assert.Assert(t, os.MkdirAll(outputPath, 0755))
-	assert.Assert(t, os.Setenv("SKUPPER_OUTPUT_PATH", outputPath))
-	assert.Assert(t, os.Setenv("XDG_CONFIG_HOME", outputPath))
+	t.Setenv("SKUPPER_OUTPUT_PATH", outputPath)
+	t.Setenv("XDG_CONFIG_HOME", outputPath)
 
 	for _, platform := range []string{"systemd", "podman", "docker"} {
 		for _, uid := range []int{0, 1000} {
-			assert.Assert(t, os.Setenv("SKUPPER_PLATFORM", platform))
-			systemdService, err := NewSystemdServiceInfo(siteState.Site)
+			//assert.Assert(t, t.Setenv("SKUPPER_PLATFORM", platform))
+			systemdService, err := NewSystemdServiceInfo(siteState.Site, platform)
 			assert.Assert(t, err)
-			assert.Equal(t, systemdService.GetServiceName(), "skupper-site-site-name.service")
+			assert.Equal(t, systemdService.GetServiceName(), "skupper-default.service")
 			systemdServiceImpl := systemdService.(*systemdServiceInfo)
-			assert.Equal(t, systemdServiceImpl.SiteScriptPath, path.Join(outputPath, "sites/site-name/runtime/scripts"))
-			assert.Equal(t, systemdServiceImpl.SiteConfigPath, path.Join(outputPath, "sites/site-name/config/router"))
+			assert.Equal(t, systemdServiceImpl.SiteScriptPath, path.Join(outputPath, "namespaces/default/runtime/scripts"))
+			assert.Equal(t, systemdServiceImpl.SiteConfigPath, path.Join(outputPath, "namespaces/default/config/router"))
 			systemdServiceImpl.command = func(name string, arg ...string) *exec.Cmd {
 				assert.Assert(t, utils.StringSliceContains(arg, "--user") == (uid != 0))
 				t.Logf("mocking command: %s %s", name, strings.Join(arg, " "))