diff --git a/directus-extension/.env.example b/directus-extension/.env.example
new file mode 100644
index 00000000..65ab850f
--- /dev/null
+++ b/directus-extension/.env.example
@@ -0,0 +1,15 @@
+# These are the keys that have been overridden to make this work with Remix
+# Add them to the top of the .env file generated by the directus init command.
+
+# Where to redirect to when navigating to /. Accepts a relative path, absolute URL, or false to disable ["./admin"]
+ROOT_REDIRECT="false"
+# Ensures Directus can't override whether we are in development or production. Set to `production` in production environments.
+REMIX_ENV=development
+# Only necessary in development environments.
+REMIX_DEV_ORIGIN=http://0.0.0.0:3001/
+# Required for the Remix app to work
+CONTENT_SECURITY_POLICY_DIRECTIVES__SCRIPT_SRC="array:'self','unsafe-inline','unsafe-eval'"
+# Required for live reload to work. Not strictly necessary in production.
+CONTENT_SECURITY_POLICY_DIRECTIVES__CONNECT_SRC="array:'self',https:,http:,wss:,ws:"
+# Tells Directus to reload extensions when the source files change. Not required in production.
+EXTENSIONS_AUTO_RELOAD=true
diff --git a/directus-extension/.eslintrc.cjs b/directus-extension/.eslintrc.cjs
new file mode 100644
index 00000000..2061cd22
--- /dev/null
+++ b/directus-extension/.eslintrc.cjs
@@ -0,0 +1,4 @@
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
+};
diff --git a/directus-extension/.gitignore b/directus-extension/.gitignore
new file mode 100644
index 00000000..dc698e79
--- /dev/null
+++ b/directus-extension/.gitignore
@@ -0,0 +1,8 @@
+node_modules
+
+/.cache
+/build
+/public/build
+.env
+data.db
+extensions
\ No newline at end of file
diff --git a/directus-extension/README.md b/directus-extension/README.md
new file mode 100644
index 00000000..579a287c
--- /dev/null
+++ b/directus-extension/README.md
@@ -0,0 +1,87 @@
+# Remix Directus Extension
+
+This example demonstrates how a Remix app can be embedded in a [Directus](https://directus.io) app using an endpoint extension. The app is comprised of three parts:
+
+- The Remix app
+- The self-hosted Directus instance
+- A Directus endpoint extension, called `remix-frontend`
+
+## What is Directus?
+
+Directus is a self-hosted CMS written in JavaScript and Vue. It allows you to connect a relational database, define data models, view and edit the data, upload and manage files, set up automated workflows and webhooks, view data panels, manage users, roles, and auth... it provides a lot.
+
+It's also highly extensible. Many parts of the UI and backend can be modified and changed using the Directus extension API.
+
+## How it Works
+
+Directus allows you to add API endpoints to a running Directus instance using a special extension. This extension exposes an Express router which we can use to pass requests to our Remix app. With a bit of clever configuration, we can make our app available at the root (eg. `/`) of our Directus instance and serve all requests (except those to the Directus admin at `/admin/*`) from our Remix app.
+
+We can also take advantage of Remix load context and pass Directus utilities to our Remix app. Things like the Directus services, which provide a convenient API for accessing Directus resources, accountability information about the currently logged in user, and direct access to the database.
+
+In this example, we use the `ItemsService`, which is accessed through load context, to pull our list of blog posts in our loader to render to the page.
+
+### Patching Vue Types
+
+This example installs Directus directly, which includes `vue` as a dependency. Unfortunately, Vue uses interface overloading to alter the way TypeScript types JSX, causing errors in idiomatic React code.
+
+To solve this, this example uses `patch-package` to change the types in the Vue package so they don't interfere with React's JSX typings.
+
+This is handled automatically with a `postinstall` package.json script.
+
+## Development
+
+This example includes an example environment variables file.
+
+Before doing anything else, you'll need to set up your Directus instance. From your terminal run:
+
+```sh
+npx directus@latest init
+```
+
+
+Follow the prompts to set up the Directus app how you want. If you're just trying it out, use the `sqlite` database driver.
+
+Once that's done you'll need to add the necessary environment variables at top of the `.env.example` file to your `.env` file. These are required to enable Remix to run from within Directus.
+
+You can apply the example snapshot by running:
+
+```sh
+npx directus schema apply ./snapshot.yml
+```
+
+Make sure the extension has been built before the Directus instance starts for the first time.
+
+From your terminal:
+
+```sh
+npm run build
+```
+
+then
+
+```sh
+npm run dev
+```
+
+This will start the Remix dev server, the extension compiler in watch mode, and the Directus data studio in
+development mode.
+
+You can access your app at `http://localhost:8055` and access the Directus data studio at `http://localhost:8055/admin`.
+
+Sign into the data studio, add a post, visit the site, and you'll see the post appear.
+
+## Deployment
+
+First, build your app for production:
+
+```sh
+npm run build
+```
+
+Then run the app in production mode:
+
+```sh
+npm start
+```
+
+Now you'll need to pick a host to deploy it to. It should work out-of-the-box with any Nixpack-compatible host, like Railway or Flightcontrol. It should also work with a simple Node-based Dockerfile on hosts that support that. Just use `npm run build` as the build command and `npm run start` as the start command, and make sure you've set up the necessary environment variables.
diff --git a/directus-extension/app/entry.client.tsx b/directus-extension/app/entry.client.tsx
new file mode 100644
index 00000000..94d5dc0d
--- /dev/null
+++ b/directus-extension/app/entry.client.tsx
@@ -0,0 +1,18 @@
+/**
+ * By default, Remix will handle hydrating your app on the client for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.client
+ */
+
+import { RemixBrowser } from "@remix-run/react";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+ );
+});
diff --git a/directus-extension/app/entry.server.tsx b/directus-extension/app/entry.server.tsx
new file mode 100644
index 00000000..0c7712b0
--- /dev/null
+++ b/directus-extension/app/entry.server.tsx
@@ -0,0 +1,137 @@
+/**
+ * By default, Remix will handle generating the HTTP Response for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.server
+ */
+
+import { PassThrough } from "node:stream";
+
+import type { AppLoadContext, EntryContext } from "@remix-run/node";
+import { createReadableStreamFromReadable } from "@remix-run/node";
+import { RemixServer } from "@remix-run/react";
+import isbot from "isbot";
+import { renderToPipeableStream } from "react-dom/server";
+
+const ABORT_DELAY = 5_000;
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+ loadContext: AppLoadContext
+) {
+ return isbot(request.headers.get("user-agent"))
+ ? handleBotRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ )
+ : handleBrowserRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ );
+}
+
+function handleBotRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onAllReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ }
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
+
+function handleBrowserRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ }
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
diff --git a/directus-extension/app/root.tsx b/directus-extension/app/root.tsx
new file mode 100644
index 00000000..54bc2cdf
--- /dev/null
+++ b/directus-extension/app/root.tsx
@@ -0,0 +1,36 @@
+import { cssBundleHref } from "@remix-run/css-bundle";
+import type { LinksFunction } from "@remix-run/node";
+import {
+ Links,
+ LiveReload,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "@remix-run/react";
+import type { EndpointExtensionContext } from "@directus/types";
+
+export interface AppLoadContext extends EndpointExtensionContext {}
+
+export const links: LinksFunction = () => [
+ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
+];
+
+export default function App() {
+ return (
+
+