diff --git a/doc/reference/app.md b/doc/reference/app.md
index 14c7d04df..c168bc368 100644
--- a/doc/reference/app.md
+++ b/doc/reference/app.md
@@ -6,6 +6,7 @@
 - [API](#api)
 - [Configuration](#configuration)
 - [`mount` helper](#mount-helper)
+- [Roots](#roots)
 - [Loading templates](#loading-templates)
 
 ## Overview
@@ -92,6 +93,33 @@ Most of the time, the `mount` helper is more convenient, but whenever one needs
 a reference to the actual Owl App, then using the `App` class directly is
 possible.
 
+## Roots
+
+An application can have multiple roots. It is sometimes useful to instantiate
+sub components in places that are not managed by Owl, such as an html editor
+with dynamic content (the Knowledge application in Odoo).
+
+To create a root, one can use the `createRoot` method, which takes two arguments:
+
+- **`Component`**: a component class (Root component of the app)
+- **`config (optional)`**: a config object that may contain a `props` object or a
+  `env` object.
+
+The `createRoot` method returns an object with a `mount` method (same API as
+the `App.mount` method), and a `destroy` method.
+
+```js
+const root = app.createRoot(MyComponent, { props: { someProps: true } });
+await root.mount(targetElement);
+
+// later
+root.destroy();
+```
+
+Note that, like with owl `App`, it is the responsibility of the code that created
+the root to properly destroy it (before it has been removed from the DOM!). Owl
+has no way of doing it itself.
+
 ## Loading templates
 
 Most applications will need to load templates whenever they start. Here is
diff --git a/src/runtime/app.ts b/src/runtime/app.ts
index ace8fd1c3..189ed5b3b 100644
--- a/src/runtime/app.ts
+++ b/src/runtime/app.ts
@@ -16,10 +16,13 @@ export interface Env {
   [key: string]: any;
 }
 
-export interface AppConfig<P, E> extends TemplateSetConfig {
-  name?: string;
+export interface RootConfig<P, E> {
   props?: P;
   env?: E;
+}
+
+export interface AppConfig<P, E> extends TemplateSetConfig, RootConfig<P, E> {
+  name?: string;
   test?: boolean;
   warnIfNoStaticProps?: boolean;
 }
@@ -49,6 +52,12 @@ declare global {
   }
 }
 
+interface Root<P, E> {
+  node: ComponentNode<P, E>;
+  mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
+  destroy(): void;
+}
+
 window.__OWL_DEVTOOLS__ ||= { apps, Fiber, RootFiber, toRaw, reactive };
 
 export class App<
@@ -65,6 +74,7 @@ export class App<
   props: P;
   env: E;
   scheduler = new Scheduler();
+  subRoots: Set<ComponentNode> = new Set();
   root: ComponentNode<P, E> | null = null;
   warnIfNoStaticProps: boolean;
 
@@ -91,14 +101,46 @@ export class App<
     target: HTMLElement | ShadowRoot,
     options?: MountOptions
   ): Promise<Component<P, E> & InstanceType<T>> {
-    App.validateTarget(target);
-    if (this.dev) {
-      validateProps(this.Root, this.props, { __owl__: { app: this } });
+    const root = this.createRoot(this.Root, { props: this.props });
+    this.root = root.node;
+    this.subRoots.delete(root.node);
+    return root.mount(target, options) as any;
+  }
+
+  createRoot<Props extends object, SubEnv = any>(
+    Root: ComponentConstructor<Props, E>,
+    config: RootConfig<Props, SubEnv> = {}
+  ): Root<Props, SubEnv> {
+    const props = config.props || ({} as Props);
+    // hack to make sure the sub root get the sub env if necessary. for owl 3,
+    // would be nice to rethink the initialization process to make sure that
+    // we can create a ComponentNode and give it explicitely the env, instead
+    // of looking it up in the app
+    const env = this.env;
+    if (config.env) {
+      this.env = config.env as any;
+    }
+    const node = this.makeNode(Root, props);
+    if (config.env) {
+      this.env = env;
     }
-    const node = this.makeNode(this.Root, this.props);
-    const prom = this.mountNode(node, target, options);
-    this.root = node;
-    return prom;
+    this.subRoots.add(node);
+    return {
+      node,
+      mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => {
+        App.validateTarget(target);
+        if (this.dev) {
+          validateProps(Root, props, { __owl__: { app: this } });
+        }
+        const prom = this.mountNode(node, target, options);
+        return prom;
+      },
+      destroy: () => {
+        this.subRoots.delete(node);
+        node.destroy();
+        this.scheduler.processTasks();
+      },
+    };
   }
 
   makeNode(Component: ComponentConstructor, props: any): ComponentNode {
@@ -134,6 +176,9 @@ export class App<
 
   destroy() {
     if (this.root) {
+      for (let subroot of this.subRoots) {
+        subroot.destroy();
+      }
       this.root.destroy();
       this.scheduler.processTasks();
     }
diff --git a/tests/app/__snapshots__/sub_root.test.ts.snap b/tests/app/__snapshots__/sub_root.test.ts.snap
new file mode 100644
index 000000000..7af7806b0
--- /dev/null
+++ b/tests/app/__snapshots__/sub_root.test.ts.snap
@@ -0,0 +1,131 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`subroot by default, env is the same in sub root 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>main app</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`subroot by default, env is the same in sub root 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>sub root</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`subroot can mount subroot 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>main app</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`subroot can mount subroot 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>sub root</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`subroot can mount subroot inside own dom 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>main app</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`subroot can mount subroot inside own dom 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>sub root</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`subroot env can be specified for sub roots 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>main app</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`subroot env can be specified for sub roots 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>sub root</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`subroot subcomponents can be destroyed, and it properly cleanup the subroots 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>main app</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`subroot subcomponents can be destroyed, and it properly cleanup the subroots 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>sub root</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
diff --git a/tests/app/sub_root.test.ts b/tests/app/sub_root.test.ts
new file mode 100644
index 000000000..2f2146dc2
--- /dev/null
+++ b/tests/app/sub_root.test.ts
@@ -0,0 +1,115 @@
+import { App, Component, xml } from "../../src";
+import { status } from "../../src/runtime/status";
+import { makeTestFixture, snapshotEverything } from "../helpers";
+
+let fixture: HTMLElement;
+
+snapshotEverything();
+
+beforeEach(() => {
+  fixture = makeTestFixture();
+});
+
+class SomeComponent extends Component {
+  static template = xml`<div>main app</div>`;
+}
+
+class SubComponent extends Component {
+  static template = xml`<div>sub root</div>`;
+}
+
+describe("subroot", () => {
+  test("can mount subroot", async () => {
+    const app = new App(SomeComponent);
+    const comp = await app.mount(fixture);
+    expect(fixture.innerHTML).toBe("<div>main app</div>");
+    const subRoot = app.createRoot(SubComponent);
+    const subcomp = await subRoot.mount(fixture);
+    expect(fixture.innerHTML).toBe("<div>main app</div><div>sub root</div>");
+
+    app.destroy();
+    expect(fixture.innerHTML).toBe("");
+    expect(status(comp)).toBe("destroyed");
+    expect(status(subcomp)).toBe("destroyed");
+  });
+
+  test("can mount subroot inside own dom", async () => {
+    const app = new App(SomeComponent);
+    const comp = await app.mount(fixture);
+    expect(fixture.innerHTML).toBe("<div>main app</div>");
+    const subRoot = app.createRoot(SubComponent);
+    const subcomp = await subRoot.mount(fixture.querySelector("div")!);
+    expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");
+
+    app.destroy();
+    expect(fixture.innerHTML).toBe("");
+    expect(status(comp)).toBe("destroyed");
+    expect(status(subcomp)).toBe("destroyed");
+  });
+
+  test("by default, env is the same in sub root", async () => {
+    let env, subenv;
+    class SC extends SomeComponent {
+      setup() {
+        env = this.env;
+      }
+    }
+    class Sub extends SubComponent {
+      setup() {
+        subenv = this.env;
+      }
+    }
+
+    const app = new App(SC);
+    await app.mount(fixture);
+    const subRoot = app.createRoot(Sub);
+    await subRoot.mount(fixture);
+
+    expect(env).toBeDefined();
+    expect(subenv).toBeDefined();
+    expect(env).toBe(subenv);
+  });
+
+  test("env can be specified for sub roots", async () => {
+    const env1 = { env1: true };
+    const env2 = {};
+    let someComponentEnv: any, subComponentEnv: any;
+    class SC extends SomeComponent {
+      setup() {
+        someComponentEnv = this.env;
+      }
+    }
+    class Sub extends SubComponent {
+      setup() {
+        subComponentEnv = this.env;
+      }
+    }
+
+    const app = new App(SC, { env: env1 });
+    await app.mount(fixture);
+    const subRoot = app.createRoot(Sub, { env: env2 });
+    await subRoot.mount(fixture);
+
+    // because env is different in app => it is given a sub object, frozen and all
+    // not sure it is a good idea, but it's the way owl 2 works. maybe we should
+    // avoid doing anything with the main env and let user code do it if they
+    // want. in that case, we can change the test here to assert that they are equal
+    expect(someComponentEnv).not.toBe(env1);
+    expect(someComponentEnv!.env1).toBe(true);
+    expect(subComponentEnv).toBe(env2);
+  });
+
+  test("subcomponents can be destroyed, and it properly cleanup the subroots", async () => {
+    const app = new App(SomeComponent);
+    const comp = await app.mount(fixture);
+    expect(fixture.innerHTML).toBe("<div>main app</div>");
+    const root = app.createRoot(SubComponent);
+    const subcomp = await root.mount(fixture.querySelector("div")!);
+    expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");
+
+    root.destroy();
+    expect(fixture.innerHTML).toBe("<div>main app</div>");
+    expect(status(comp)).not.toBe("destroyed");
+    expect(status(subcomp)).toBe("destroyed");
+  });
+});
diff --git a/tests/components/__snapshots__/basics.test.ts.snap b/tests/components/__snapshots__/basics.test.ts.snap
index 699a8fb75..1c5405519 100644
--- a/tests/components/__snapshots__/basics.test.ts.snap
+++ b/tests/components/__snapshots__/basics.test.ts.snap
@@ -97,6 +97,19 @@ exports[`basics a component cannot be mounted in a detached node (even if node i
 }"
 `;
 
+exports[`basics a component cannot be mounted in a detached node 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div/>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
 exports[`basics a component inside a component 1`] = `
 "function anonymous(app, bdom, helpers
 ) {
@@ -261,6 +274,19 @@ exports[`basics can mount a simple component with props 1`] = `
 }"
 `;
 
+exports[`basics cannot mount on a documentFragment 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div>content</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
 exports[`basics child can be updated 1`] = `
 "function anonymous(app, bdom, helpers
 ) {
@@ -1002,6 +1028,19 @@ exports[`basics three level of components with collapsing root nodes 3`] = `
 }"
 `;
 
+exports[`basics throws if mounting on target=null 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<span>simple vnode</span>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
 exports[`basics two child components 1`] = `
 "function anonymous(app, bdom, helpers
 ) {
diff --git a/tests/components/__snapshots__/props_validation.test.ts.snap b/tests/components/__snapshots__/props_validation.test.ts.snap
index 56351a125..07cf4182b 100644
--- a/tests/components/__snapshots__/props_validation.test.ts.snap
+++ b/tests/components/__snapshots__/props_validation.test.ts.snap
@@ -924,6 +924,20 @@ exports[`props validation props: list of strings 1`] = `
 }"
 `;
 
+exports[`props validation validate props for root component 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div><block-text-0/></div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    let txt1 = ctx['message'];
+    return block1([txt1]);
+  }
+}"
+`;
+
 exports[`props validation validate simple types 1`] = `
 "function anonymous(app, bdom, helpers
 ) {