# Overview Directus is a backend for building your projects. Connect it to your database, asset storage, and external services, and immediately get a REST and GraphQL API, an SDK, realtime, auth, file management, automations, and a web application to work with your data. Granular access control means users can only see, interact with, and create the data their role allows. :partial{content="engine-studio-box"} ## How It Works :video-embed{video-id="96b44cbc-1b14-4bea-87cd-0c4cb34d261d"} ## Choose How to Run Directus You can use Directus Cloud for a fully managed project or self-host with Docker on your own infrastructure. ## Directus Cloud Directus Cloud is a fully managed service. Cloud architecture can be complicated and resource-intensive, so Directus Cloud provides scalable, optimized storage and infrastructure, and automatic updates so you can focus on building your project. With your Directus Cloud account, you can set up teams to organize projects and billing. You'll be able to get a project running in about 90 seconds. Directus Cloud projects scale automatically to improve reliability, even with unexpected peaks in traffic. :cta-cloud ## Self-Hosted If you want to run Directus on your own infrastructure, start with [Create a project](https://directus.com/docs/getting-started/create-a-project). ::callout --- color: secondary icon: i-lucide-graduation-cap to: https://directus.com/docs/tutorials/self-hosting --- See all deployment options (Cloud, Docker, and platform-specific guides). :: Need advanced configuration, unlimited scalability, and dedicated support? **[Contact us to check out Directus Enterprise Cloud](https://directus.com/contact){rel=""nofollow""}.** :partial{content="license"} # Accessibility ## Keyboard Navigation You can navigate through Directus Studio entirely with your keyboard. The Skip Menu makes it easy to jump between page sections. ### Shortcuts - Navigation is primarily done with the `Tab` key and occasionally with the arrow keys. - Enter your selection with the `enter` or `space` keys. - Save with `meta` + `s`. - Apply edits in modals/drawers/popovers with `meta` + `enter`. - Cancel/exit modals/drawers/popovers with the `escape` key. Special shortcuts for the WYSIWYG editor: - Use `Tab` to move focus into and through the toolbar, and arrow keys to move between adjacent tools. - Press `Esc` to return focus to the editor content area. - Standard formatting shortcuts apply in the content area, for example `meta` + `b` (bold), `meta` + `i` (italic), and `meta` + `k` (insert link). The WYSIWYG editor was rebuilt on [Tiptap](https://tiptap.dev){rel=""nofollow""} in Directus 12.2.0. See the [Version 12 breaking changes](https://directus.com/docs/releases/breaking-changes/version-12#wysiwyg-editor-rebuilt-on-tiptap) for details. ### Things to keep in mind - Manual Sorting is currently not supported/accessible. - Once focused, the code interface (Codemirror) cannot be exited using the tab key. - The Markdown interface also doesn’t allow you to exit the field. This is because it supports tabs inside the editor’s text content. # Create a Project There are several ways to start a new Directus project. This guide walks through the most common scenarios. If you want to compare all deployment paths (including one-click platform guides like Railway), see [How to Deploy Directus](https://directus.com/docs/tutorials/self-hosting). New self-hosted instances run on the core tier by default. To unlock higher limits or additional features, [add a license](https://directus.com/docs/licensing/overview). :partial{content="license"} ## Directus Cloud Directus Cloud provides infrastructure from the team who builds Directus. Projects can be created in over 15 global deployment regions and feature autoscaling for improved availability. Create and login to your [Directus Cloud account](https://directus.cloud/){rel=""nofollow""}. The very first time you log in to your Directus Cloud account, you will be prompted to create a team. Each Directus Cloud project exists within the scope of one team. :cta-cloud Once started, it should take around 90 seconds for the Cloud project to be created. During this time, a link will be sent to the email associated with your Cloud account. The email will contain your project URL as well as an email and password to login. If you used GitHub to create your account, this will be the email address associated with your GitHub account. Login to your new project using the URL in your email inbox or on your Directus Cloud Dashboard. ## Docker Installation You will need Docker installed and running on your machine. You can [download it here](https://docs.docker.com/get-docker/){rel=""nofollow""}. ::callout{icon="i-lucide-info"} **What is Docker?** Docker is a developer tool that allows software-creators to distribute their work along with all dependencies and required environment settings. This means that applications can run reliably and consistently, making it the perfect way to use Directus both locally and in-production. As soon as there are new releases of Directus, we publish them on [Docker Hub](https://hub.docker.com/r/directus/directus){rel=""nofollow""}. :: ### Quickstart Run the following command in your terminal: ```bash docker run -p 8055:8055 directus/directus ``` Directus should now be available at [http://localhost:8055](http://localhost:8055/){rel=""nofollow""} or [http://127.0.0.1:8055](http://127.0.0.1:8055/){rel=""nofollow""}, where you'll see an onboarding screen to configure your first Admin account. ![Directus onbaording](https://directus.com/docs/img/directus_setup.png) This quickstart allows you to explore Directus at a glance, but lacks many features including persistence. Once you stop the Docker container from running, any changes you’ve made will be lost. ### Docker Compose This is the recommended way to get started with Directus. Create a new empty directory on your machine called `directus`. Within this new directory, create the three empty sub-directories `database`, `uploads`, and `extensions`. Create a `docker-compose.yml` file in the `directus` directory: ```yaml [docker-compose.yml] services: directus: image: directus/directus:12.0.2 ports: - 8055:8055 volumes: - ./database:/directus/database - ./uploads:/directus/uploads - ./extensions:/directus/extensions environment: SECRET: "replace-with-random-value" DB_CLIENT: "sqlite3" DB_FILENAME: "/directus/database/data.db" WEBSOCKETS_ENABLED: "true" PUBLIC_URL: "http://localhost:8055/" LICENSE_KEY: "" ``` ::callout{icon="i-lucide-info"} **Breakdown of Docker Compose File** - This file defines a single Docker container that will use the specified version of the `directus/directus` image. - The `ports` list maps internal port `8055` is made available to our machine using the same port number, meaning we can access it from our computer's browser. - The `volumes` section maps internal `directus/database` and `directus/uploads` to our local file system alongside the `docker-compose.yml` meaning data is backed up outside of Docker containers. - The `environment` section contains any [configuration environment variables](https://directus.com/docs/configuration/general)we wish to set. - `SECRET` is required and should be a long random value. `SECRET` is used to sign access tokens. - `DB_CLIENT` and `DB_FILENAME` are defining the connection to your database. - `WEBSOCKETS_ENABLED` is not required, but enables [Directus Realtime](https://directus.com/docs/getting-started/connect-to-realtime). - `PUBLIC_URL` is the full URL where your project is accessed. It's used to generate links, asset URLs, and redirects — and a license binds to it on first use, so set it accurately. - `LICENSE_KEY` is optional — leave empty for the core tier, or add your key to unlock a [licensed tier](https://directus.com/docs/licensing/overview). :: Open the Terminal, navigate to your `directus` directory, and run the following command: ```text docker compose up ``` Directus should now be available at {rel=""nofollow""} or {rel=""nofollow""}, where you'll see an onboarding screen to configure your first Admin account. The project that runs from this `docker-compose.yml` file is not production-ready but enough to use many features. ## Deploy Directus We also have a number of guides on self-hosting Directus on various cloud providers, like Amazon Web Services, Microsoft Azure, and Google Cloud Platform. ::callout --- color: secondary icon: i-lucide-graduation-cap to: https://directus.com/docs/tutorials/self-hosting --- See how to deploy Directus (all options). :: ## Next Steps Now you have a project running, [learn how to create a data model](https://directus.com/docs/getting-started/data-model), and then use the auto-generated APIs created by :product-link{product="connect"}. # Configure a Data Model :video-embed{video-id="637aafa2-b323-4ad0-adf0-ba52328bb798"} This guide will cover creating a collection in Directus via the Data Studio, creating fields, and configuring relationships. :cta-cloud ## Creating a Collection Log into the Directus Data Studio as an administrator. If this is a brand-new project, you will be presented with the option to create your first collection. Otherwise, go to the settings module and create a new collection from the Data Model page. Set the name of this collection to be `posts`, leaving all other options in both the collection setup and optional field pages as their defaults. You now have a new collection with only a primary key. ![A brand new collection](https://directus.com/docs/img/2e088221-6bc5-4c00-b348-e23f77a9a748.webp) ## Creating Fields Your collection only has a primary key. From your new `posts` collection configuration page, click the **Create Field** button and select the Input interface. Set the key to `title` and leave all other options as their defaults. Create another new field with a What You See Is What You Get (WYSIWYG) interface. Set the key to `content`. ## Configuring a Relationship Create a new collection called `authors`. In the new collection, create a new field with an Input interface and set the key to `name`. Go to the `posts` collection configuration and create a new field with the Many to One interface and set the key to `author`. Set the related collection to `authors` and configure the Display Template to show just the author's name by clicking :icon{name="material-symbols:add-circle-outline-rounded"} and selecting the `name` field. Now that you have successfully configured a relationship between the two tables, you can start creating data. In the module bar, go to the content module. Enter your `authors` collection and create 2 authors with the names `Ben Haynes` and `Rijk van Zanten`. Enter the `posts` collection and create two posts, selecting an author from the Many to One interface. ![Selecting an item from a relation](https://directus.com/docs/img/73e236ac-322f-4565-ba98-172a5596bcad.webp) ![Both collections' settings](https://directus.com/docs/img/8a641c77-e13b-4bec-ae54-085a5484cd32.webp) ## Next Steps Read more about configuring [collections](https://directus.com/docs/guides/data-model/collections), [fields](https://directus.com/docs/guides/data-model/fields), and [relationships](https://directus.com/docs/guides/data-model/relationships). See all available [interfaces](https://directus.com/docs/guides/data-model/interfaces) in Directus. Access your new collections via API or SDK using :product-link{product="connect"}. # Use the API :video-embed{video-id="4cc18530-ba2a-44f3-bb2e-2bfe4ad024d5"} This guide will cover interacting with collections in Directus via the REST APIs automatically created on your behalf. You will fetch and create data, and make your first request with the Directus SDK. :partial{content="quickstart-making-calls"} ## Before You Start You will need a Directus project. :cta-cloud Create a `posts` collection with at least a `title` and `content` field. [Follow the Data Modeling quickstart to learn more](https://directus.com/docs/getting-started/data-model). You also need an admin static token. In the Data Studio, go to your user detail page. Create a new token, take note of it, and then save. ::callout --- color: primary icon: i-lucide-book-open to: https://directus.com/docs/guides/auth/tokens-cookies --- Read more about tokens and cookies in Directus Auth. :: ## Fetching Data Open your terminal and run the following command to read items from the `posts` collection. ```bash [Terminal] curl \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --url 'https://directus.example.com/items/posts' ``` ::callout{icon="i-lucide-info"} **Replace values** - The Base URL (`https://directus.example.com`) must be replaced with your project URL. - In the Authorization Header, replace `YOUR_ACCESS_TOKEN` with your admin static token. - If you used a different collection, replace `posts` with the name of the collection. :: Directus will respond with an array of items. The default limit is 100, so if there are more than 100 items, you must either provide a higher limit or request a second page. ## Using Query Parameters You can use any of the global query parameters to change the data that is returned by Directus. ```bash [Terminal] curl \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --url 'https://directus.example.com/items/posts?filter[status][_eq]=published&fields=id,title' ``` This request will only show items with a `status` value of `published`, and only return the `id` and `title` fields. ::callout --- color: primary icon: i-lucide-book-open to: https://directus.com/docs/guides/connect/query-parameters --- See all available query parameters in Directus. :: ## Creating Data All collections are given consistent endpoints. By sending a POST request to `/items/posts` with an object containing properties in the collection, a new item will be created. ```bash [Terminal] curl \ --request POST \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "title": "Hello Universe!" }' \ --url 'https://directus.example.com/items/posts' ``` ## Next Steps :video-embed{video-id="0cbf2b23-545e-4ea7-ae45-47707292caec"} All endpoints in Directus are documented in our API Reference, which also shows all expected parameters and properties in the payload. The API reference shows examples using the REST API, GraphQL API, and the Directus SDK. ::callout --- color: green icon: i-lucide-square-code to: https://directus.com/docs/api --- Explore the Directus API Reference. :: # Authenticate a User :video-embed{video-id="04ffd615-6d1d-45de-9c1b-2ff9206fe343"} This guide will cover registering users, logging in, and making an authenticated request. :partial{content="quickstart-making-calls"} ## Before You Start You will need a Directus project. :cta-cloud Create a `posts` collection with at least a `title` and `content` field. [Follow the data modeling quickstart to learn more](https://directus.com/docs/getting-started/data-model). Create a single item in the collection. ## Creating a Role and a Policy From your settings, navigate to User Roles and create a new role named "User". This role will later be applied to new users who register. Within the role page, create a new policy named "Read Posts". Add a permission to the policy to allow **Read** action on `posts` collection. ## Allow User Registration From your settings, enable User Registration. Select the User role that was just created and disable the Verify Email setting. ## Registering via the Data Studio Log out of the Data Studio. From the Sign In screen, you will see a new option to Sign Up. Once a user is signed up, they will immediately be able to log in. ## Registering via API Open your terminal and run the following command to register a new user. ::code-group ```bash [Terminal] curl \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "email": "hello@example.com", "password": "d1r3ctu5" }' \ --url 'https://directus.example.com/users/register' ``` ```graphql [GraphQL] mutation { users_register(email: "hello@example.com", password: "d1r3ctu5") } ``` ```js [SDK] import { createDirectus, rest, registerUser } from '@directus/sdk'; const client = createDirectus('https://directus.example.com').with(rest()); const result = await client.request(registerUser('hello@example.com', 'd1r3ctu5')); ``` :: Go to the user directory in the module bar and you should see a new user has been created. ## Logging In ::code-group ```bash [Terminal] curl \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "email": "hello@example.com", "password": "d1r3ctu5" }' \ --url 'https://directus.example.com/auth/login' ``` ```graphql [GraphQL] mutation { auth_login(email: "hello@example.com", password: "d1r3ctu5") { access_token refresh_token } } ``` ```js [SDK] import { createDirectus, authentication } from '@directus/sdk'; const email = "hello@example.com"; const password = "d1r3ctu5"; const client = createDirectus('http://directus.example.com').with(authentication()); const token = await client.login({ email, password }); ``` :: ## Authenticating Requests You can use the access token while making requests. If your token has expired, you must refresh it. ```bash [Terminal] curl \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ --url 'https://directus.example.com/items/posts' ``` ::callout --- color: primary icon: i-lucide-book-open to: https://directus.com/docs/guides/auth/email-login --- Read more about refreshing tokens. :: ## Next Steps Read more about [access tokens](https://directus.com/docs/guides/auth/tokens-cookies), [access control](https://directus.com/docs/guides/auth/access-control), and then refer to the Users API reference to manage user accounts. ::callout --- color: green icon: i-lucide-square-code to: https://directus.com/docs/api/users --- Explore the Users API Reference. :: # Upload & Access Files :video-embed{video-id="43612e4c-1bd9-411e-bd73-9c835a9b51e0"} This guide will cover importing a file via URL, requesting assets, and using transformation parameters. :cta-cloud ## Import a File In the module bar, click :icon{name="material-symbols:folder-outline-rounded"} to go to the files module. Click the :icon{name="material-symbols:add"} button and :icon{name="material-symbols:link"} to import a file via URL. Use any publicly-accessible image URL and the file will be uploaded to your asset storage. ## Access a File The uploaded file is immediately available via the Data Studio for users with the correct access control. From here, you can download, edit, or replace files. You can access files via URL in your applications by using the following URL pattern: ```text https://example.directus.app/assets/?access_token=token ``` The token must belong to a user who has access to read files in the `directus_files` collection. If the public role has read access, you can omit the `access_token` parameter. ::callout --- color: primary icon: i-lucide-book-open to: https://directus.com/docs/guides/auth/access-control --- Learn how to limit access to data in your project through custom permissions. :: ## Transform an Image Directus can transform images via query parameters, commonly used to provide the most suitable size and file format. Add the following query parameter to the end of your file URL: ```text width=200 ``` Your new URL should look like this: ```text https://example.directus.app/assets/?access_token=token&width=200 ``` The asset will be transformed, saved to your asset storage, and returned to the user. On subsequent requests, the already transformed asset will be returned. ## Next Steps Read more about [uploading files](https://directus.com/docs/guides/files/upload), [advanced transformations](https://directus.com/docs/guides/files/access), and then refer to the Files API reference to manage user accounts. ::callout --- color: green icon: i-lucide-square-code to: https://directus.com/docs/api/files --- Explore the Files API Reference. :: # Create a Flow :video-embed{video-id="6e6965e7-13cc-4f86-b512-f567d66cfbe9"} This guide will cover custom event-driven data processing using Flows. ## Before You Start You will need a Directus project. :cta-cloud Create a `posts` collection with at least a `title` and `content` field. [Follow the Data Modeling quickstart to learn more](https://directus.com/docs/getting-started/data-model). ## Create a Flow ![Create a new flow pane - including name, metadata, and option to track activity and logs.](https://directus.com/docs/img/3c337848-a40f-4e62-9370-c943e8d5e761.webp) Navigate to the Flows section in the Settings module. Click on :icon{name="material-symbols:add-circle-outline"} in the page header and name the new flow "Post Created". ## Configure a Trigger ![Create a new flow pane - trigger setup where the trigger is an event hook.](https://directus.com/docs/img/bf02c5e9-ebe2-4bf4-9fc5-3c00811a7a8a.webp) Click on :icon{name="material-symbols:play-arrow-outline"} to open trigger setup. Select "Event Hook" as the trigger type and select "Action (Non-Blocking)". This will allow you to set up this flow to respond to when an event takes place by running an action that doesn't interrupt. Select `items.create` as the scope, and then check the "Posts" collection. This combination means that the operation will be triggered when an post is created. ## Configure an Operation ![In the flow editor, the trigger connects to an operation. The operation configuration pane is open with a type of Send Notification.](https://directus.com/docs/img/4c072da6-b396-47ad-85ff-f300e3eb9661.webp) Click on :icon{name="material-symbols:add-circle-outline"} on the trigger panel. Here, you can create an operation. Give it the name "Notify Post Created" and the key "notify\_post\_created" will be written alongside. Select the "Send Notification" operation type. Fill in the "User" field with the UUID of your user, which can be found in the user directory. Under "Permissions", select "From Trigger", which will cause the operation to have the same permissions as those that came from the trigger itself. Fill in the "Subject" and "Message" fields as desired, leaving "Collection" and "Item" blank. When finished, click on :icon{name="material-symbols:check-circle-rounded"} in the flow's top right corner. Now, when you create a post, the user you entered will be notified. ## Next Steps Read more about different [triggers](https://directus.com/docs/guides/flows/triggers) available in flows and how data is passed through a flow with [the data chain](https://directus.com/docs/guides/flows/data-chain). # Connect to Realtime Data :video-embed{video-id="4d3c062f-0f30-41b7-83e9-3d2ed34a86f4"} Instead of needing to make a request to see if data has changed, your application can receive updates in realtime over a persistent connection with Directus. All subscriptions and actions over a realtime connection use the permissions of the authenticated user, or public permissions if not authenticated. This guide will cover getting started with :product-link{product="realtime"} by connecting to Directus with the Directus SDK on the web, subscribing to changes, and creating new items. ## Before You Start You will need a Directus project. :cta-cloud Create a `messages` collection with a `date_created` field enabled on collection creation. Add `text` and `user` text fields. Follow the [data modeling quickstart](https://directus.com/docs/getting-started/data-model) to learn more. Add an [access policy](https://directus.com/docs/guides/auth/access-control) called **Public Posts** to your user in the Data Studio. Within it, create a new permission to allow the `read` and `create` actions on the `messages` collection. In the Data Studio, create a [static token](https://directus.com/docs/guides/auth/tokens-cookies) for your user, copy it, and save your user profile. ## Enable Realtime Directus Realtime is disabled by default on self-hosted projects. Set the `WEBSOCKETS_ENABLED` environment variable to `true`. If you use Directus Cloud to host your project, you do not need to manually enable Realtime. ## Connect via Directus Realtime Create an `index.html` file, import the Directus SDK from a CDN, create a client with the `realtime` composable, and connect. Be sure to replace your Directus project URL and access token. ```html ``` ## Subscribe to Changes After subscribing to collections over your connection, you will receive new messages whenever items in the collection are created, updated, or deleted. At the bottom of your ` ``` ::callout{icon="i-lucide-info"} More information can be found at {rel=""nofollow""}. :: ```js ``` # Studio Module The visual editor module enables content editors to render their website within the Directus Studio, navigate around within the site, and make edits to content in place. ![An image of the visual editor with the drawer open on a page and an input being hovered over](https://directus.com/docs/img/visual_editor_drawer_editing.png) ::callout{color="info" icon="i-lucide-info"} Visual editing also works in the [**Live Preview**](https://directus.com/docs/guides/content/live-preview#visual-editing-in-live-preview) pane on item detail pages. This gives the same editing experience without switching modules. :: ## Configure Visual Editor URLs Navigate to **Settings → Visual Editor** and add the URL of your website that you want to visually edit. If you have multiple websites, add multiple URLs. ![An image of the visual editor section of the Directus settings page with one URL entered](https://directus.com/docs/img/visual_editor_settings_url.png) Be sure to enable the Visual Editor from the Modules section of the settings page so it shows up in your project's module bar. ### Version Support in URLs The URL field supports a `{{$version}}` template variable. When included, the Visual Editor will pass the currently selected version key to your website, enabling version-aware previews. ```text https://your-site.com/preview?version={{$version}} ``` - **Resolution**: When no version is selected, `{{$version}}` resolves to `published`. - **Flexibility**: The variable can be placed in any part of the URL (query parameters, path segments, subdomains, or hash fragments). #### Implementation Checklist To ensure version-aware editing functions correctly, verify the following configuration steps: **1. Frontend Integration** - **Template Variable**: You must include `{{$version}}` in the URL field. If omitted, the version selection dropdown will not appear in the Visual Editor toolbar. - **Directus Frontend Library**: Your website must be configured using our publicly available [Frontend Library](https://directus.com/docs/frontend-library). - **Version-Aware Fetching**: Your code must detect the version parameter from the URL and pass it to the Directus API (e.g., `/items/posts/42?version=draft`). Without this, the site will continue to display "Published" content regardless of your selection. **2. Environment Configuration** Update your Directus instance environment variables to authorize the connection and ensure content refreshes: | Variable | Required Value | Purpose | | :---------------------------------------------- | :--------------------- | :-------------------------------------------------------------------- | | `CONTENT_SECURITY_POLICY_DIRECTIVES__FRAME_SRC` | `""` | Allows your website to be embedded within the Directus Studio iframe. | | `CACHE_AUTO_PURGE` | `true` | Ensures the preview reflects changes immediately after saving edits. | ::callout{color="warning" icon="i-lucide-triangle-alert"} **Critical Setup**: Your website will be unable to communicate with Directus if the `CONTENT_SECURITY_POLICY_DIRECTIVES__FRAME_SRC` directive is missing. :br:br Additionally, without `CACHE_AUTO_PURGE` enabled, the Visual Editor will continue to display stale data until the cache naturally expires. :: ## Editing in the Module Once your URLs are set up, navigate to the visual editor module by selecting it from module bar. Your first entered URL will render in the module. ![An image of the visual editor module open on a page](https://directus.com/docs/img/visual_editor_open_page.png) Navigating between different added URLs can be done by clicking the dropdown in the top toolbar. ![An image of the visual editor module open on a page with the url dropdown open](https://directus.com/docs/img/visual_editor_open_url.png) Hovering over an editable item will highlight it within the module. ![An image of the visual editor module open on a page with a hovered item highlighted](https://directus.com/docs/img/visual_editor_open_hover.png) Click the :icon{name="material-symbols:edit-outline"} icon in the toolbar will highlight all the editable items on the page. ![An image of the visual editor module open on a page with all editable items highlighted](https://directus.com/docs/img/visual_editor_open_all.png) Clicking the :icon{name="material-symbols:edit-outline"} beside an editable element will open an editor in either a drawer, modal, or popover depending on which `mode` was specified in the elements `data-directus` attribute on the frontend. ![An image of the visual editor with the drawer open on a page and an input being edited in a popover](https://directus.com/docs/img/visual_editor_open_popover.png) Once you are done editing your item, click the save button and your website will refresh to show your changes. ## Working with Versions When a URL includes the `{{$version}}` variable, a version dropdown appears in the toolbar of the Visual Editor. ### Selecting a Version The dropdown lists: - **Published** — the published version (default) - **Draft** — the global [draft version](https://directus.com/docs/guides/content/content-versioning#working-with-the-draft-version), always available for collections with versioning enabled If your website URL contains a version key that doesn't match "published" or "draft" (e.g. from a custom query parameter), it will also appear as a dynamic option in the dropdown. ### Version-Aware Editing When a version other than "Published" is selected: - **Only items on collections with versioning enabled** will show editable elements. Items on non-versioned collections are hidden from editing. - **Saving an edit** creates or updates the version for that specific item. If the version doesn't exist yet for the item, it's created automatically on save. - **Items without content in the selected version** display their published version content as a read-only fallback. ::callout{icon="i-lucide-info"} The version dropdown requires the user to have **read** permission on `directus_versions`. Editing in a version additionally requires **create** or **update** permission on `directus_versions`. :: ## Permissions Editable elements are gated by field-level permissions. When visual editing is active, Directus validates each element against the current user's access before making it interactive: - **Admin users** can edit all elements. - **Non-admin users** only see editable overlays on fields they have **update** permission for. - When a **version is selected**, elements are additionally hidden for collections that don't have versioning enabled, and for users without the required `directus_versions` permissions. Elements that fail permission checks remain completely inert — no overlay, no hover effect, no click handler. ## AI-Assisted Editing When [AI Assistant](https://directus.com/docs/guides/ai/assistant) is available, you can add visual elements as context for AI conversations. Hover over an editable element and click the AI icon to select it, then open AI Assistant to send your message with the element as context. ![AI icon on editable element in visual editor](https://directus.com/docs/img/visual-editor-ai-icon.png) For more details on using context attachments, see [Adding Context](https://directus.com/docs/guides/ai/assistant/usage#adding-context). # Customization When developing your website with the Visual Editing [Frontend Library](https://directus.com/docs/guides/content/visual-editor/frontend-library), you are able to customize the various editable elements beyond their default styles. This can be done by applying styling using the included CSS selectors, CSS variables, or even by adding your own custom classes. Once these classes have been applied on the frontend, then they will render when using the visual editor module in the Directus Studio. ## CSS Selectors The library ships with a number of built in CSS Selectors already applied to its various elements. Styles can be applied to these by targeting them with your own custom styles. ```css #directus-visual-editing { /* container div that contains all overlay rectangles */ } .directus-visual-editing-overlay { /* wraps the rectangle */ } .directus-visual-editing-rect { /* the element that will be positioned */ } .directus-visual-editing-rect-highlight { /* a modifier that highlights the element */ } .directus-visual-editing-rect-hover { /* class that applies when the original element is hovered */ } .directus-visual-editing-rect-parent-hover { /* class that applies to the parent element when a child element is hovered */ } .directus-visual-editing-rect-inner { /* the element with the rectangle styles */ } .directus-visual-editing-edit-button { /* the edit button */ } .directus-visual-editing-actions-flipped { /* a modifier on the rect that flips the action buttons below it, applied automatically when the rect is near the top of the viewport */ } ``` ## CSS Variables The library also ships with a number of predefined CSS variables. These can be overwritten with your own custom variables if you prefer. ```css :root { --directus-visual-editing--overlay--z-index: 999999999; --directus-visual-editing--rect--border-spacing: 8px; --directus-visual-editing--rect--border-width: 2px; --directus-visual-editing--rect--border-color: #6644ff; --directus-visual-editing--rect--border-radius: 6px; --directus-visual-editing--rect-hover--opacity: 0.333; --directus-visual-editing--rect-highlight--opacity: 0.333; --directus-visual-editing--actions--offset: 4px; --directus-visual-editing--actions--focus-ring-color: #6644ff; --directus-visual-editing--actions--focus-ring-width: 2px; --directus-visual-editing--actions--focus-ring-offset: 2px; --directus-visual-editing--edit-btn--width: 24px; --directus-visual-editing--edit-btn--height: 24px; --directus-visual-editing--edit-btn--radius: 6px; --directus-visual-editing--edit-btn--bg-color: #6644ff; --directus-visual-editing--edit-btn-hover--bg-color: color-mix(in srgb, #6644ff, #2e3C43 25%); --directus-visual-editing--edit-btn--icon-bg-image: url('data:image/svg+xml,'); --directus-visual-editing--edit-btn--icon-bg-size: 66.6%; --directus-visual-editing--ai-btn--bg-color: #6644ff; --directus-visual-editing--ai-btn-hover--bg-color: color-mix(in srgb, #6644ff, #2e3C43 25%); } ``` ::callout{icon="material-symbols:info-outline"} **Defaults inside the Directus Studio** When the visual editor runs inside the Directus Studio iframe, defaults for several variables are sourced from the active Studio theme (primary color, border radius, button size, focus-ring width/offset) rather than the compiled-in fallbacks shown above. Your own `:root` or `customClass` overrides still take precedence. :: ## Custom Classes Finally, custom classes can be added to all or a subset of elements defined by the library’s [apply method](https://directus.com/docs/guides/content/visual-editor/frontend-library#api) using the `customClass` property. This class will be applied to the `div.directus-visual-editing-overlay` element within the `div#directus-visual-editing` container. ```js apply({ directusUrl, customClass: 'my-class' }) ``` ```css .my-class { --directus-visual-editing--overlay--z-index: 40; --directus-visual-editing--rect--border-spacing: 14px; --directus-visual-editing--rect--border-width: 4px; --directus-visual-editing--rect--border-color: red; --directus-visual-editing--rect--border-radius: 10px; --directus-visual-editing--rect-visible--opacity: 0.5; --directus-visual-editing--edit-btn--width: 20px; --directus-visual-editing--edit-btn--height: 15px; --directus-visual-editing--edit-btn--radius: 2px; --directus-visual-editing--edit-btn--bg-color: lightgreen; --directus-visual-editing--edit-btn--icon-bg-image: url('data:image/svg+xml,'); --directus-visual-editing--edit-btn--icon-bg-size: contain; } ``` # Collaborative Editing ![Collaborative editing thumbnail](https://directus.com/docs/img/collaborative-post.png) Collaborative Editing transforms your Directus project into a real-time collaborative platform where multiple users can edit content simultaneously. This feature provides conflict-free collaborative editing through smart field locking, user awareness indicators, and instant synchronization across all connected clients. This documentation covers everything you need to know about configuring, using, and developing with collaborative editing in your Directus projects. ## Overview Video ::div{style="padding:56.33% 0 0 0;position:relative;"} :iframe{allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media" frameBorder="0" src="https://www.youtube.com/embed//R2Tx35sLm3I" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Directus-Visual-Editor-Preview"} :: ## Key Features - **Real-time Collaboration** - Multiple users edit simultaneously with instant synchronization - **Smart Field Locking** - Automatic conflict prevention through field-level locking - **User Awareness** - Visual indicators show who's editing what in real-time - **Universal Support** - Works across collections, file library, user directory, and relationships - **Easy Configuration** - Deploy globally across your project ## How It Works Collaborative Editing provides a sophisticated real-time collaboration experience: | Feature | Traditional Way | Collaborative Editing | | ------------------------ | ---------------------------- | ------------------------------------------ | | **User Awareness** | No visibility of other users | Real-time avatars show who's editing | | **Conflict Prevention** | Manual coordination required | Automatic field locking prevents conflicts | | **Real-time Updates** | Manual refresh needed | Instant synchronization across all users | | **Relationship Editing** | Limited to single users | Multiple users can edit related content | ## Getting Started Follow these guides to set up collaborative editing in your Directus project: ::callout --- color: primary icon: i-lucide-settings to: https://directus.com/docs/guides/content/collaborative-editing/configuration --- **Configuration** Configure settings and environment variables. :: ::callout --- color: secondary icon: i-lucide-book-open to: https://directus.com/docs/guides/content/collaborative-editing/usage --- **Usage Guide** Learn the basics of collaborative editing. :: ::callout --- color: green icon: i-lucide-square-code to: https://directus.com/docs/guides/content/collaborative-editing/development --- **Development & Custom Extensions** Integrate with custom interfaces. :: ## Requirements - **Directus 11.15.0 or higher** - **[WebSockets enabled](https://directus.com/docs/configuration/realtime)** in your Directus configuration ## Technology Overview The feature uses a custom WebSocket implementation for real-time synchronization and smart field locking to prevent conflicts. All collaborative actions respect Directus user permissions and access controls. ## Next Steps ::callout --- color: primary icon: i-lucide-arrow-right to: https://directus.com/docs/guides/content/collaborative-editing/configuration --- **Ready to get started?** Start with the Configuration guide to get up and running. :: Transform your Directus project into a collaborative workspace where teams can work together seamlessly on content creation and management. # Configuration This guide covers the configuration settings for the Collaborative Editing feature. ## Configuration ::callout{icon="i-lucide-info"} **Enabling Collaborative Editing** If WebSockets are active on your Directus instance, then enabling Collaborative Editing is as simple as checking it on your Directus settings. It will be automatically active across all collections in your instance. For detailed configuration options and environment variables, please refer to the [Collaborative Editing Configuration](https://directus.com/docs/configuration/realtime#collaborative-editing) documentation. :: ## Verification To verify that collaborative editing is working correctly: 1. **Verify WebSockets**: Ensure your Directus instance has WebSockets enabled (`WEBSOCKETS_ENABLED=true`). 2. **Verify Project Settings**: Ensure your Directus instance has Collaborative Editing enabled in Project Settings. 3. **Test Collaboration**: Open any collection item and look for the collaboration indicators (avatars are shown in the header). 4. **Multi-User Test**: Have another user open the same item and you should see their avatar appear. ## Troubleshooting ### WebSocket Connection Failed - Confirm `WEBSOCKETS_ENABLED=true` is set. - Check that your server/proxy supports WebSocket connections. - Verify firewall settings allow WebSocket traffic. ### Redis Issues (Multi-Instance) - Ensure all instances are connected to the same Redis server. - Verify the `WEBSOCKETS_COLLAB_STORE_NAMESPACE` if sharing a Redis instance with other applications (default is `collab`). ### Debug Logging Enable debug logging to troubleshoot issues. ```bash LOG_LEVEL="debug" ``` This will provide detailed information about WebSocket connections, user events, and collaboration activities in your Directus logs. # Usage Guide This guide covers the essential features you'll use when collaborating on content in real-time. ## Visual Indicators ![Collaborative editing indicators](https://directus.com/docs/img/collaborative-editing-explanation.png) When you open any item for editing, you'll see collaboration indicators: - **User avatar stack** - appears in the header to show how the users currently editing the item - **User avatars** appear next to fields when someone is editing them - **Field locking** prevents you from editing fields others are actively using - **Real-time updates** show changes as they happen ## Basic Usage 1. Open any collection item 2. Start editing - your avatar appears for others to see 3. Other users' avatars show which fields they're working on 4. Locked fields automatically unlock when users move away ## Where It Works Collaborative editing works across: - **All collections and items**![Collaborative pages](https://directus.com/docs/img/collaborative-pages.png) - **File library**![File library metadata](https://directus.com/docs/img/collaborative-file-library.png) - **User directory profiles**![User directory profiles](https://directus.com/docs/img/collaborative-user.png) - **Relational fields (even within) and page builders**![Relational fields and page builders](https://directus.com/docs/img/collaborative-relationships-drawer.png) ## Summary Collaborative editing happens automatically once enabled. Multiple users can work on the same content simultaneously without conflicts, with clear visual indicators showing who's working where. ## Known Limitations - Translation forms: The entire form locks rather than on a field-by-field basis. - Relational fields (M2A/M2M): When editing a relational entry in a drawer the entire relational interface will lock rather than on an entry-by-entry basis. - Saving without permissions: When a user tries to save changes on an item while there are changes on a field they don't have write access to, they will receive an error. **Next Steps:** - Test with teammates to see real-time collaboration in action - Check out the [Configuration Guide](https://directus.com/docs/guides/content/collaborative-editing/configuration) if you need to configure settings # Development & Custom Extensions **Collaborative Editing works out-of-the-box for most Custom Interfaces**. Unlike the previous extension-based implementation, you do **not** need to add specific data attributes to your components. The system automatically handles presence, field locking, and synchronization through the `v-form` and `form-field` wrappers. ## How it Works When your Custom Interface is rendered within a Directus Form: 1. **Automatic wrapping**: Your interface is wrapped by the `form-field` component. 2. **Event Detection**: The wrapper listens for standard DOM `focusin` and `focusout` events bubbling up from your component. 3. **State Management**: When your component receives focus, the wrapper automatically notifies the collaboration system to "lock" the field for other users and display your avatar. ## Requirements for Custom Interfaces For your custom interface to fully support collaborative editing, ensure the following: ### 1. Event Bubbling Your component should allow `focus` and `blur` (or `focusin`/`focusout`) events to bubble up to the parent. This is standard behavior for native HTML inputs (``, ` ``` ### Load `upsert.vue` in `router.js` ```js import Upsert from "../views/upsert.vue"; ``` ```js { path: "/note/:id", name: "upsert", meta: { public: false }, component: Upsert, }, ``` ![Create Note](https://directus.com/docs/img/0f21f1a5-ee69-4e45-8535-2200bf985184.webp) ![Edit Note](https://directus.com/docs/img/a5946d5b-75cd-45b6-9f8b-fcded5eb8916.webp) ## Summary In this tutorial, you've learnt how to build a Chrome Extension that authenticates with Directus and allows the user to manage data. There's still some more polish and functionality you can build, but a lot of it will be based on the same concepts we've worked through here. # Build a Realtime Chat App with Directus and Astro Directus offers realtime capabilities powered by WebSockets. You can use these with the Directus SDK to create your own realtime applications. In this tutorial, you will build a chat application using Astro and a Directus project. ## Before You Start You will need: - A Directus project with admin access. - Fundamental understanding of Astro concepts and dynamic island architecture. - Optional but recommended: Familiarity with data modeling in Directus. ## Set Up Your Directus Project ## Enable Websockets in Directus Directus uses websockets to enable realtime capabilities. If you are using the [Directus Cloud](https://directus.com/start){rel=""nofollow""}, websockets are enabled by default. If you are [self-hosting Directus](https://directus.com/docs/self-hosting/overview){rel=""nofollow""}, you will need to enable websockets in your `directus` config file. To enable websockets, update your `docker-compose.yml` config file to include the following: ```yaml environment: WEBSOCKETS_ENABLED: true WEBSOCKETS_HEARTBEAT_ENABLED: true ``` ### Create a Collection Create a new collection called `messages` with the following fields: - `content` (Type: textarea) After which you can go to the optional fields and activate the following: - `user_created` - `date_created` ### Edit Public Policy To allow unauthenticated users to view the posts collection, you need to modify the public [access policy](https://directus.com/docs/guides/auth/access-control){rel=""nofollow""} to offer read access. Ideally, in a real project, you should create a new policy and authenticate users to Directus before allowing them access to your content. To enable access, go to **Settings** -> **Access Policies** -> **Public**, and under Permissions, add `messages` with full access for `create` and `read`. The public policy also needs access to the `directus_users` collection so that the user who created the message can be displayed. To do this, add `directus_users` with custom `read` access to the public policy, and under Field Permissions, uncheck all fields except for `first_name` and `last_name`. ![Public policy for messages and directus\_user ](https://directus.com/docs/img/astro-chat-app-public-policy.png) ### Create a User for Chatting For messages to be sent, they need to be sent by a user. You can create a user in the Directus admin panel by going to **User Directory** -> **Add User** and create a new user. Be sure to remember the email and password you used to create the user, as you will need it to log in to the chat application later in this tutorial. Assign the user to public policy by clicking "Add Existing" under policies and check "Public". ![Directus User Policy](https://directus.com/docs/img/astro-chat-app-user-policy.png) ## Set Up an Astro Project To set up an Astro project, run the following command in your terminal: ```bash npm create astro@latest astro-live-chat ``` This command will create a new Astro project with the name `astro-live-chat`. During installation, when prompted, choose the following configurations: ```bash How would you like to start your new project? A basic, minimal starter Install dependencies? Yes Initialize a new git repository? No ``` Once completed, open the directory in your desired code editor and install the Directus JavaScript SDK using the command: ```bash npm install @directus/sdk ``` Run `npm run dev` in the terminal to start the development server at `http://localhost:4321`. Open the URL on your browser to be sure Astro is set up correctly. ### Configure the Directus SDK First, create a `.env` file in the root of your project and add the following environment variables: ```bash PUBLIC_DIRECTUS_URL=https://your-directus-project-url.com ``` In the `src` directory, create a `lib` directory and inside of it, create a `directus.ts` file to set up your Directus client instance: ```ts /// import { createDirectus, rest, authentication, realtime } from "@directus/sdk"; const DIRECTUS_URL = import.meta.env.PUBLIC_DIRECTUS_URL; const client = createDirectus(DIRECTUS_URL).with(rest()).with(authentication()).with(realtime()); export default client; ``` The code above imports the Directus SDK and sets up the Directus client instance with authentication and realtime from the SDK. ### Install Preact Framework Since building a chat application involves interacting with the client side, a front-end framework is required. While you can use any UI framework of your choice, this tutorial will use [Preact](https://preactjs.com/){rel=""nofollow""} for its lightweight and easy-to-use nature. To install Preact, run the following command in your terminal: ```bash npx astro add preact ``` This command will install Preact and update your `astro.config.mjs` file to use Preact as the default frontend framework. ### Create a Login Form Component Before you can send messages, you need to log in to the chat application. Create a new file called `LoginForm.tsx` in the `src/components` directory and add the following code: ```tsx import { useState } from "preact/hooks"; interface LoginFormProps { formData: { email: string; password: string; }; setFormData: (data: { email: string; password: string }) => void; onSubmit: (e: SubmitEvent) => Promise; } export default function LoginForm({ formData, setFormData, onSubmit }: LoginFormProps) { const [errorMessage, setErrorMessage] = useState(""); async function handleSubmit(e: SubmitEvent) { e.preventDefault(); const form = new FormData(e.target as HTMLFormElement); const email = form.get("email"); const password = form.get("password"); if (!email || !password) { setErrorMessage("Please fill in all fields"); return; } try { await onSubmit(e); } catch (error) { console.log(error); setErrorMessage("Invalid email or password"); } } return (

Login to start chatting

setFormData({ ...formData, email: (e.target as HTMLInputElement).value }) } /> setFormData({...formData, password: (e.target as HTMLInputElement).value }) } /> {errorMessage &&

{errorMessage}

}
); } ``` The code above creates a simple login form with email and password fields. It also handles form submission and displays error messages if the login fails. ### Create a Chat Component Next, create a new file called `Chat.tsx` in the `src/components` directory. This component that will handle sending and receiving messages. In the file, add the following code: ```tsx import { useState } from "preact/hooks"; import client from "../lib/directus"; import LoginForm from "./LoginForm"; interface Message { id: string; content: string; user_created?: { first_name: string; }; } export default function Chat() { const [refreshToken, setRefreshToken] = useState(undefined); const [formData, setFormData] = useState({ email: "", password: "", }); async function submit(e: SubmitEvent) { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const email = formData.get("email"); const password = formData.get("password"); if (!email || !password) { return; } // TODO: Handle via websocket here } return ( <> {refreshToken === undefined ? ( ) : (

Chat

Sign in to start chatting
)} ); } ``` The code above: - Imports the necessary dependencies, including the Directus `client` and the `LoginForm` component. - Defines a `Message` interface to represent the structure of a message. - Sets up state variables for the refresh token, form data, and error message. - Defines a `submit` function to handle form submission and log in to Directus. - Renders the `LoginForm` component if the refresh token is not set, otherwise renders the chat interface. So if the user is not logged in, the login form will be displayed. If the user is logged in, the chat interface will be displayed. At the moment, the chat interface is not implemented yet. You will implement it in the later steps of this tutorial, but first, begin by rendering the Chat component in the src/pages/index.astro file. In the `src/pages/index.astro` file, update the content to include the `Chat` component: ```astro --- import Chat from "../components/Chat"; import Layout from "../layouts/Layout.astro"; --- ``` Adding the `client:load` directive to the `Chat` component will ensure that the component is only loaded on the client side, which is important for components that rely on client-side state and interactions. Navigate to `http://localhost:4321/` and you will be provided you with a UI that looks like this: ![Astro Chat UI](https://directus.com/docs/img/astro-chat-app-login-to-chat.png) To authenticate the realtime client using the handshake mode, you will first need to authenticate the REST client using the email and password from the login form. Then, you can use the [handshake mode](https://directus.com/docs/guides/realtime/authentication#handshake-mode){rel=""nofollow""} to authenticate the realtime client right after calling `directus.connect()`. To do that, inside of the `Chat.tsx` file, create a `initializeWebSocket` function that will handle the websocket connection and authentication: ```tsx async function initializeWebSocket() { await client.connect(); client.sendMessage({ type: "auth", email: formData.email, password: formData.password, }); client.onWebSocket("message", (message) => { try { console.log("Websocket message:", message); if(message.type === "auth" && message.status === "ok") { setRefreshToken(message.refresh_token); //TODO: Add the subscription calls here } // TODO: Receive incoming messages here } catch (error) { console.error("Error parsing websocket message:", error); } }); client.onWebSocket("error", (error) => { console.log("Websocket error:", error); }); } ``` The `initializeWebSocket` function connects to the websocket server and sends an authentication message with the email and password. It also listens for incoming messages and errors from the websocket server. To use the `initializeWebSocket` function, update the `submit` function to call it after successfully logging in, you should add this part just after the `//TODO` comment in the `submit` function: ```tsx // TODO: Handle via websocket here try { const response = await client.login({ email: email as string, password: password as string, }); console.log("Login successful", response); initializeWebSocket(); } catch (error) { console.log(error); } ``` This will call the `initializeWebSocket` function after successfully logging in to Directus, allowing you to start receiving messages in the chat application. ### Subscribe to Incoming Messages At the moment, `directus.onWebSocket("message", {})` receives all messages, however, Directus SDK provides a much better approach to subscribe to specific events. In this case, you can subscribe to the `messages` collection to receive specific fields from any messages as they are created and uniquely identify the subscription with a [UID](https://directus.com/docs/guides/realtime/actions#use-uids-to-better-understand-responses){rel=""nofollow""} for [best practice](https://directus.com/docs/guides/realtime/subscriptions#using-uids){rel=""nofollow""}. To handle incoming messages, start by creating a `subscribeToMessages` function inside the `Chat.tsx` component: ```tsx const [messages, setMessages] = useState([]); async function subscribeToMessages() { try { await client.subscribe("messages", { event: "create", query: { fields: ["id", "content", "user_created.first_name"], }, uid: "messages-subscription", }); } catch (error) { console.error("Subscription error:", error); } } ``` The code above: - Defines a `messages` state variable to store the incoming messages. - Defines a `subscribeToMessages` function that subscribes to the `messages` collection for the `create` event and specifies the fields to receive. - The `uid` is set to `messages-subscription` to uniquely identify the subscription. Next, create a function to handle incoming `messages` and update the messages state variable: ```tsx const addMessageToList = (message: Message) => { setMessages((prev) => [...prev, message]); }; ``` Now that there is a subscription function, you also need a function to receive the incoming messages and call the `addMessageToList` function to update the state variable. You can do this by creating a `receiveMessage` function that will handle the incoming messages: ```tsx const receiveMessage = (data: any) => { if ( data.uid === "messages-subscription" && data.type === "subscription" && data.event === "create" ) { const message = data.data?.[0]; if (message) { addMessageToList(message); } } else if (data.type === "ping") { client.sendMessage({ type: "pong" }); } }; ``` The `receiveMessage` function checks if the incoming message is from the `messages-subscription` and if the event is `create`. If so, it calls the `addMessageToList` function to update the messages state variable. It also handles ping messages by sending a pong message back to the server to keep the connection alive. You can now call the `subscribeToMessages` function, and `receiveMessage` in the `initializeWebSocket` function after the authentication: ```tsx client.onWebSocket("message", (message) => { try { console.log("Websocket message:", message); if(message.type === "auth" && message.status === "ok") { setRefreshToken(message.refresh_token); //TODO: Add the subscription calls here subscribeToMessages(); } // TODO: Receive incoming messages here receiveMessage(message); } catch (error) { console.error("Error parsing websocket message:", error); } }); ``` To display the messages in the chat interface, you can create a `MessageList` component that will render the list of messages. Create a new file called `MessageList.tsx` in the `src/components` directory and add the following code: ```tsx interface Message { id: string; content: string; user_created?: { first_name: string; }; } interface MessageListProps { messages: Message[]; } export default function MessageList({ messages }: MessageListProps) { return (
{messages.map((message) => (
{message.user_created?.first_name || "Unknown User"}: {message.content}
))}
); } ``` Now, you can import the `MessageList` component in the `Chat.tsx` file and render it inside the chat interface: ```tsx import MessageList from "./MessageList"; ``` Then, render the `MessageList` component and pass the `messages` state variable as a prop: ```tsx return ( <> {refreshToken === undefined ? ( ) : (

Chat

)} ); ``` Navigate to `http://localhost:4321/` and you should have a chat interface with the messages being displayed as they are created in Directus when you are logged in. ![Astro Chat UI with messages from Directus Admin](https://directus.com/docs/img/astro-chat-app-admin-messages.png) ### Send Messages To send messages, you need to create a form that will allow users to enter their messages and submit them to Directus. You can create a new file called `MessageForm.tsx` in the `src/components` directory and add the following code: ```tsx interface MessageFormProps { newMessage: string; setNewMessage: (message: string) => void; onSubmit: () => void; } export default function MessageForm({ newMessage, setNewMessage, onSubmit }: MessageFormProps) { const handleSubmit = (e: Event) => { e.preventDefault(); onSubmit(); }; return (
setNewMessage((e.target as HTMLInputElement).value)} />
); } ``` The `MessageForm` component is a simple form that allows users to enter their messages and submit them. It takes the `newMessage`, `setNewMessage`, and `onSubmit` props to handle the message input and submission. Next, head over to the `Chat.tsx` file and make some updates. First import the `MessageForm` component: ```tsx import MessageForm from "./MessageForm"; ``` Then create a new state variable called `newMessage` to store the message input: ```tsx const [newMessage, setNewMessage] = useState(""); ``` Then create a function called `submitMessage` that will handle sending the message to Directus: ```tsx const submitMessage = () => { if (!newMessage.trim()) return; client.sendMessage( JSON.stringify({ type: "items", collection: "messages", action: "create", data: { content: newMessage }, }) ); setNewMessage(""); }; ``` Also create a `Logout` function that will handle logging out of the chat application: ```tsx const logout = () => { client.disconnect() setRefreshToken(undefined); }; ``` Then render the `MessageForm` component in `Chat.tsx` and pass the `newMessage`, `setNewMessage`, and `submitMessage` props to it: ```tsx return ( <> {refreshToken === undefined ? ( ) : (

Chat

)} ); ``` Visit `http://localhost:4321/` and you should now be able to send messages in the chat application. The messages will be sent to Directus and displayed in the chat interface in realtime. ![Astro Chat UI with sending messages form](https://directus.com/docs/img/astro-chat-app-send-messages.png) ## Fetching the Latest Messages On Load Congratulations! You have successfully built a chat application using Astro and Directus. However, there is one last thing to do: fetching the latest messages when the chat application loads. To do this, you can create a new function in `Chat.tsx` called `fetchLatestMessages` that will fetch the latest messages from Directus when the chat application loads: ```tsx const fetchLatestMessages = () => { client.sendMessage( JSON.stringify({ type: "items", collection: "messages", action: "read", query: { limit: 20, sort: "-date_created", fields: ["id", "content", "user_created.first_name"], }, uid: "get-recent-messages", }) ); }; ``` The code above sends a message to Directus to fetch the latest `20` messages from the `messages` collection in Directus. To use this function when the application loads, update the `initializeWebSocket` function to call `fetchLatestMessages` after the authentication: ```tsx //previous code here if (message.type === "auth" && message.status === "ok") { setRefreshToken(message.refresh_token); // Fetch the most recent messages only if they are not loaded if (messages.length === 0) { fetchLatestMessages(); subscribeToMessages(); } } ``` Finally, you need to update the `receiveMessage` function to handle the message events with the `uid` `get-recent-messages` to be able to process older messages and add them to the messages list. You also need to reverse the order of the messages so that the latest messages are displayed at the top of the list. To do this, update the `receiveMessage` function to handle the `get-recent-messages` event with an `else if` statement: ```tsx const receiveMessage = (data: any) => { if ( data.uid === "messages-subscription" && data.type === "subscription" && data.event === "create" ) { const message = data.data?.[0]; if (message) { addMessageToList(message); } } else if (data.type === "ping") { client.sendMessage({ type: "pong" }); }else if (data.uid === "get-recent-messages") { data.data?.reverse().forEach(addMessageToList); } }; ``` Refresh the page and you should have the latest messages displayed when the application loads. ![Latest messages from Astro Chat UI](https://directus.com/docs/img/astro-chat-app-latest-messages.png) ## Handling Connection Stability Directus Realtime uses websockets to provide a realtime connection to the server. However, websockets can be unstable and may disconnect from time to time. Behind the scenes, the Directus SDK handles reconnection automatically by sending a heartbeat or ping every 30 seconds which you already handled in the `receiveMessage` function by sending a pong message back, but you can also handle connection stability in your application by using the handshake mode to re-authenticate the user and re-subscribe to the messages collection when the access token expires. To do this, update the `receiveMessage` function to handle authentication events when expired and re-authenticate the user: ```tsx const receiveMessage = (data) => { if ( data.uid === "messages-subscription" && data.type === "subscription" && data.event === "create" ) { const message = data.data?.[0]; if (message) { addMessageToList(message); } } else if (data.type === "ping") { client.sendMessage({ type: "pong" }); } else if (data.uid === "get-recent-messages") { data.data?.reverse().forEach(addMessageToList); } else if (data.type === "auth" && data.status === "expired") { console.log("Authentication expired, re-authenticating..."); if (refreshToken) { try { client.sendMessage({ type: "auth", refresh_token: refreshToken, }); console.log("Re-authenticated successfully"); } catch (error) { console.error("Re-authentication failed:", error); } } else { console.log("No refresh token available, cannot re-authenticate."); } } }; ``` With this in place, the application will automatically re-authenticate the user and re-subscribe to the messages collection when the access token expires. ## Summary In this tutorial, you learned how to build a chat application using Astro and Directus. You set up a Directus project with a messages collection, enabled websockets, and created an Astro project with Preact. You also created a login form, chat interface, and message form to send and receive messages in realtime. Realtime communication in Directus is a useful feature that allows you to build dynamic applications that can respond to changes in data in real-time. By using the Directus SDK, you can easily integrate realtime capabilities into your applications and create engaging user experiences. You can extend this chat application by adding more features such as: - User authentication and registration - Message timestamps - Message reactions - Styling the chat interface with CSS - Adding a typing indicator - Adding a notification system for new messages # Build a Realtime Chat App with Directus and Nuxt Directus offers realtime capabilities, powered by websockets. You can use these with the Directus SDK to create your own realtime applications. In this tutorial, you will build a chat application using Nuxt and a Directus project. a ## Before You Start You will need: - A Directus project with admin access. - Fundamental understanding of Nuxt concepts. - Optional but recommended: Familiarity with data modeling in Directus. ## Set Up Your Directus Project ### Create a Collection Create a new collection called `messages` with the following fields: - `content` (Type: textarea) After which you can go to the optional fields and activate the following: - `user_created` - `date_created` ### Edit Public Policy So that Nuxt can access the messages collection you need to edit the public policy. Navigate to Settings -> Access Policies -> Public and under Permissions add `messages` with full access for `create` and `read`. The frontend will display the name of the user who created the message so the public policy will also need to have access to the `directus_users` collection. Add `directus_users` with custom `read` access and under Field Permissions check `first_name` and `last_name`. ### Create a User for Chatting Messages will need to be assigned to a user. Create a new user in Directus by navigating to User Directory -> Add User and create a new user. Be sure to remember the email and password you use. Assign the user with the Public policy that was edited in the previous step by clicking "Add Existing" under policies and selecting "Public". ### Configure Realtime Directus Realtime may disabled on self-hosted projects. To enable it if you are using Docker, edit your `docker-compose.yml` file as follows: ```yml environment: WEBSOCKETS_ENABLED: "true" WEBSOCKETS_HEARTBEAT_ENABLED: "true" ``` If you use Directus Cloud to host your project, you do not need to manually enable Realtime. ## Set Up Your Nuxt Project ### Initialize Your Project Create a new Nuxt project using [Nuxi](https://nuxt.com/docs/api/commands/init){rel=""nofollow""}: ```bash npx nuxi@latest init directus-realtime cd directus-realtime ``` Note: Just hit enter when asked to select additional packages (none are required for this project). ### Configure Nuxt Configure Nuxt so that it is able to communicate with the (external) Directus API. Create a `.env` file with the Directus URL: ```text API_URL="http://0.0.0.0:8055" ``` Add a type definition for our new environment variable by creating an `env.d.ts` file with the following content: ```ts /// interface ImportMetaEnv { readonly API_URL: string; } interface ImportMeta { readonly env: ImportMetaEnv; } ``` Depending on your project configuration and if you are in development or production you may need to configure a Nuxt proxy to allow access between your Nuxt project and Directus in your `nuxt.config.ts`: ```ts routeRules: { "/directus/**": { proxy: `${import.meta.env.API_URL}/**` }, }, ``` This will allow your Nuxt project to access directus via your Nuxt URL, eg. {rel=""nofollow""} Inside your Nuxt project, install the Directus SDK package by running: ```bash npm install @directus/sdk ``` ### Define a Directus Schema TypeScript needs to know what the structure of the Directus data is. To achieve this create a `directus.d.ts` file in the root of our project which defines our schema: ```ts /// interface DirectusSchema { messages: Message[]; } interface Message { id: number; content: string; user_created: string; date_created: string; } ``` ### Use Nuxt page router Configure Nuxt to use the page router by editing `app.vue` replacing the content with: ```html ``` ### Create a Directus plugin Create a Nuxt plugin to streamline accessing Directus throughout your application. Create a new file `plugins/directus.ts` Copy and paste in the code below, replace the `your-website-url` with your Nuxt URL and port: ```ts import { createDirectus, realtime } from "@directus/sdk"; const directus = createDirectus( "http://your-website-url/directus", ).with(realtime()); export default defineNuxtPlugin(() => { return { provide: { directus }, }; }); ``` This file handles all the interaction with Directus and provides Nuxt with the required Directus SDK features. ### Create a Login Form The chat system will need to know who is sending messages to Directus so the user will need to login before they can send messages. The websocket will return a refresh token Nuxt can use this to determine if a user is logged in. In `pages/index.vue` script set up add some variables to store the token and the login credentials. ```ts ``` Then, in the template, add a form to capture the user's email and password and display it if there is no token. ```html ``` If you run `npm run dev` and navigate to `http://localhost:3000` you should see a login form. Directus Realtime (Websockets) will be used to authenticate the user as well as send and receive messages. To connect the client to Directus use [handshake mode](https://directus.com/docs/guides/realtime/authentication#handshake-mode){rel=""nofollow""} which requires a connection followed quickly and immediately by an authentication. After the variable definitions in `pages/index.vue` script setup add the following code: ```ts const saveRefreshToken = (token: string) => { refreshToken.value = token localStorage.setItem('directus_refresh_token', token) } onMounted(() => { const storedToken = localStorage.getItem('directus_refresh_token') if (storedToken) { refreshToken.value = storedToken $directus.connect() $directus.onWebSocket('open', () => { $directus.sendMessage({ type: 'auth', refresh_token: storedToken }) }) } else { $directus.connect() } const cleanup = $directus.onWebSocket('message', (message) => { if (message.type === 'auth' && message.status === 'ok') { saveRefreshToken(message.refresh_token) } }) onBeforeUnmount(cleanup) }) const login = async () => { const login = { type: 'auth', email: credentials.value.email, password: credentials.value.password } $directus.sendMessage(JSON.stringify(login)) } ``` The code added above does the following: 1. Check if there is an existing refresh token in local storage. If there is, connect to Directus and authenticate using the refresh token. If not, just connect to Directus. 2. Set up a listener for the `message` event on the websocket. When any message is received, check if it is an authentication message and if it is, save the refresh token to local storage. 3. Provide a login function that sends the credentials from the login form to Directus for authentication. Visit `http://your-website-url` and try logging in with the user you created in Directus in the steps above. ### Subscribe to Incoming Messages Although `$directus.onWebSocket('message', (message) => {}` will receive all messages, the Directus SDK provides a more convenient way to subscribe to specific events. In this case the client can subscribe to the `messages` collection to receive specific fields from any messages as they are created and uniquely identify our subscription with a [UID](https://directus.com/docs/guides/realtime/actions#use-uids-to-better-understand-responses){rel=""nofollow""} for [best practice](https://directus.com/docs/guides/realtime/subscriptions#using-uids){rel=""nofollow""}. At the bottom of the setup script in `pages/index.vue` add the following code: ```ts const messageList: Ref = ref([]) const subscribe = async (event) => { const { subscription } = await $directus.subscribe('messages', { event, query: { fields: ['*', 'user_created.first_name'], }, uid: "messages-subscription" }) for await (const message of subscription) { receiveMessage(message) } } const receiveMessage = (data) => { if (data.type === 'ping') { $directus.sendMessage({ type: 'pong', }) } if (data.type === 'subscription' && data.event === 'create') { const message = data.data[0] addMessageToList(message) } } const addMessageToList = (message: Message) => { messageList.value.push(message) } ``` This subscribes to the `messages` collection when the user is authenticated. Update the cleanup function to include the subscription: ```ts const cleanup = $directus.onWebSocket('message', (message) => { if (message.type === 'auth' && message.status === 'ok') { saveRefreshToken(message.refresh_token) subscribe('create') } }) ``` Then display the message list in the template by updating the `else` condition: ```html

Chat

{{ message.user_created.first_name }}: {{ message.content }}
``` Visit `http://your-website-url` and you should see an empty chat window after logging in. **Be sure to refresh the page rather than relying on hot reload which may cause connections issues with websockets**. Go back to Directus (hint: this is best done with 2 browser windows side by side) and create a new message in the `messages` collection. You should see the message appear in the chat window. ## Send Messages Having proven that Nuxt can receive messages created in Directus, add a new form to our template to send messages from Nuxt. In the template section of `pages/index.vue` replace the existing `else` statement with the following: ```html

Chat

{{ message.user_created.first_name }}: {{ message.content }}
``` Now add code to the script setup section of `pages/index.vue` to make the form work. Directly under the last function, add the following: ```ts const newMessage: Ref = ref('') const messageSubmit = () => { $directus.sendMessage({ type: 'items', collection: 'messages', action: 'create', data: { content: newMessage.value }, }) newMessage.value = '' } const logout = () => { $directus.sendMessage({ type: 'auth', action: 'logout', }) refreshToken.value = undefined localStorage.removeItem('directus_refresh_token') } ``` Visit your website url again (remember to refresh) and enter a message in the form and submit it. The message should appear in the chat window, with the first name of the user. You can also logout of the chat by clicking the logout button but if you do this you will notice the previously added messages have disappeared. ## Fetching the Latest Messages On Load When the page first loads there are no messages in the chat window. This can be fixed by making a request for the latest messages from Directus using a realtime message when the page first loads. Add another function to the script setup section of `pages/index.vue`: ```ts const readAllMessages = () => { $directus.sendMessage({ type: 'items', collection: 'messages', action: 'read', query: { limit: 10, sort: '-date_created', fields: ['*', 'user_created.first_name'], }, uid: 'get-recent-messages' }) } ``` To call this function when the page loads, replace the `cleanup` function with the following: ```ts const cleanup = $directus.onWebSocket('message', (message) => { if (message.type === 'auth' && message.status === 'ok') { saveRefreshToken(message.refresh_token) if (messageList.value.length === 0) { readAllMessages() subscribe('create') } } // The only message of type items required to process is the initial array of messages // All other messages are handled by the subscription if (message.uid === 'get-recent-messages' && message.type === 'items') { for (const item of message.data) { messageList.value.unshift(item) } } }) ``` When the message list is returned Nuxt can identify it by the `uid` that was set in the `readAllMessages` function. Messages are then added to the message list in reverse order so that the most recent messages are at the bottom. Visit your website url again and refresh the page. You should see the last 10 messages in the chat window. ## Handling Connection Stability Directus Realtime uses websockets to maintain a connection to the server. Behind the scenes Directus is sending a heartbeat or ping message every 30 seconds to keep the connection alive. If the connection is lost, then the user will not receive updates. Nuxt already responds to this message in `receiveMessage` by sending a pong message back to Directus. To ensure a stable connection [use the refresh token from handshake mode](https://directus.com/docs/guides/realtime/authentication#handshake-mode){rel=""nofollow""} to re-authenticate the user and re-subscribe to the messages collection. At the bottom of the script setup section in `pages/index.vue` add the following code: ```ts $directus.onWebSocket('close', () => { if (refreshToken.value) { $directus.connect() $directus.sendMessage({ type: 'auth', refresh_token: refreshToken.value }) } }) ``` Now if the connection is lost, Nuxt will attempt to reconnect and re-authenticate the user. ## Summary Realtime communication via websockets is a powerful feature of Directus that can be used, not just for message communication but also user authentication and data filtering and synchronization. The full code from this tutorial can be found on [Github](https://github.com/craigharman/directus-guest-authoring/tree/master/019-directus-realtime-chat){rel=""nofollow""}. # Build a Realtime Chat App with Directus and Next.js Directus offers realtime capabilities, powered by websockets. You can use these with the Directus SDK to create your own realtime applications. In this tutorial, you will build a chat application using Next.js and a Directus project. ## Before You Start You will need: - A Directus project with admin access. - Fundamental understanding of Next.js and React concepts. - Optional but recommended: Familiarity with data modeling in Directus. ## Set Up Your Directus Project ### Create a Collection Create a new collection called `messages` with the following fields: - `content` (Type: textarea) After which you can go to the optional fields and activate the following: - `user_created` - `date_created` ### Edit Public Policy To allow viewing the posts collections and field listings as an unauthenticated user, you will need to modify the public [access policy](https://directus.com/docs/guides/auth/access-control){rel=""nofollow""}. In a real project, you would want to create a new policy and authenticate users to Directus before allowing them access to your content. To do this, navigate to **Settings** -> **Access Policies** -> **Public** and under Permissions add `messages` with full access for `create` and `read`. The Next.js app will display the name of the user who created the message so the public policy will also need to have access to the `directus_users` collection. Add `directus_users` with custom `read` access and under Field Permissions check `first_name` and `last_name`. ### Create a User for Chatting Messages will need to be assigned to a user. Create a new user in Directus by navigating to **User Directory** -> **Add User** and create a new user. Be sure to remember the email and password you use. Assign the user with the Public policy that was edited in the previous step by clicking "Add Existing" under policies and selecting "Public". ### Configure Realtime Directus Realtime may disabled on self-hosted projects. To enable it if you are using Docker, edit your `docker-compose.yml` file as follows: ```yml environment: WEBSOCKETS_ENABLED: "true" WEBSOCKETS_HEARTBEAT_ENABLED: "true" ``` If you use Directus Cloud to host your project, you do not need to manually enable Realtime. ### Configure CORS You may need set your content security policy to allow your Next.js app to access the Directus instance. For example if you are self-hosting, or in development, and using Docker, then you can do this by adding the following environment variable to your `docker-compose.yml` file: ```yml environment: CONTENT_SECURITY_POLICY_DIRECTIVES__FRAME_SRC: your-website-url ``` > Replace `your-website-url` with your Next.js app's URL and the port. eg. if your app URL is in development is `http://localhost:3000`, replace `your-website-url` with `localhost:3000`. ## Set Up Your Next.js Project Next, create a new Next.js app by running the following command: ```bash npx create-next-app \ directus-next-chat \ --js \ --app \ --eslint \ --no-src-dir \ --no-tailwind \ --turbopack \ --import-alias "@/*" ``` Next, change your terminal's working directory into the newly created project directory and install the Directus SDK into it: ```bash cd directus-next-chat npm i @directus/sdk ``` Now, open the project directory in your code editor to start building the app. First of all, clear out the CSS in `app/globals.css` and replace the code in `app/page.js` with the following: ```js export default function Home() { return
} ``` ### Set up Directus To make it easy to access the Directus instance through the SDK, it is recommended to create a helper file that you can import anywhere in your Next.js app. To do that, create a new directory called `lib` in the project directory and save the following code snippet in a file called `directus.js` in it: ```js import { createDirectus, authentication, realtime, rest } from '@directus/sdk'; const url = 'http://localhost:8055'; export const directus = createDirectus(url) .with(authentication()) .with(rest()) .with(realtime()); export default directus; ``` Important: Because Next.js extends the native fetch API with a `force-cache` configuration by default, you may sometimes run into scenarios where Next.js returns stale data. To fix this, update the `rest()` composable to add the following option: ```js .with( rest({ onRequest: (options) => ({ ...options, cache: 'no-store' }), }) ) ``` ### Create a Login Form To start things off, you will need to allow the user to sign into the app using their email and password. To implement that, paste the following code in the `app/page.js` file: ```js 'use client'; import { useState, useEffect } from 'react'; import directus from '@/lib/directus'; export default function Home() { const [credentials, setCredentials] = useState({ email: '', password: '', }); const [refreshToken, setRefreshToken] = useState(undefined); // Handle login via WebSocket const login = async () => { // TODO }; // Handle input change const handleInputChange = (e) => { const { name, value } = e.target; setCredentials((prev) => ({ ...prev, [name]: value, })); }; return (

Directus Realtime Chat

{refreshToken === undefined ? (

Login



) : (

Chat

Signed in!

)}
); } ``` If you run `npm run dev` and navigate to `http://localhost:3000` you should see a login form. ![Login form](https://directus.com/docs/img/next-login-form.png) To authenticate the realtime client using the default handshake mode, you will first need to authenticate the REST client using the email and password that the user provides in the login form. Then, you can use the [handshake mode](https://directus.com/docs/guides/realtime/authentication#handshake-mode){rel=""nofollow""} to authenticate the realtime client right after calling `directus.connect()`. To do that, add the following functions to the Home component: ```js // Handle login via WebSocket const login = async () => { const authResponse = await directus.login({ email: credentials.email, password: credentials.password }, { mode: "json", }); console.log("Login successful", authResponse); setupWebSocket() }; // Connects the realtime client, authenticates via handshake, and sets up the message listener async function setupWebSocket() { await directus.connect(); directus.sendMessage({ type: "auth", email: credentials.email, password: credentials.password, }) directus.onWebSocket('message', (message) => { try { console.log('Received WebSocket message:', message); if (message.type === 'auth' && message.status === 'ok') { setRefreshToken(message.refresh_token); } } catch (err) { console.error('Error parsing WebSocket message:', err); } }); directus.onWebSocket('error', (err) => { console.error('WebSocket error:', err); }); } ``` Now, visit `http://localhost:3000` and try logging in with the user you created in Directus in the steps above. ### Subscribe to Incoming Messages Although `directus.onWebSocket('message', {})` will receive all messages, the Directus SDK provides a more convenient way to subscribe to specific events. In this case, you can subscribe to the `messages` collection to receive specific fields from any messages as they are created and uniquely identify your subscription with a [UID](https://directus.com/docs/guides/realtime/actions#use-uids-to-better-understand-responses){rel=""nofollow""} for [best practice](https://directus.com/docs/guides/realtime/subscriptions#using-uids){rel=""nofollow""}. To do that, add the following code to the Home component: ```js const [messages, setMessages] = useState([]) async function subscribeToMessages() { try { await directus.subscribe("messages", { event: "create", query: { fields: ["id", "content", "user_created.first_name"], }, uid: 'messages-subscription', }); } catch (error) { console.error("Subscription error:", error); } } const addMessageToList = (message) => { setMessages((prev) => [...prev, message]); }; const receiveMessage = (data) => { if ( data.uid === 'messages-subscription' && data.type === 'subscription' && data.event === 'create' ) { const message = data.data?.[0]; if (message) { addMessageToList(message); } } else if (data.type === 'ping') { directus.sendMessage({ type: 'pong' }); } }; ``` Now, you just need to subscribe to the `messages` collection when the user is authenticated. Update the "message" event listener to include the subscription and the message processing calls: ```js directus.onWebSocket('message', (message) => { try { console.log('Received WebSocket message:', message); if (message.type === 'auth' && message.status === 'ok') { setRefreshToken(message.refresh_token); // Add the subscription call here subscribeToMessages(); } // Process received messages receiveMessage(message); } catch (err) { console.error('Error parsing WebSocket message:', err); } }); directus.onWebSocket('error', (err) => { console.error('WebSocket error:', err); }); ``` Then display the message list in the page by updating the `else` condition with the following JSX: ```jsx

Chat

{messages.map((message) => (
{message.user_created?.first_name || 'Anonymous'}: {message.content}
))}
``` You can now visit `http://localhost:3000` and you should see an empty chat window after logging in. Try going back to Directus and creating a new message in the `messages` collection. You should see the message appear in the chat window. ![Received message](https://directus.com/docs/img/next-chat-message.png) ## Send Messages Now that your Next.js app can receive messages, it's time to enable it to send messages as well! To To do that, you will need to add a new form to the JSX. In the `return` statement of the Home component, replace the contents of the `else` condition with the following: ```jsx

Chat

{/* Message list */} {messages.map((message) => (
{message.user_created?.first_name || 'Anonymous'}: {message.content}
))} {/* Message form */}
{ e.preventDefault(); submitMessage(); }} > setNewMessage(e.target.value)} />
{/* Logout button */}
``` You will also need to create a state container to hold the contents of the message as the user types it, the submit function to send the message to Directus as the authenticated user, and a logout function to log the user out when needed. To implement these, paste the following code in your Home component: ```ts const [newMessage, setNewMessage] = useState(''); const submitMessage = () => { if (!newMessage.trim()) return; directus.sendMessage( JSON.stringify({ type: 'items', collection: 'messages', action: 'create', data: { content: newMessage }, }) ); setNewMessage(''); }; const logout = () => { directus.disconnect() setRefreshToken(undefined); }; ``` You can now visit your website URL again and enter a message in the form and send it. The message should appear in the chat window, with the first name of the user. You can also logout of the chat by clicking the logout button but if you do this you will notice the previously added messages have disappeared. ## Fetching the Latest Messages On Load Now that your Next.js app can send and receive messages, it's time to configure it to load old messages upon logging in. To do that, create the `readAllMessages` function in the Home component: ```js const readAllMessages = () => { directus.sendMessage( JSON.stringify({ type: 'items', collection: 'messages', action: 'read', query: { limit: 100, sort: '-date_created', fields: ['*', 'user_created.first_name'], }, uid: 'get-recent-messages', }) ); }; ``` To call this function when the page loads, update the message event listener: ```ts directus.onWebSocket('message', (message) => { try { console.log('Received WebSocket message:', message); if (message.type === 'auth' && message.status === 'ok') { setRefreshToken(message.refresh_token); // Fetch recent messages only if not already loaded if (messages.length === 0) { readAllMessages(); subscribeToMessages(); } } receiveMessage(message); } catch (err) { console.error('Error parsing WebSocket message:', err); } }); directus.onWebSocket('error', (err) => { console.error('WebSocket error:', err); }); ``` Finally, you also need to update the `receiveMessage` function to handle the message events with the uid `get-recent-messages` to be able to process the older messages and add them to the messages list. You'll also need to reverse the list to ensure that the most recent messages are at the bottom. To do that, replace the `receiveMessage` function with the following: ```js const receiveMessage = (data) => { if ( data.uid === 'messages-subscription' && data.type === 'subscription' && data.event === 'create' ) { const message = data.data?.[0]; if (message) { addMessageToList(message); } } else if (data.type === 'ping') { directus.sendMessage({ type: 'pong' }); } else if (data.uid === 'get-recent-messages') { data.data?.reverse().forEach(addMessageToList); } }; ``` You can now visit your website url again and try logging in again. You should see the last messages in the chat window (up to the last 100 messages). ## Handling Connection Stability As you have seen before, Directus Realtime uses websockets to maintain a connection to the server. Behind the scenes, the Directus instance sends a heartbeat or ping message every 30 seconds to keep the connection alive. If the connection is lost, then the client will not receive updates. You already respond to this message in `receiveMessage` by sending a pong message back to Directus. However, to ensure a more stable connection, you can [use the refresh token from handshake mode](https://directus.com/docs/guides/realtime/authentication#handshake-mode){rel=""nofollow""} to re-authenticate the user and re-subscribe to the messages collection when the access token expires. To do that, update the `receiveMessage` function to handle the `expired` auth message appropriately: ```js const receiveMessage = (data) => { if ( data.uid === 'messages-subscription' && data.type === 'subscription' && data.event === 'create' ) { const message = data.data?.[0]; if (message) { addMessageToList(message); } } else if (data.type === 'ping') { directus.sendMessage({ type: 'pong' }); } else if (data.uid === 'get-recent-messages') { data.data?.reverse().forEach(addMessageToList); } else if (data.type === "auth" && data.status === "expired") { console.log("Authentication expired, re-authenticating..."); if (refreshToken) { try { directus.sendMessage({ type: "auth", refresh_token: refreshToken, }); console.log("Re-authenticated successfully"); } catch (error) { console.error("Re-authentication failed:", error); } } else { console.log("No refresh token available, cannot re-authenticate."); } } }; ``` Now if the connection is lost, the app will attempt to reconnect and re-authenticate the user. ## Summary Realtime communication via websockets is a powerful feature of Directus that can be used, not just for message communication but also user authentication and data filtering and synchronization. # Build a Realtime Chat App with Directus and SvelteKit Directus offers real-time capabilities powered by WebSockets. You can use these with the Directus SDK to create your own real-time applications. In this tutorial, you will build a chat application using SvelteKit and a Directus project. ## Before You Start You will need: - A Directus project with admin access. - Fundamental understanding of Svelte concepts. - Optional but recommended: Familiarity with data modeling in Directus. ## Set Up Your Directus Project ## Configure Cors and WebSocket You also need to configure CORS and WebSocket. Update your `docker-compose.yml` file as follows: ```bash WEBSOCKETS_ENABLED: "true" CORS_ENABLED: "true" CORS_ORIGIN: "http://localhost:5173" CORS_CREDENTIALS: "true" ``` ### Create a Collection Create a new collection called `messages` with the following fields: - `content` (Type: textarea) After which, you can go to the optional fields and add the following: - `user_created` - `date_created` ![image displaying the messages\_colection](https://directus.com/docs/img/sveltekit_message_collection.png) ### Edit Public Policy Navigate to Settings -> Access Policies -> Public. Under `messages` grant full access for `create` and `read`. ## Set Up Your Sveltekit Project ### Initialize Your Project To start building, you need to install SvelteKit and Directus sdk. Run this command to install SvelteKit: ```bash npx sv create realtime-app ``` When prompted, select SvelteKit minimal as the template. Do not add type checking, as this tutorial is implemented in JavaScript. Your output should look like this: ```bash Welcome to the Svelte CLI! (v0.6.16) │ ◇ Which template would you like? │ SvelteKit minimal │ ◇ Add type checking with TypeScript? │ No │ ◆ Project created │ ◇ What would you like to add to your project? (use arrow keys / space bar) │ none │ ◇ Which package manager do you want to install dependencies with? │ npm │ ◆ Successfully installed dependencies │ ◇ Project next steps ─────────────────────────────────────────────────────╮ │ │ │ 1: cd realtime-app │ │ 2: git init && git add -A && git commit -m "Initial commit" (optional) │ │ 3: npm run dev -- --open ``` Afterward, `cd` into your project directory and install the Directus SDK by running this command: ```bash npm install @directus/sdk ``` You need to initialize Directus SDK in your project. Create a file called `directus.js` inside the `./src/lib` directory. Add the following code: ```javascript import { createDirectus, authentication, realtime, rest } from "@directus/sdk"; const directusURL = "http://localhost:8055"; export const directus = createDirectus(directusURL) .with(authentication()) .with(rest()) .with(realtime()); ``` ### Create a Login Form Create a file called `+page.svelte` file in the `./src/route` directory. Add the following code: ```javascript ``` In the code above we use WebSocket authentication via [handshake mode](https://directus.com/docs/guides/realtime/authentication#handshake-mode){rel=""nofollow""} to connect to Directus in real-time. When the WebSocket starts, the app sends authentication details to stay connected. The authentication function handles login, stores tokens, loads recent messages, and reconnects automatically if the connection drops or authentication expires. ### Subscribe to Incoming Messages Add the following code at the bottom of the `script` in your `./src/routes/+page.svelte`: ```javascript async function subscribeToMessages() { try { const { subscription } = await directus.subscribe("messages", { event: "create", query: { fields: ["id", "content", "user_created.first_name"], }, }); for await (const event of subscription) { receiveMessage(event); } } catch (error) { console.error("Subscription error:", error); if (websocketConnected) { websocketConnected = false; attemptReconnect(); } } } ``` The `subscribeToMessages()` function sets up a real-time listener for new messages in Directus using WebSocket subscriptions. It subscribes to the `messages` collection, requesting only the message ID, content, and sender’s first name while also including a [UID](https://directus.com/docs/guides/realtime/actions#use-uids-to-better-understand-responses){rel=""nofollow""} for [good practice](https://directus.com/docs/guides/realtime/subscriptions#using-uids){rel=""nofollow""} This allows the app to match responses with specific requests, improving reliability when handling multiple subscriptions. As new messages arrive, the function processes each event in a loop and calls `receiveMessage(event)`, ensuring real-time updates in the app. ## Send Messages To begin sending messages, add the following code at the bottom of the script in your `.src/routes/+page.svelte` file ```javascript const sendMessage = async (event) => { event.preventDefault(); if (!messageContent.trim() || !refreshToken) return; try { if (!websocketConnected) { await connectWebSocket(); } await directus.sendMessage({ type: "items", collection: "messages", action: "create", data: { content: messageContent }, }); console.log("Message sent via WebSocket"); messageContent = ""; } catch (error) { console.error("Failed to send message:", error); if (!websocketConnected) { attemptReconnect(); } } }; ``` The `sendMessage` function handles sending a new message via WebSocket in Directus. It first prevents the default form submission behavior and checks if the message content is empty or if the user is not logged in, in which case it stops execution. If the WebSocket is not connected, it attempts to reconnect before sending the message. It then sends the message as a create action in the "messages" collection using Directus' WebSocket API. If successful, it logs confirmation and clears the message input. If sending fails, it logs the error, and if the WebSocket is disconnected, a reconnection attempt is triggered. ## Fetching the Latest Messages On Load Add the following code at the bottom of the `script` in your `./src/routes/+page.svelte`.: ```javascript async function receiveMessage(newMessage) { console.log("New message received with UID:", newMessage.uid, newMessage); if (newMessage.data && Array.isArray(newMessage.data)) { messages = [ ...messages, ...newMessage.data.map((msg) => ({ id: msg.id, content: msg.content, user: msg.user_created?.first_name || "User", })), ]; await tick(); } } ``` The receiveMessage function processes incoming WebSocket messages and ensures they belong to the correct subscription by checking the UID before updating the app. If valid, it extracts the message ID, content, and sender’s first name, then updates the message list. ## Display Incoming Messages To display the messages, you need to add the UI templates for the chats. Right after the script tag in your `./src/routes/+page.svelte`, add the following code: ```javascript
{#if !loggedIn}

Login

{:else}
Chat Room {websocketConnected ? "• Connected" : "• Disconnected"}
    {#each messages as msg (msg.id)}
  • {msg.user}: {msg.content}
  • {/each}
{/if}
``` This manages the login form and the real-time chat interface, switching between them based on the user's authentication status. ## Handling Connection Stability Add the following code at the bottom of the `script` in your `./src/routes/+page.svelte`: ```javascript function setupWebSocketEventHandlers() { directus.onWebSocket("close", () => { console.log("WebSocket connection closed"); websocketConnected = false; if (refreshToken) { attemptReconnect(); } }); directus.onWebSocket("error", (error) => { console.error("WebSocket error:", error); websocketConnected = false; }); directus.onWebSocket("message", async (message) => { if (message.type === "ping") { directus.sendMessage({ type: "pong" }); } if (message.uid === "get-recent-messages") { console.log("Received past messages:", message); if (message.data && Array.isArray(message.data)) { const pastMessages = [...message.data].reverse().map((msg) => ({ id: msg.id, content: msg.content, user: msg.user_created?.first_name || "User", })); messages = [...pastMessages, ...messages]; await tick(); } } if (message.type === "auth" && message.status === "expired") { console.log("Authentication expired, re-authenticating..."); if (refreshToken) { try { await directus.sendMessage({ type: "auth", refresh_token: refreshToken, }); console.log("Re-authentication successful"); } catch (error) { console.error("Re-authentication failed:", error); attemptReconnect(); } } else { console.log("No refresh token available, cannot re-authenticate."); attemptReconnect(); } } }); } function attemptReconnect() { if (reconnectAttempts >= maxReconnectAttempts) { console.log("Max reconnect attempts reached. Please log in again."); dispatch("connectionLost"); return; } reconnectAttempts++; setTimeout(async () => { if (!websocketConnected && refreshToken) { try { await directus.connect(); websocketConnected = true; await directus.sendMessage({ type: "auth", refresh_token: refreshToken, }); console.log("Reconnected and authenticated successfully"); subscribeToMessages(); reconnectAttempts = 0; reconnectDelay = 2000; } catch (error) { console.error("Reconnection failed:", error); reconnectDelay = Math.min(reconnectDelay * 1.5, 30000); attemptReconnect(); } } }, reconnectDelay); } ``` The `setupWebSocketEventHandlers()` and `attemptReconnect()` functions ensures a stable WebSocket connection by handling authentication expiration and keeping the session alive respectively. ## Test the Application To test the application, run this command: ```bash npm run dev ``` Afterward, open **{rel=""nofollow""}** in your browser. You should see a login form displayed: ![image showing the login page](https://directus.com/docs/img/sveltekit_realtime_chat_login.png) Next, you'll see an empty chat. Go to the Directus dashboard and create a new message in the 'Messages' collection. After that, you should see the message displayed in the chat box, as shown in the image below. ![image showing the real-time chat section](https://directus.com/docs/img/sveltekit_realchat.png) You can also interact with the chat box by sending new messages, as shown in the image below. ![images showing the interaction](https://directus.com/docs/img/sveltekit_interaction.png) ## Summary In this tutorial, you built a real-time chat application using Directus, SvelteKit, and WebSockets. You can expand it by adding features like user presence indicators, typing notifications, or even file sharing. # Build a Testimonial Widget with SvelteKit and Directus In this tutorial, we will setup a testimonial widget using SvelteKit and Directus as a backend. ## Before You Start You will need: - To install Node.js and a code editor on your computer. - A Directus project - follow our [quickstart guide](https://directus.com/docs/getting-started/overview) if you don't already have one. - Some knowledge of Svelte and SvelteKit. ## Setting Up Your Directus Project Create a `testimonials` collection with the following fields: - `full_name` (Type: String, Interface: Input): To capture the user's full name. - `email_address` (Type: String, Interface: Input): To store the user's email address. - `review` (Type: Text, Interface: TextArea): To store the user's testimonials. Then give the public role full access to create and read items in the `testimonials` collection. Create 3 example testimonials from the content module. ## Initializing a Svelte project Initialize a new Svelte project by running the following command: ```bash npm create svelte@latest testimonial-frontend # Choose Skeleton project cd testimonial-frontend npm install npm install @directus/sdk ``` Type `npm run dev` in your terminal to start the Vite development server and open {rel=""nofollow""} in your browser to access the Svelte website. ## Setting Up the Directus SDK To make the Directus SDK available to your project, you need to setup a wrapper for the Directus SDK. Add a `directus.js` file to the `./src/lib` directory and add the following to the file. ```js import { createDirectus, rest } from '@directus/sdk'; import { PUBLIC_API_URL } from '$env/static/public'; function getDirectusInstance(fetch) { const options = fetch ? { globals: { fetch } } : {}; const directus = createDirectus(PUBLIC_API_URL, options).with(rest()); return directus; } export default getDirectusInstance; ``` Add a `hooks.server.js` file to your `./src` directory, and add the following to the file. ```js export async function handle({ event, resolve }) { return await resolve(event, { filterSerializedResponseHeaders: (key, value) => { return key.toLowerCase() === 'content-type'; }, }); } ``` The `hooks.server.js` ensures that request headers required by the Directus backend are added to every request sent from your frontend to the Directus server. Create a `.env` file in your project’s root directory and add the following to the file ```bash PUBLIC_API_URL='directus_server_url' ``` Change `directus_server_url` to the URL of your Directus project. ### Fetching Data From Directus Add a `+page.js` file to your `./src/routes` directory, and add the following content to the file. ```js /** @type {import('./$types').PageLoad} */ import getDirectusInstance from "$lib/directus"; import { error } from "@sveltejs/kit"; import { readItems } from "@directus/sdk"; export async function load({ fetch }) { const directus = getDirectusInstance(fetch); try { return { testimonials: await directus.request(readItems("testimonials")), }; } catch (err) { error(err); } } ``` The `load` function fetch data from your testimonials collection on every page load. Update your `+page.svelte` file to the following. ```js
{data.testimonials[0].full_name}
{data.testimonials[0].email_address}
{data.testimonials[0].review}
``` Your page should contain information from your testimonials collection. ## Create a Testimonial Carousel Add a `TestimonialCard.svelte` and `TestimonialCarousel.svelte` file to your `./src/lib` directory. Add the following to your `TestiomonialCard.svelte` file: ```js
{review}
{full_name} {email_address}
``` This code displays individual testimonial data in a Card. Add the following to your `TestimonialCarousel.svelte` file to implement the testimonial carousel: ```js ``` Update your `+page.svelte` file: ```js

Product testimonials

``` Your page should change to something similar to the following. ![Svelte Testimonial Carousel](https://directus.com/docs/img/155ded4b-87c7-445b-b1c9-4cb9024ba464.webp) ## Creating the Add Testimonial Form The final step is to implement your Add Testimonial form. This form will allow users add data to your Testimonials collection directly from your svelte website. Add a `TestimonialCreate.svelte` file your `./src/lib` directory and add the following code to the file. ```js

Add your Testimonial