# What is NuxtHub? On top of deploying your Nuxt application, NuxtHub aims to provide a complete backend experience on top of the framework, allowing developers to build full-stack applications on the Edge, read more about [Nuxt on the Edge](https://nuxt.com/blog/nuxt-on-the-edge){rel="nofollow"}. It leverages Cloudflare features such as Pages, Workers Analytics, AI, KV, D1, R2 and more. ::callout **NuxtHub is what Vercel / Netlify is for AWS, but for Cloudflare.** :br It also deploys to your Cloudflare account so you stay in control of your data and billing as we don't mark-up Cloudflare prices. :: ## Features NuxtHub provides optional features to help you build full-stack applications: ::card-group :::card --- icon: i-lucide-wand title: AI Models to: https://hub.nuxt.com/docs/features/ai --- Run machine learning models, such as LLMs. ::: :::card --- icon: i-lucide-shapes title: Blob to: https://hub.nuxt.com/docs/features/blob --- Store static assets, such as images, videos and more ::: :::card --- icon: i-lucide-zap title: Cache to: https://hub.nuxt.com/docs/features/cache --- Caching system for your Nuxt pages, API routes or server functions ::: :::card --- icon: i-lucide-database title: SQL database to: https://hub.nuxt.com/docs/features/database --- Store your application's data in a secure and scalable serverless SQL database. ::: :::card --- icon: i-lucide-list title: Key-Value to: https://hub.nuxt.com/docs/features/kv --- Key-Value to store JSON data accessible globally with low-latency ::: :::card --- icon: i-lucide-hard-drive-upload title: Remote Access to: https://hub.nuxt.com/docs/getting-started/remote-storage --- Connect to your project's resources from your local environment. ::: :: ## Dashboard ::tabs :::div{label="Projects"} ![NuxtHub Admin](https://hub.nuxt.com/images/landing/nuxthub-admin.png){dataZoomSrc="/images/landing/nuxthub-admin.png" height="515" width="915"} ::: :::div{label="Deployments"} ![NuxtHub Admin Deployments](https://hub.nuxt.com/images/landing/nuxthub-admin-project.png){dataZoomSrc="/images/landing/nuxthub-admin-project.png" height="515" width="915"} ::: :::div{label="Database"} ![NuxtHub Admin Database](https://hub.nuxt.com/images/landing/nuxthub-admin-database.png){dataZoomSrc="/images/landing/nuxthub-admin-database.png" height="515" width="915"} ::: :::div{label="KV"} ![NuxtHub Admin KV](https://hub.nuxt.com/images/landing/nuxthub-admin-kv.png){dataZoomSrc="/images/landing/nuxthub-admin-kv.png" height="515" width="915"} ::: :::div{label="Blob"} ![NuxtHub Admin Blob](https://hub.nuxt.com/images/landing/nuxthub-admin-blob.png){dataZoomSrc="/images/landing/nuxthub-admin-blob.png" height="515" width="915"} ::: :::div{label="Logs"} ![NuxtHub Admin Logs](https://hub.nuxt.com/images/landing/nuxthub-admin-server-logs.png){dataZoomSrc="/images/landing/nuxthub-admin-server-logs.png" height="515" width="915"} ::: :::div{label="Open API"} ![NuxtHub Admin Open API](https://hub.nuxt.com/images/landing/nuxthub-admin-open-api.png){dataZoomSrc="/images/landing/nuxthub-admin-open-api.png" height="515" width="915"} ::: :::div{label="Cache"} ![NuxtHub Admin Cache](https://hub.nuxt.com/images/landing/nuxthub-admin-cache.png){dataZoomSrc="/images/landing/nuxthub-admin-cache.png" height="515" width="915"} ::: :: The [NuxtHub admin](https://admin.hub.nuxt.com){rel="nofollow"} is a web based dashboard to manage your NuxtHub apps. It helps you deploy your NuxtHub apps with a single command on your Cloudflare account while provisioning all the necessary resources for you. It abstracts the complexity of managing full-stack Nuxt applications on Cloudflare: - Link your Cloudflare account and stay in control, we never mark-up Cloudflare prices - [Deploy your application](https://hub.nuxt.com/docs/getting-started/deploy) with `npx nuxthub deploy` command or with Cloudflare Pages CI - Relax while it provisions all the necessary resources (ai, blob, cache, database, kv) - Manage your app's resources with an admin panel - Visualize application, database, and cache metrics - Give access to team members to manage the application without sharing your Cloudflare account - Monitor your application with logs and analytics ::tip{icon="i-lucide-rocket" to="https://admin.hub.nuxt.com"} Get started with NuxtHub Admin. :: ## Nuxt DevTools NuxtHub also integrates with the [Nuxt DevTools](https://devtools.nuxt.com/){rel="nofollow"} to provide a complete development experience. ::tabs :::div{label="Database"} ![Nuxt DevTools Database](https://hub.nuxt.com/images/landing/nuxt-devtools-database.png){dataZoomSrc="/images/landing/nuxt-devtools-database.png" height="515" width="915"} ::: :::div{label="KV"} ![Nuxt DevTools KV](https://hub.nuxt.com/images/landing/nuxt-devtools-kv.png){dataZoomSrc="/images/landing/nuxt-devtools-kv.png" height="515" width="915"} ::: :::div{label="Blob"} ![Nuxt DevTools Blob](https://hub.nuxt.com/images/landing/nuxt-devtools-blob.png){dataZoomSrc="/images/landing/nuxt-devtools-blob.png" height="515" width="915"} ::: :: ## Upcoming NuxtHub is built with a modular approach: - [`@nuxthub/core`](https://github.com/nuxt-hub/core){rel="nofollow"}: Main package to provide storage features - `@nuxthub/auth`: Add authentication for user management (soon) - `@nuxthub/email`: Send transactional emails to your users (soon) - `@nuxthub/forms`: Collect forms from users (soon) - `@nuxthub/analytics`: Understand your traffic and track events within your application and API (soon) - `@nuxthub/...`: You name it! ::callout We are currently in the early stages of development (beta) and are looking for feedback from the community. If you are interested in contributing, please join us on [nuxt-hub/core](https://github.com/nuxt-hub/core){rel="nofollow"}. :: # Installation ## Quickstart The easiest way to get started with NuxtHub is to start with one of [our templates](https://hub.nuxt.com/templates). It includes all the necessary configuration and resources to get you started. Click on the `GitHub` button, then once on GitHub, click on `Use this template` to create a new repository based on the template. ::callout{icon="i-lucide-panels-top-left" to="https://hub.nuxt.com/templates"} Explore NuxtHub templates. :: ## CLI Run this command to create a new project locally using our [hello-edge template](https://github.com/nuxt-hub/hello-edge){rel="nofollow"}: ```bash [Terminal] npx nuxthub init my-app ``` Then, inside your project directory (`my-app` in the example above), run your development server: ```bash [Terminal] npm run dev ``` Your project will be available on {rel="nofollow"} ## Add to a Nuxt project 1. Install the NuxtHub module to your project: ```bash [Terminal] npx nuxi module add hub ``` This command will install `@nuxthub/core` as dependency and add it to your `modules` section of your `nuxt.config`. 2. Install [wrangler](https://developers.cloudflare.com/workers/wrangler/){rel="nofollow"} development dependency to your project: ::code-group ```bash [pnpm] pnpm add -D wrangler ``` ```bash [yarn] yarn add --dev wrangler ``` ```bash [npm] npm install --save-dev wrangler ``` ```bash [bun] bun add --dev wrangler ``` :: That's it! You can now use NuxtHub features in your Nuxt project. ::note NuxtHub will create a `.data/hub` directory in your project root, which contains the necessary configuration files and resources for the features to work. It will also add it to the `.gitignore` file to avoid committing it to your repository. :: ## Options Configure options in your `nuxt.config.ts` as such: ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxthub/core'], hub: { // NuxtHub options } }) ``` ::field-group :::field{name="analytics" type="boolean"} Default to `false` - Enables analytics for your project (coming soon). ::: :::field{name="blob" type="boolean"} Default to `false` - Enables blob storage to store static assets, such as images, videos and more. ::: :::field{name="cache" type="boolean"} Default to `false` - Enables cache storage to cache your server route responses or functions using Nitro's `cachedEventHandler` and `cachedFunction` ::: :::field{name="database" type="boolean"} Default to `false` - Enables SQL database to store your application's data. ::: :::field{name="kv" type="boolean"} Default to `false` - Enables Key-Value to store JSON data accessible globally. ::: :::field{name="remote" type="boolean|string"} Default to `false` - Allows working with remote storage (database, kv, blob) from your deployed project. :br[Read more about remote storage for usage](https://hub.nuxt.com/docs/getting-started/remote-storage). ::: :::field{name="dir" type="string"} Default to `'.data/hub'` - The directory used for storage (D1, KV, R2, etc.) in development mode. ::: :: ::tip{icon="i-lucide-rocket"} You're all set! Now, let's dive into connecting to your Cloudflare account and [deploying it on the Edge](https://hub.nuxt.com/docs/getting-started/deploy). :: ## Nightly Builds You can also use the latest features and bug fixes (commited on the `main` branch) by installing the [nightly tag](https://www.npmjs.com/package/@nuxthub/core?activeTab=versions){rel="nofollow"}: ```diff [package.json] { "Dependencies": { - "@nuxthub/core": "^0.5.0" + "@nuxthub/core": "npm:@nuxthub/core@nightly" } } ``` Then run `npm install`, `pnpm install`, `yarn install` or `bun install` to update the dependency. # Deploy Nuxt on the Edge ::note To deploy your Nuxt application on the Edge, we use Cloudflare Pages. Therefore, we require you to create a [Cloudflare](https://www.cloudflare.com/){rel="nofollow"} account. **You can deploy NuxtHub projects with a free Cloudflare account.** :: The [NuxtHub Admin](https://admin.hub.nuxt.com){rel="nofollow"} is made to simplify your experience with NuxtHub, enabling you to effortlessly manage teams and projects, as well as deploying NuxtHub application with zero-configuration on your Cloudflare account. ::tabs :::div{label="Deployments"} ![NuxtHub Admin Deployments](https://hub.nuxt.com/images/landing/nuxthub-admin-project.png){dataZoomSrc="/images/landing/nuxthub-admin-project.png" height="515" width="915"} ::: :::div{label="Deployment Details"} ![NuxtHub Admin Deployment](https://hub.nuxt.com/images/landing/nuxthub-admin-deployment.png){dataZoomSrc="/images/landing/nuxthub-admin-deployment.png" height="515" width="915"} ::: :: ## Production vs Preview Deployments NuxtHub supports two types of deployments: production and preview. ### Production Deployments - When setting up your project, you can specify a production branch (defaults to `main`) - Successful deployments to the production branch will be: - Accessible via your primary domain - Also available at `..pages.dev` ### Preview Deployments - Any deployment from a non-production branch (including pull requests) is considered a preview - Successful preview deployments are accessible via: - `..pages.dev` - `..pages.dev` ::tip Toggle between production and preview environments in the NuxtHub admin using the "Preview mode" switch. :: ## NuxtHub CLI Deploy your local project with a single command: ```bash [Terminal] npx nuxthub deploy ``` The command will: 1. Ensure you are logged in on [admin.hub.nuxt.com](https://admin.hub.nuxt.com){rel="nofollow"} 2. Make sure you linked your Cloudflare account 3. Link your local project with a NuxtHub project or help you create a new one 4. Build your Nuxt project with the correct preset 5. Deploy it to your Cloudflare account with all the necessary resources (D1, KV, R2, etc.) 6. Provide you with a URL to access your project with a free `.nuxt.dev` domain :video{controls className="w-full,h-auto,rounded" controls="true" poster="https://res.cloudinary.com/nuxt/video/upload/v1723569534/nuxthub/nuxthub-deploy_xxs5s8.jpg"} ::note You can also install the [NuxtHub CLI](https://github.com/nuxt-hub/cli){rel="nofollow"} globally with: `npm i -g nuxthub`. :: ### Usage with CI/CD ::tip If you are using GitHub for your project, jump to the [Github Action](https://hub.nuxt.com/#github-action) section. :: ::important The `nuxthub deploy` command is designed to run **non-interactively** in CI/CD environments. It won’t prompt for additional input (such as logging in or linking the project). As long as the required environment variables are set, deployment will proceed automatically. :: To integrate the `nuxthub deploy` command within your CI/CD pipeline, set the following environment variables: - `NUXT_HUB_PROJECT_KEY` – Your project key available in: - Your project settings in the [NuxtHub Admin](https://admin.hub.nuxt.com){rel="nofollow"} - Your `.env` file (if you ran `npx nuxthub link`) - `NUXT_HUB_USER_TOKEN` – Your personal token, available in **User settings** → **Tokens** in the [NuxtHub Admin](https://admin.hub.nuxt.com){rel="nofollow"} **Example command:** ```bash [Terminal] NUXT_HUB_PROJECT_KEY= NUXT_HUB_USER_TOKEN= npx nuxthub deploy ``` This will authenticate your user and link your NuxtHub project for deployment. ::note For security, **do not hardcode these values**. Instead, store them as environment variables in your CI/CD pipeline. :: ## GitHub Action After linking a GitHub repository to your project, NuxtHub automatically adds a GitHub Actions workflow to automatically deploy your application on every commit using the [NuxtHub GitHub Action](https://github.com/marketplace/actions/deploy-to-nuxthub){rel="nofollow"}. NuxtHub integrates with [GitHub deployments](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments){rel="nofollow"}. This allows you to: - [View deployment statuses within GitHub](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/viewing-deployment-history){rel="nofollow"} - [Setup deployment concurrency](https://docs.github.com/en/actions/use-cases-and-examples/deploying/deploying-with-github-actions#using-concurrency){rel="nofollow"} - [Require approvals for deploying to environments](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/reviewing-deployments){rel="nofollow"} After deploying from a pull request, NuxtHub automatically adds a comment with information about the deployment. ![NuxtHUb GitHub Action commenting on pull requests](https://hub.nuxt.com/images/docs/nuxthub-github-app-pr-comment.png){height="520" width="926"} ::tip You can customise the workflow to tailor to any specific custom DevOps requirements. :: ::note{to="https://hub.nuxt.com/#linking-a-repository-to-existing-projects"} Projects created prior to releasing our GitHub Action uses Pages CI for deployments. Read our [migration guide](https://hub.nuxt.com/#linking-a-repository-to-existing-projects). :: ### Default workflow The GitHub Workflow added to your repository is automatically tailored to your project's package manager. This is an example of a workflow added for a project using pnpm. We support pnpm, yarn, npm and Corepack. If you use a different package manager, you can customise the generated `nuxthub.yml` GitHub Action. ::code-collapse ```yml [.github/workflows/nuxthub.yml] name: Deploy to NuxtHub on: push jobs: deploy: name: "Deploy to NuxtHub" runs-on: ubuntu-latest environment: name: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} url: ${{ steps.deploy.outputs.deployment-url }} permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 9 - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: 'pnpm' - name: Install dependencies run: pnpm install - name: Build application run: pnpm run build - name: Deploy to NuxtHub uses: nuxt-hub/action@v1 id: deploy ``` :: ### Options #### Inputs The following input parameters can be provided to the GitHub Action. Learn more about [Workflow syntax for GitHub Actions](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepswith){rel="nofollow"} on GitHub's documentation. ::field-group :::field{default="dist" name="directory" type="string"} The directory of the built Nuxt application. Defaults to `dist`. ::: :::field{name="project-key" type="string"} The project key of the NuxtHub project to deploy to. If the repository is linked to more than one project, project key is required. ::: :: #### Outputs The GitHub Action provides the following outputs that you can use in subsequent workflow steps. ::field-group :::field{name="environment" type="'production' | 'preview'"} The environment of the deployment (e.g. production, preview). ::: :::field{name="deployment-url" type="string"} The URL of the deployment. For preview environments, it links to the deployment of the commit. Examples: - {rel="nofollow"} (main) - {rel="nofollow"} (feat/example) ::: :::field{name="branch-url" type="string"} The permanent URL for the current branch deployment. Examples: - {rel="nofollow"} (main) - {rel="nofollow"} (feat/example) ::: :: ### Environment Variables & Secrets NuxtHub automatically copies all your project's environment variables to your GitHub repository actions environment variables. When encrypting an environment variable in the NuxtHub Admin, a GitHub actions secret will be created in your repository. You can view the environment variables and secrets synchronized by NuxtHub by navigating to **Repository Settings -> Secrets and variables -> Actions** on GitHub. ::warning If you have a private repository on a free GitHub account or organization, NuxtHub won't be able to sync the env variables & secrets as GitHub repository environments (production / preview) are not available. In this case, you must manually set up the environment variables by navigating to **Repository Settings -> Secrets and variables -> Actions** on GitHub. :: In order to use GitHub Actions variables and secrets, you need to update your workflow to expose them as environment variables: ```diff [.github/workflows/nuxthub.yml] - name: Build application run: pnpm run build + env: + NUXT_PUBLIC_VAR: ${{ vars.NUXT_PUBLIC_VAR }} + NUXT_UI_PRO_LICENSE: ${{ secrets.NUXT_UI_PRO_LICENSE }} ``` ::note This is mostly useful for build-time environment variables. :: ### Setup #### Creating a new project When creating a new project from a template, or importing a Git repository, the GitHub Action workflow will automatically be set up for you. #### Linking a repository to existing projects Link your project to a GitHub repository within [NuxtHub Admin](https://admin.hub.nuxt.com/){rel="nofollow"} → Projects → `` → Settings → General → Git Repository #### Migration from Cloudflare CI to GitHub Actions Migrate your project to GitHub Actions within [NuxtHub Admin](https://admin.hub.nuxt.com/){rel="nofollow"} → Projects → `` → Settings → General → Git Repository → Begin Migration. ::warning Only non-secret environment variables are automatically copied to GitHub. Existing environment secrets are not automatically migrated to GitHub, and should be updated to sync them to GitHub. :: #### Monorepo setup Our GitHub integration supports deploying multiple applications from the same repository. When linking a Git repository, set "project root directory" to the base folder of your Nuxt application corresponding to that NuxtHub project. When a repository is already linked to at least one project, additional projects linked will have the generated GitHub Actions workflow named `nuxthub-.yml`. ::note When multiple projects are linked to the same repository, the [`project-key`](https://hub.nuxt.com/#inputs) input parameter must be specified on each [Deploy to NuxtHub GitHub Action](https://github.com/marketplace/actions/deploy-to-nuxthub){rel="nofollow"}. :: **Current limitations** - Separate applications should be deployed using different workflow jobs. ## GitLab CI This section will guide you to implement the GitLab CI. The integration with GitLab CI builds on the [Usage with CI/CD](https://hub.nuxt.com/docs/getting-started/deploy#usage-with-cicd){rel="nofollow"} section, make sure you have those two variables `NUXT_HUB_PROJECT_KEY` and `NUXT_HUB_USER_TOKEN`. ### User Token Variable ::important It is good practice to place the user token inside GitLab CI Variables. :: 1. Open your Repository (on GitLab) > Settings > CI/CD > Variables 2. Click on *Add variable* 3. Set Visibility to Masked and hidden 4. Remove protected Flag (since we may use this variable also on non-protected branches) 5. Give a Key name = `NUXT_HUB_USER_TOKEN` 6. Paste your `NUXT_HUB_USER_TOKEN` — Your personal token, available in **User settings** → **Tokens** in the [NuxtHub Admin](https://admin.hub.nuxt.com){rel="nofollow"} 7. Click Add variable ::note The `NUXT_HUB_PROJECT_KEY` is used later in [Configure Projects for GitLab](https://hub.nuxt.com/#configure-projects-for-gitlab). :: ### Project Configuration 1. Create a `.gitlab-ci.yml` in your repository root 2. Paste the following configuration ::code-collapse ```yml [.gitlab-ci.yml] # if one job fails, pipeline should fail workflow: auto_cancel: on_job_failure: all variables: APP_PATH: "PATH/TO/YOUR/APP" NUXT_HUB_PROJECT_KEY: "YOUR_NUXT_HUB_PROJECT_KEY" # add additional steps as needed stages: - deploy - callback deploy_project: stage: deploy # here we use Bun, you can change this. image: "oven/bun:slim" script: - echo "Deploying project_a app..." - cd $APP_PATH # you can not use node_modules from cache, since nuxthub needs write access - bun install # if you set up your variables correctly this should deploy successfully - bunx nuxthub deploy rules: # here we configure auto deploy on branch: staging and main - if: '$CI_COMMIT_BRANCH == "staging" || $CI_COMMIT_BRANCH == "main"' manual_deploy_project: stage: deploy image: "oven/bun:slim" when: manual script: - echo "Deploying project_a app..." - cd $APP_PATH - bun install - bunx nuxthub deploy callback: stage: callback script: - echo "Deploy reached." ``` :: ::note If you have a monorepo with multiple projects (apps), do this step for each of your project (e.g. `./apps/project-foo/.gitlab-ci.yml`). Then follow the [Monorepo Setup](https://hub.nuxt.com/#monorepo-setup-1) section. :: ::warning The `callback` job is needed to mark the pipeline as successfully, especially if `deploy_project` has not run. :br You can avoid this by setting `allow-failure: true` but this will result in other drawbacks. Or simply deploy on every branch, which results in more traffic. :: ::tip You can always deploy from your working branches, just use the manual trigger. Deployed branches different from 'main' (or what you configured in nuxthub dashboard as your main) will be marked as `preview`. :: ### Monorepo Setup If you have a monorepo with multiple apps (e.g. `./apps/project-foo`), then we make sure our pipelines can run parallel. 1. Create in your repository root a `.gitlab-ci.yml` 2. Paste the following configuration ::code-collapse ```yml [.gitlab-ci.yml] # if one job fails, pipeline should fail workflow: auto_cancel: on_job_failure: all stages: - trigger_apps trigger_project_a: stage: trigger_apps rules: - changes: - "apps/project-foo/**/*" trigger: strategy: depend include: - local: "apps/project-foo/.gitlab-ci.yml" trigger_project_b: stage: trigger_apps rules: - changes: - "apps/project-bar/**/*" trigger: strategy: depend include: - local: "apps/project-bar/.gitlab-ci.yml" add_manual_triggers: stage: trigger_apps trigger: strategy: depend include: - local: ".manual-triggers.yml" ``` :: ::note Change the paths to your own project paths. :: This configuration separates your main pipeline from child pipelines, each project (app) has its own `.gitlab-ci.yml` Those child pipelines are only triggered if changes are made in their app folder. Sometimes GitLab changes are not recognized (previous pipeline failed and new push has no changes in app folder), for that case, you can add manual triggers for the child pipelines by adding a `.manual-triggers.yml` in the project root. ::code-collapse ```yml [.manual-triggers.yml] workflow: auto_cancel: on_job_failure: all stages: - trigger_apps - callback trigger_project_a: stage: trigger_apps when: manual trigger: strategy: depend include: - local: "apps/project-foo/.gitlab-ci.yml" trigger_project_b: stage: trigger_apps when: manual trigger: strategy: depend include: - local: "apps/project-bar/.gitlab-ci.yml" callback: stage: callback script: - echo "manual triggers set" ``` :: ::warning With the options `depends` and `workflow.auto_cancel.on_job_failure: all` a pipeline is failed, if one job fails. This assures a clean main / staging branch. Change it to your needs. :: ## Cloudflare Pages CI Importing an existing Cloudflare Pages project that is already linked to a Git repository will use [Cloudflare Pages CI](https://pages.cloudflare.com){rel="nofollow"} to deploy your project. - Each commit will trigger a new deployment within Pages CI. - Environment variables set within NuxtHub Admin will be available during CI. ::note All existing projects with a Git repository linked to Cloudflare Pages prior to our GitHub Action being released uses [Cloudflare Pages CI](https://pages.cloudflare.com){rel="nofollow"} for automated deployments. :: ::tip{to="https://hub.nuxt.com/#migrating-to-from-pages-ci"} You can migrate from Cloudflare Pages CI to [GitHub Actions](https://hub.nuxt.com/#github-action) at any time. Read our [migration guide](https://hub.nuxt.com/#linking-a-repository-to-existing-projects). :: ## Self-hosted You can deploy your project on your own Cloudflare account without using the NuxtHub Admin. For that, you need to create the necessary resources in your Cloudflare account and configure your project to use them ([D1](https://dash.cloudflare.com/?to=/\:account/workers/d1){rel="nofollow"}, [KV](https://dash.cloudflare.com/?to=/\:account/workers/kv/namespaces){rel="nofollow"}, [R2](https://dash.cloudflare.com/?to=/\:account/r2/new){rel="nofollow"}, etc.). ::note You only need to create these resources if you have explicitly enabled them in the Hub Config. :: Then, create a [Cloudflare Pages project](https://dash.cloudflare.com/?to=/\:account/pages/new/provider/github){rel="nofollow"} and link your GitHub or GitLab repository and choose the Nuxt Framework preset in the build settings. Once your project is created, open the `Settings` tab and set: - Runtime > Compatibility flags - Add the `nodejs_compat` flag - Bindings - KV namespace: `KV` and select your KV namespace created - KV namespace: `CACHE` and select your KV namespace for caching created - R2 bucket: `BLOB` and select your R2 bucket created - D1 database: `DB` and select your D1 database created - AI: `AI` - Browser: `BROWSER` - Vectorize: `VECTORIZE_` and select your Vectorize index created :br ```bash # Create the Vectorize index manually using npx wrangler vectorize create --dimensions= --metric= ``` Go back to the `Deployment` tab and retry the last deployment by clicking on `...` then `Retry deployment`. ::tip Once the deployment is done, you should be able to use `npx nuxt dev --remote` after [configuring the remote storage](https://hub.nuxt.com/docs/getting-started/remote-storage#self-hosted) :: # Remote Storage ![NuxtHub Remote Access](https://hub.nuxt.com/images/docs/nuxthub-remote-access.png){dataZoomSrc="/images/docs/nuxthub-remote-access.png" height="377" width="766"} One of the main features of NuxtHub is the ability to access your remote storage from your local environment or from external Nuxt projects. This is made possible by our secured proxy system. There are two ways to use the remote storage: - [**Local development**](https://hub.nuxt.com/#local-development): Access your remote storage from your local environment, useful for sharing your database, KV, and blob data with your team or work with your production data. - [**External Nuxt projects**](https://hub.nuxt.com/#external-nuxt-projects): Access your remote storage from another Nuxt project, useful if your frontend is deployed on a different hosting platform and you want to use your NuxtHub project as a backend. ::important Your project [must be deployed](https://hub.nuxt.com/docs/getting-started/deploy) in order to use this feature. :: ## Local Development Once your project is deployed, you can use the remote storage in your local project. Start your Nuxt project with: ```bash [Terminal] npx nuxt dev --remote ``` The development project will now use the remote storage from your deployed project and these logs should happen in your terminal: ```bash [Terminal] ℹ Using production environment ℹ Using remote storage from https://my-project.nuxt.dev ℹ Remote storage available: database, kv, blob ``` ::tip{icon="i-lucide-rocket"} That's it! Your local project is now using the remote storage from your deployed project. :: To always use the remote storage in your local project in development, you can use the `remote` option in your `nuxt.config` file: ```ts [nuxt.config.ts] export default defineNuxtConfig({ // Apply only in development $development: { hub: { remote: true } } }) ``` ::important You should not use the `remote` option in your `nuxt.config` file in production if you are deploying your project to NuxtHub or Cloudflare Pages. Use it in production only when you are [connecting from external Nuxt projects](https://hub.nuxt.com/#external-nuxt-projects). :: ::note NuxtHub will use the remote storage from your deployed project **as long as you are logged in with the [NuxtHub CLI](https://github.com/nuxt-hub/cli){rel="nofollow"} and the local project is linked to a NuxtHub project** with `npx nuxthub link` or `npx nuxthub deploy`. :: ### Production vs Preview Based on your current git branch, NuxtHub will either use your production or preview environment. You can configure your production environment in your project settings on the [NuxtHub Admin](https://admin.hub.nuxt.com){rel="nofollow"}, default is set to `main`. To force a specific environment, you can set the value to the `--remote` option: ```bash [Terminal] # Force remote storage from the production environment npx nuxt dev --remote=production # Force remote storage from the preview environment npx nuxt dev --remote=preview ``` You can also set the `remote` options in your `nuxt.config` file to force a specific environment: ```ts [nuxt.config.ts] export default defineNuxtConfig({ // Apply only in development $development: { hub: { // Force remote storage to preview environment remote: 'preview' } } }) ``` ### Custom Preview URL By default, NuxtHub will fetch the latest preview deployment URL to connect to the remote storage. If you want to use a custom preview URL, you can use the `hub.projectUrl` option: ```ts [nuxt.config.ts] export default defineNuxtConfig({ // Apply only in development $development: { hub: { projectUrl ({ env, branch }) { // Select the preview URL from the dev branch if (env === 'preview') { return 'https://dev.my-project.nuxt.dev' } return 'https://my-project.nuxt.dev' } } } }) ``` ### Self-hosted If you are not using the [NuxtHub Admin](https://admin.hub.nuxt.com){rel="nofollow"} to manage your project, you can still use the remote storage in your local development environment. 1. Set the `NUXT_HUB_PROJECT_SECRET_KEY` environment variable in your Cloudflare Pages project settings and retry the last deployment to apply the changes (you can generate a random secret on {rel="nofollow"}) 2. Set the same `NUXT_HUB_PROJECT_SECRET_KEY` and `NUXT_HUB_PROJECT_URL` environment variables in your local project ```bash [.env] NUXT_HUB_PROJECT_SECRET_KEY=my-project-secret-used-in-cloudflare-env NUXT_HUB_PROJECT_URL=https://my-nuxthub-project.pages.dev ``` Then, start your Nuxt project with: ```bash [Terminal] npx nuxt dev --remote ``` ::note NuxtHub will use the remote data from your deployed project as long as the `NUXT_HUB_PROJECT_SECRET_KEY` matches the one in your Cloudflare Pages project settings. :: ::warning You cannot specify the environment (production or preview) when using the remote option with a self-hosted project since you specify the deployed URL with the `NUXT_HUB_PROJECT_URL` environment variable. :: ## External Nuxt Projects It is possible to use the remote storage from another Nuxt project. This can be useful if your frontend is deployed on another hosting platform and you want to use your NuxtHub as your backend. ::note [See an example in video](https://twitter.com/Atinux/status/1766622889992757317){rel="nofollow"}. :: To access your remote storage from another Nuxt project: 1. Install `@nuxthub/core` to your project: ::code-group ```bash [pnpm] pnpm add @nuxthub/core ``` ```bash [yarn] yarn add @nuxthub/core ``` ```bash [npm] npm install @nuxthub/core ``` ```bash [bun] bun add @nuxthub/core ``` :: 2. Add it to the `modules` section in your `nuxt.config` and set the `remote` option to `true`: ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxthub/core'], hub: { remote: true } }) ``` 3. Add the `NUXT_HUB_PROJECT_URL` and `NUXT_HUB_PROJECT_SECRET_KEY` environment variables to your project: ```bash [.env] NUXT_HUB_PROJECT_URL=https://my-nuxthub-project.pages.dev NUXT_HUB_PROJECT_SECRET_KEY=my-project-secret-used-in-cloudflare-env ``` If you haven't, set the `NUXT_HUB_PROJECT_SECRET_KEY` environment variable in your NuxtHub deployed project and make sure to redeploy. ::tip{icon="i-lucide-rocket"} You can now use the remote storage from your NuxtHub project in your external Nuxt project (both locally and in production). :: # Server Logs ## NuxtHub Admin When you have a successful deployment, you can access to the logs of the deployment in the [NuxtHub Admin](https://admin.hub.nuxt.com/){rel="nofollow"}. Logs are available under the `Server > Logs` section of your project page. You can also access to the logs of each successful deployment in the `Deployments` section. ![NuxtHub Admin Server Logs](https://hub.nuxt.com/images/landing/nuxthub-admin-server-logs.png){className="rounded" dataZoomSrc="/images/landing/nuxthub-admin-server-logs.png" height="515" width="915"} ## NuxtHub CLI Using the [NuxtHub CLI](https://github.com/nuxt-hub/cli){rel="nofollow"}, you can access to the logs of both `production` and `preview` deployments. By default, the CLI will detect based on the current branch the canonical deployment of your project and stream the logs of that deployment in the CLI. ```bash [Terminal] npx nuxthub logs ``` ![NuxtHub CLI Server Logs](https://hub.nuxt.com/images/landing/nuxthub-cli-server-logs.png){className="rounded" dataZoomSrc="/images/landing/nuxthub-cli-server-logs.png" height="515" width="915"} ### Production environment To access the logs of the production environment, you can use the `--production` flag. ```bash [Terminal] npx nuxthub logs --production ``` ### Preview environment In preview environment, NuxtHub will stream the logs of the latest successful deployment in the CLI. ```bash [Terminal] npx nuxthub logs --preview ``` # Run AI Models NuxtHub AI lets you integrate machine learning models into your Nuxt application. Built on top of [Workers AI](https://developers.cloudflare.com/workers-ai/){rel="nofollow"}, it provides a simple and intuitive API that supports models for text generation, image generation, embeddings, and more. ::code-group ```ts [text.ts] const response = await hubAI().run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Who is the author of Nuxt?' }) ``` ```ts [image.ts] const response = await hubAI().run('@cf/runwayml/stable-diffusion-v1-5-img2img', { prompt: 'A sunset over the ocean.', }) ``` ```ts [embeddings.ts] // returns embeddings that can be used for vector searches in tools like Vectorize const embeddings = await hubAI().run("@cf/baai/bge-base-en-v1.5", { text: "NuxtHub AI uses `hubAI()` to run models." }); ``` :: ## Getting Started Enable AI in your NuxtHub project by adding the `ai` property to the `hub` object in your `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { ai: true }, }) ``` ::note This option will enable [Workers AI](https://developers.cloudflare.com/workers-ai){rel="nofollow"} (LLM powered by serverless GPUs on Cloudflare’s global network) and automatically add the binding to your project when you [deploy it](https://hub.nuxt.com/docs/getting-started/deploy). :: ### Local Development During development, `hubAI()` will call the Cloudflare API. Make sure to run `npx nuxthub link` to create/link a NuxtHub project (even if the project is empty). This project is where your AI models will run. NuxtHub AI will always run AI models on your Cloudflare account, including during local development. [See pricing and included free quotas on Cloudflare's documentation](https://developers.cloudflare.com/workers-ai/platform/pricing/){rel="nofollow"}. ## Models Workers AI comes with a curated set of popular open-source models that enable you to do tasks such as image classification, text generation, object detection, and more. ::u-button --- trailing: true icon: i-lucide-arrow-up-right label: See all Workers AI models target: _blank to: https://developers.cloudflare.com/workers-ai/models/ variant: outline --- :: ## hubAI() `hubAI()` is a server composable that returns a [Workers AI client](https://developers.cloudflare.com/workers-ai/configuration/bindings/#methods){rel="nofollow"}. ```ts const ai = hubAI() ``` ::callout This documentation is a small reflection of the [Cloudflare Workers AI documentation](https://developers.cloudflare.com/workers-ai/configuration/bindings/#methods){rel="nofollow"}. We recommend reading it to understand the full potential of Workers AI. :: ### `run()` Runs a model. Takes a model as the first parameter, and an object as the second parameter. ```ts [server/api/ai-test.ts] export default defineEventHandler(async () => { const ai = hubAI() // access AI bindings return await ai.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Who is the author of Nuxt?' }) }) ``` #### Options ::field-group :::field{required name="model" required="true" type="string"} The model to run ::: :::field{name="options" type="object"} The model options. ::::collapsible :::::field{name="...modelOptions" type="any"} Options for the model you choose can be found in the [Worker AI models documentation](https://developers.cloudflare.com/workers-ai/models/){rel="nofollow"}. ::::::field{name="stream" type="boolean"} Whether results should be [streamed](https://hub.nuxt.com/#streaming-responses) as they are generated. :::::: ::::: :::: ::: :::field{name="AI Gateway" type="object"} Options for configuring [`AI Gateway`](https://hub.nuxt.com/#ai-gateway) - `id`, `skipCache`, and `cacheTtl`. ::: :: ## Tools Tools are actions that your LLM can execute to run functions or interact with external APIs. The result of these tools will be used by the LLM to generate additional responses. This can help you supply the LLM with real-time information, save data to a KV store, or provide it with external data from your database. With Workers AI, tools have 4 properties: - `name`: The name of the tool - `description`: A description of the tool that will be used by the LLM to understand what the tool does. This allows it to determine when to use the tool - `parameters`: The parameters that the tool accepts. - `function`: The function that will be executed when the tool is called. ```ts const tools = [ { name: 'get-weather', description: 'Gets the weather for a given city', parameters: { type: 'object', properties: { city: { type: 'number', description: 'The city to retrieve weather information for' }, }, required: ['city'], }, function: ({ city }) => { // use an API to get the weather information return '72' }), } ] ``` #### Tool Fields ::field-group :::field{required name="name" required="true" type="string"} The name of the tool ::: :::field{required name="description" required="true" type="string"} A description of the tool that will be used by the LLM to understand what the tool does. This allows it to determine when to use the tool ::: :::field{name="parameters" type="JsonSchema7"} The parameters and options for parameters that the model will use to run the tool. ::::collapsible :::::field{name="type" type="string"} The type of your functions parameter. It's recommended to use an `object` so you can easily add additional properties in the future. ::::::field{name="properties" type="Object"} The properties that will be passed to your function. The keys of this object should match the keys in your function's parameter. :::::::collapsible ::::::::field{name="type" type="string"} The type of the property (`string`, `number`, `boolean`, etc.) :::::::::field{name="description" type="string"} A description of the property that the LLM will use to understand what the property is when trying to use the tool. ::::::::: :::::::: ::::::: :::::::field{name="required" type="string[]"} All the properties that are required to be passed to the tool. ::::::: :::::: ::::: :::: ::: :::field{name="function" type="(args) => Promise"} The function that the LLM can execute. ::: :: ### `runWithTools()` The [`@cloudflare/ai-utils`](https://github.com/cloudflare/ai-utils){rel="nofollow"} package provides a `runWithTools` function that will handle the recursive calls to the LLM with the result of the tools. ```bash npx nypm i @cloudflare/ai-utils ``` `runWithTools` works with multi-tool calls, handles errors, and has the same return type as `hubAI().run()` so any code relying on the response from a model can remain the same. ```ts import { runWithTools } from '@cloudflare/ai-utils' export default defineEventHandler(async (event) => { return await runWithTools(hubAI(), '@cf/meta/llama-3.1-8b-instruct', { messages: [ { role: 'user', content: 'What is the weather in New York?' }, ], tools: [ { name: 'get-weather', description: 'Gets the weather for a given city', parameters: { type: 'object', properties: { city: { type: 'number', description: 'The city to retrieve weather information for' }, }, required: ['city'], }, function: ({ city }) => { // use an API to get the weather information return '72' }, }, ] }, { // options streamFinalResponse: true, maxRecursiveToolRuns: 1, } ) }) ``` #### Params ::field-group :::field{required name="AI Binding" required="true" type="Ai"} Your AI Binding (`hubAI()`) ::: :::field{required name="model" required="true" type="string"} The model to run ::: :::field{name="input" type="object"} The messages and tools to use for the model ::::collapsible :::::field --- name: messages type: "{ role: 'user' | 'system' | 'assistant', content: string }[]" --- An array of messages to send to the model. Each message has a role and content. ::::::field{name="tools" type="AiTextGenerationToolInputWithFunction[]"} An array of the tools available to the model. :::::: ::::: :::: ::: :::field{name="Options" type="object"} An array of optional properties that can be passed to the model. ::::collapsible :::::field{name="streamFinalResponse" type="boolean"} Whether to stream the final response or not. ::::::field{name="maxRecursiveToolRuns" type="number"} The maximum number of recursive tool runs to perform. :::::::field{name="strictValidation" type="boolean"} Whether to perform strict validation (using zod) of the arguments passed to the tools. ::::::::field{name="verbose" type="boolean"} Whether to enable verbose logging. :::::::: ::::::: :::::: ::::: :::: ::: :: ::callout See the full [`runWithTools()` documentation](https://developers.cloudflare.com/workers-ai/function-calling/embedded/api-reference/){rel="nofollow"}. :: ## AI Gateway Workers AI is compatible with AI Gateway, which enables caching responses, analytics, real-time logging, ratelimiting, and fallback providers. Learn more about [AI Gateway](https://developers.cloudflare.com/ai-gateway/){rel="nofollow"}. ### Options Configure options for AI Gateway by passing an additional object to `hubAI().run()`, [learn more on Cloudflare's docs](https://developers.cloudflare.com/ai-gateway/providers/workersai/#worker){rel="nofollow"}. ```ts [server/api/who-created-nuxt.get.ts] export default defineEventHandler(async () => { const ai = hubAI() return await ai.run('@cf/meta/llama-3-8b-instruct', { prompt: 'Who is the creator of Nuxt?' }, { gateway: { id: '{gateway_slug}', skipCache: false, cacheTtl: 3360 } }) }) ``` ::field-group :::field{name="id" type="string"} Name of your existing [AI Gateway](https://developers.cloudflare.com/ai-gateway/get-started/#create-gateway){rel="nofollow"}. Must be in the same Cloudflare account as your NuxtHub application. ::: :::field{default="false" name="skipCache" type="boolean"} Controls whether the request should [skip the cache](https://developers.cloudflare.com/ai-gateway/configuration/caching/#skip-cache-cf-skip-cache){rel="nofollow"}. ::: :::field{name="cacheTtl" type="number"} Controls the [Cache TTL](https://developers.cloudflare.com/ai-gateway/configuration/caching/#cache-ttl-cf-cache-ttl){rel="nofollow"}, the duration (in seconds) that a cached request will be valid for. The minimum TTL is 60 seconds and maximum is one month. ::: :: ## Streaming The recommended method to handle text generation responses is streaming. LLMs work internally by generating responses sequentially using a process of repeated inference — the full output of a LLM model is essentially a sequence of hundreds or thousands of individual prediction tasks. For this reason, while it only takes a few milliseconds to generate a single token, generating the full response takes longer. If your UI waits for the entire response to be generated, a user may see a loading spinner for several seconds before the response is displayed. Streaming lets you start displaying the response as soon as the first tokens are generated, and append each additional token until the response is complete. This yields a much better experience for the end user. Displaying text incrementally as it’s generated not only provides instant responsiveness, but also gives the end-user time to read and interpret the text. To enable, set the `stream` parameter to `true`. You can check if the model you're using supports streaming on [Cloudflare's models documentation](https://developers.cloudflare.com/workers-ai/models/#text-generation){rel="nofollow"}. ```ts export default defineEventHandler(async (event) => { const messages = [ { role: 'system', content: 'You are a friendly assistant' }, { role: 'user', content: 'What is the origin of the phrase Hello, World?' } ] const ai = hubAI() const stream = await ai.run('@cf/meta/llama-3.1-8b-instruct', { stream: true, messages }) return stream }) ``` ### Handling Streaming Responses To manually handle streaming responses, you can use [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream){rel="nofollow"} and Nuxt's `$fetch` function to create a new `ReadableStream` from the response. Creating a reader allows you to process the stream in chunks as it's received. ```ts const response = await $fetch('/api/chats/ask-ai', { method: 'POST', body: { query: "Hello AI, how are you?", }, responseType: 'stream', }) // Create a new ReadableStream from the response with TextDecoderStream to get the data as text const reader = response.pipeThrough(new TextDecoderStream()).getReader() // Read the chunks of data as they're received while (true) { const { value, done } = await reader.read() if (done) break console.log('Received:', value) } ``` ## Vercel AI SDK Another way to handle streaming responses is to use [Vercel's AI SDK](https://sdk.vercel.ai/){rel="nofollow"} with `hubAI()`. This uses the [Workers AI Provider](https://sdk.vercel.ai/providers/community-providers/cloudflare-workers-ai){rel="nofollow"}, which supports a subset of Vercel AI features. ::callout `tools` and `streamObject` are currently not supported. :: To get started, install the Vercel AI SDK and the Cloudflare AI Provider in your project. ```text [Terminal] npx nypm i ai @ai-sdk/vue workers-ai-provider ``` ::note [`nypm`](https://github.com/unjs/nypm){rel="nofollow"} will detect your package manager and install the dependencies with it. :: ### `useChat()` `useChat()` is a Vue composable provided by the Vercel AI SDK that handles streaming responses, API calls, state for your chat. It requires a `POST /api/chat` endpoint that uses the `hubAI()` server composable and returns a compatible stream for the Vercel AI SDK. ```ts [server/api/chat.post.ts] import { streamText } from 'ai' import { createWorkersAI } from 'workers-ai-provider' export default defineEventHandler(async (event) => { const { messages } = await readBody(event) const workersAI = createWorkersAI({ binding: hubAI() }) return streamText({ model: workersAI('@cf/meta/llama-3.1-8b-instruct'), messages }).toDataStreamResponse() }) ``` Then, we can create a chat component that uses the `useChat()` composable. ```vue [app/pages/chat.vue] ``` Learn more about the [`useChat()` composable](https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-chat){rel="nofollow"}. ::callout Check out our [`pages/ai.vue` full example](https://github.com/nuxt-hub/core/blob/main/playground/app/pages/ai.vue){rel="nofollow"} with Nuxt UI & [Nuxt MDC](https://github.com/nuxt-modules/mdc){rel="nofollow"}. :: ## Templates Explore open source templates made by the community: ::card-group :::card{title="Atidraw" to="https://github.com/atinux/atidraw"} Generate the alt text of the user drawing and generate an alternative image with AI. ::: :::card{title="Hub Chat" to="https://github.com/ra-jeev/hub-chat"} A chat interface to interact with various text generation AI models. ::: :::card --- title: Flux AI Image Generator to: https://github.com/atinux/flux-ai-image-generator --- Generate images with AI using Flux-1 Schnell with different presets. ::: :::card{title="Chat with PDF" to="https://github.com/RihanArfan/chat-with-pdf"} A full-stack AI-powered application that lets you to ask questions to PDF documents. ::: :: ## Pricing ::pricing-table{:tabs='["AI"]'} :: # Blob Storage ## Getting Started Enable the blob storage in your NuxtHub project by adding the `blob` property to the `hub` object in your `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { blob: true } }) ``` ::note This option will use Cloudflare platform proxy in development and automatically create a [Cloudflare R2](https://developers.cloudflare.com/r2){rel="nofollow"} bucket for your project when you [deploy it](https://hub.nuxt.com/docs/getting-started/deploy). :: ::tabs :::div{label="Nuxt DevTools"} ![Nuxt DevTools Blob](https://hub.nuxt.com/images/landing/nuxt-devtools-blob.png){dataZoomSrc="/images/landing/nuxt-devtools-blob.png" height="515" width="915"} ::: :::div{label="NuxtHub Admin"} ![NuxtHub Admin Blob](https://hub.nuxt.com/images/landing/nuxthub-admin-blob.png){dataZoomSrc="/images/landing/nuxthub-admin-blob.png" height="515" width="915"} ::: :: ## `hubBlob()` Server composable that returns a set of methods to manipulate the blob storage. ### `list()` Returns a paginated list of blobs (metadata only). ```ts [server/api/files.get.ts] export default eventHandler(async () => { const { blobs } = await hubBlob().list({ limit: 10 }) return blobs }) ``` #### Params ::field-group :::field{name="options" type="Object"} The list options. ::::collapsible :::::field{name="limit" type="Number"} The maximum number of blobs to return per request. Defaults to `1000`. ::::: :::::field{name="prefix" type="String"} Filters the results to only those that begin with the specified prefix. ::::: :::::field{name="cursor" type="String"} The cursor to continue from a previous list operation. ::::: :::::field{name="folded" type="Boolean"} If `true`, the list will be folded using `/` separator and list of folders will be returned. ::::: :::: ::: :: #### Return Returns [`BlobListResult`](https://hub.nuxt.com/#bloblistresult). #### Return all blobs To fetch all blobs, you can use a `while` loop to fetch the next page until the `cursor` is `null`. ```ts let blobs = [] let cursor = null do { const res = await hubBlob().list({ cursor }) blobs.push(...res.blobs) cursor = res.cursor } while (cursor) ``` ### `serve()` Returns a blob's data and sets `Content-Type`, `Content-Length` and `ETag` headers. ::code-group ```ts [server/routes/images/[...pathname\\].get.ts] export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) return hubBlob().serve(event, pathname) }) ``` ```vue [pages/index.vue] ``` :: ::important To prevent XSS attacks, make sure to control the Content type of the blob you serve. :: You can also set a `Content-Security-Policy` header to add an additional layer of security: ```ts [server/api/images/[...pathname\\].get.ts] export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) setHeader(event, 'Content-Security-Policy', 'default-src \'none\';') return hubBlob().serve(event, pathname) }) ``` #### Params ::field-group :::field{name="event" type="H3Event"} Handler's event, needed to set headers. ::: :::field{name="pathname" type="String"} The name of the blob to serve. ::: :: #### Return Returns the blob's raw data and sets `Content-Type` and `Content-Length` headers. ### `head()` Returns a blob's metadata. ```ts const metadata = await hubBlob().head(pathname) ``` #### Params ::field-group :::field{name="pathname" type="String"} The name of the blob to serve. ::: :: #### Return Returns a [`BlobObject`](https://hub.nuxt.com/#blobobject). ### `get()` Returns a blob body. ```ts const blob = await hubBlob().get(pathname) ``` #### Params ::field-group :::field{name="pathname" type="String"} The name of the blob to serve. ::: :: #### Return Returns a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob){rel="nofollow"} or `null` if not found. ### `put()` Uploads a blob to the storage. ```ts [server/api/files.post.ts] export default eventHandler(async (event) => { const form = await readFormData(event) const file = form.get('file') as File if (!file || !file.size) { throw createError({ statusCode: 400, message: 'No file provided' }) } ensureBlob(file, { maxSize: '1MB', types: ['image'] }) return hubBlob().put(file.name, file, { addRandomSuffix: false, prefix: 'images' }) }) ``` See an example on the Vue side: ```vue [pages/upload.vue] ``` #### Params ::field-group :::field{name="pathname" type="String"} The name of the blob to serve. ::: :::field --- name: body type: String | ReadableStream | ArrayBuffer | ArrayBufferView | Blob --- The blob's data. ::: :::field{name="options" type="Object"} The put options. Any other provided field will be stored in the blob's metadata. ::::collapsible :::::field{name="contentType" type="String"} The content type of the blob. If not given, it will be inferred from the Blob or the file extension. ::::: :::::field{name="contentLength" type="String"} The content length of the blob. ::::: :::::field{name="addRandomSuffix" type="Boolean"} If `true`, a random suffix will be added to the blob's name. Defaults to `false`. ::::: :::::field{name="prefix" type="string"} The prefix to use for the blob pathname. ::::: :::::field{name="customMetadata" type="Record"} An object with custom metadata to store with the blob. ::::: :::: ::: :: #### Return Returns a [`BlobObject`](https://hub.nuxt.com/#blobobject). ### `del()` Delete a blob with its pathname. ```ts [server/api/files/[...pathname\\].delete.ts] export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) await hubBlob().del(pathname) return sendNoContent(event) }) ``` You can also delete multiple blobs at once by providing an array of pathnames: ```ts await hubBlob().del(['images/1.jpg', 'images/2.jpg']) ``` ::note You can also use the `delete()` method as alias of `del()`. :: #### Params ::field-group :::field{name="pathname" type="String"} The name of the blob to serve. ::: :: #### Return Returns nothing. ### `handleUpload()` This is an "all in one" function to validate a `Blob` by checking its size and type and upload it to the storage. ::note This server util is made to be used with the [`useUpload()`](https://hub.nuxt.com/#useupload) Vue composable. :: It can be used to handle file uploads in API routes. ::code-group ```ts [server/api/blob.put.ts] export default eventHandler(async (event) => { return hubBlob().handleUpload(event, { formKey: 'files', // read file or files form the `formKey` field of request body (body should be a `FormData` object) multiple: true, // when `true`, the `formKey` field will be an array of `Blob` objects ensure: { types: ['image/jpeg', 'image/png'], // allowed types of the file }, put: { addRandomSuffix: true } }) }) ``` ```vue [pages/upload.vue] ``` :: #### Params ::field-group :::field{name="formKey" type="string"} The form key to read the file from. Defaults to `'files'`. ::: :::field{name="multiple" type="boolean"} When `true`, the `formKey` field will be an array of `Blob` objects. ::: :::field{name="ensure" type="BlobEnsureOptions"} See [`ensureBlob()`](https://hub.nuxt.com/#ensureblob) options for more details. ::: :::field{name="put" type="BlobPutOptions"} See [`put()`](https://hub.nuxt.com/#put) options for more details. ::: :: #### Return Returns a [`BlobObject`](https://hub.nuxt.com/#blobobject) or an array of [`BlobObject`](https://hub.nuxt.com/#blobobject) if `multiple` is `true`. Throws an error if `file` doesn't meet the requirements. ### `handleMultipartUpload()` Handle the request to support multipart upload. ```ts [server/api/files/multipart/[action\\]/[...pathname\\].ts] export default eventHandler(async (event) => { return await hubBlob().handleMultipartUpload(event) }) ``` ::important Make sure your route includes `[action]` and `[...pathname]` params. :: On the client side, you can use the `useMultipartUpload()` composable to upload a file in parts. ```vue ``` ::note{to="https://hub.nuxt.com/#usemultipartupload"} See [`useMultipartUpload()`](https://hub.nuxt.com/#usemultipartupload) on usage details. :: #### Params ::field-group :::field{name="contentType" type="string"} The content type of the blob. ::: :::field{name="contentLength" type="string"} The content length of the blob. ::: :::field{name="addRandomSuffix" type="boolean"} If `true`, a random suffix will be added to the blob's name. Defaults to `false`. ::: :: ### `createMultipartUpload()` ::note We suggest to use [`handleMultipartUpload()`](https://hub.nuxt.com/#handlemultipartupload) method to handle the multipart upload request. :: Start a new multipart upload. ```ts [server/api/files/multipart/[...pathname\\].post.ts] export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) const mpu = await hubBlob().createMultipartUpload(pathname) return { uploadId: mpu.uploadId, pathname: mpu.pathname, } }) ``` #### Params ::field-group :::field{name="pathname" type="String"} The name of the blob to serve. ::: :::field{name="options" type="Object"} The put options. Any other provided field will be stored in the blob's metadata. ::::collapsible :::::field{name="contentType" type="String"} The content type of the blob. If not given, it will be inferred from the Blob or the file extension. ::::: :::::field{name="contentLength" type="String"} The content length of the blob. ::::: :::::field{name="addRandomSuffix" type="Boolean"} If `true`, a random suffix will be added to the blob's name. Defaults to `true`. ::::: :::: ::: :: #### Return Returns a `BlobMultipartUpload` ### `resumeMultipartUpload()` ::note We suggest to use [`handleMultipartUpload()`](https://hub.nuxt.com/#handlemultipartupload) method to handle the multipart upload request. :: Continue processing of unfinished multipart upload. To upload a part of the multipart upload, you can use the `uploadPart()` method: ```ts [server/api/files/multipart/[...pathname\\].put.ts] export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) const { uploadId, partNumber } = getQuery(event) const stream = getRequestWebStream(event)! const body = await streamToArrayBuffer(stream, contentLength) const mpu = hubBlob().resumeMultipartUpload(pathname, uploadId) return await mpu.uploadPart(partNumber, body) }) ``` Complete the upload by calling `complete()` method: ```ts [server/api/files/multipart/complete.post.ts] export default eventHandler(async (event) => { const { pathname, uploadId } = getQuery(event) const parts = await readBody(event) const mpu = hubBlob().resumeMultipartUpload(pathname, uploadId) return await mpu.complete(parts) }) ``` If you want to cancel the upload, you need to call `abort()` method: ```ts [server/api/files/multipart/[...pathname\\].delete.ts] export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) const { uploadId } = getQuery(event) const mpu = hubBlob().resumeMultipartUpload(pathname, uploadId) await mpu.abort() return sendNoContent(event) }) ``` A simple example of multipart upload in client with above routes: ```ts [utils/multipart-upload.ts] async function uploadLargeFile(file: File) { const chunkSize = 10 * 1024 * 1024 // 10MB const count = Math.ceil(file.size / chunkSize) const { pathname, uploadId } = await $fetch( `/api/files/multipart/${file.name}`, { method: 'POST' }, ) const uploaded = [] for (let i = 0; i < count; i++) { const start = i * chunkSize const end = Math.min(start + chunkSize, file.size) const partNumber = i + 1 const chunk = file.slice(start, end) const part = await $fetch( `/api/files/multipart/${pathname}`, { method: 'PUT', query: { uploadId, partNumber }, body: chunk, }, ) uploaded.push(part) } return await $fetch( '/api/files/multipart/complete', { method: 'POST', query: { pathname, uploadId }, body: { parts: uploaded }, }, ) } ``` #### Params ::field-group :::field{name="pathname" type="String"} The name of the blob to serve. ::: :::field{name="uploadId" type="String"} The upload ID of the multipart upload. ::: :: #### Return Returns a `BlobMultipartUpload` #### Params ::field-group :::field{required name="event" required="true" type="H3Event"} The event to handle. ::: :: ### `createCredentials()` Creates temporary access credentials that can be optionally scoped to prefixes or objects. Useful to create presigned URLs to upload files to R2 from client-side ([see example](https://hub.nuxt.com/#create-presigned-urls-to-upload-files-to-r2)). ::note This method is only available in production or in development with `--remote` flag. :: ```ts // Create credentials with default permission & scope (admin-read-write) const credentials = await hubBlob().createCredentials() // Limit the scope to a specific object & permission const credentials = await hubBlob().createCredentials({ permission: 'object-read-write', pathnames: ['only-this-file.png'] }) ``` Read more about [creating presigned URLs to upload files to R2](https://hub.nuxt.com/#create-presigned-urls-to-upload-files-to-r2). #### Params ::field-group :::field{name="options" type="Object"} The options to create the credentials. ::::collapsible :::::field{name="permission" type="string"} The permission of the credentials, defaults to `'admin-read-write'`{className="language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" lang="ts"}. ```ts 'admin-read-write' | 'admin-read-only' | 'object-read-write' | 'object-read-only' ``` ::::: :::::field{name="ttl" type="number"} The ttl of the credentials in seconds, defaults to `900`. ::::: :::::field{name="pathnames" type="string[]"} The pathnames to scope the credentials to. ::::: :::::field{name="prefixes" type="string[]"} The prefixes to scope the credentials to. ::::: :::: ::: :: #### Return Returns an object with the following properties: ```ts { accountId: string bucketName: string accessKeyId: string secretAccessKey: string sessionToken: string } ``` ## `ensureBlob()` `ensureBlob()` is a handy util to validate a `Blob` by checking its size and type: ```ts // Will throw an error if the file is not an image or is larger than 1MB ensureBlob(file, { maxSize: '1MB', types: ['image']}) ``` #### Params ::field-group :::field{required name="file" required="true" type="Blob"} The file to validate. ::: :::field{required name="options" required="true" type="Object"} Note that at least `maxSize` or `types` should be provided. ::::collapsible :::::field{name="maxSize" type="BlobSize"} The maximum size of the file, should be: :br (`1` | `2` | `4` | `8` | `16` | `32` | `64` | `128` | `256` | `512` | `1024`) + (`B` | `KB` | `MB` | `GB`) :br e.g. `'512KB'`, `'1MB'`, `'2GB'`, etc. ::::: :::::field{name="types" type="BlobType[]"} Allowed types of the file, e.g. `['image/jpeg']`. ::::: :::: ::: :: #### Return Returns nothing. Throws an error if `file` doesn't meet the requirements. ## Vue Composables ::note The following composables are meant to be used in the Vue side of your application (not the `server/` directory). :: ### `useUpload()` `useUpload` is to handle file uploads in your Nuxt application. ```vue ``` #### Params ::field-group :::field{required name="apiBase" required="true" type="string"} The base URL of the upload API. ::: :::field{required name="options" required="true" type="Object"} Optionally, you can pass Fetch options to the request. Read more about Fetch API [here](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options){rel="nofollow"}. ::::collapsible :::::field{name="formKey" type="string"} The key to add the file/files to the request form. Defaults to `'files'`. ::::: :::::field{name="multiple" type="boolean"} Whether to allow multiple files to be uploaded. Defaults to `true`. ::::: :::: ::: :: #### Return Return a `MultipartUpload` function that can be used to upload a file in parts. ```ts const { completed, progress, abort } = upload(file) const data = await completed ``` ### `useMultipartUpload()` Application composable that creates a multipart upload helper. ```ts [utils/multipart-upload.ts] export const mpu = useMultipartUpload('/api/files/multipart') ``` #### Params ::field-group :::field{name="baseURL" type="string"} The base URL of the multipart upload API handled by [`handleMultipartUpload()`](https://hub.nuxt.com/#handlemultipartupload). ::: :::field{name="options"} The options for the multipart upload helper. ::::collapsible :::::field{name="partSize" type="number"} The size of each part of the file to be uploaded. Defaults to `10MB`. ::::: :::::field{name="concurrent" type="number"} The maximum number of concurrent uploads. Defaults to `1`. ::::: :::::field{name="maxRetry" type="number"} The maximum number of retry attempts for the whole upload. Defaults to `3`. ::::: :::::field{name="prefix" type="string"} The prefix to use for the blob pathname. ::::: :::::field --- name: fetchOptions type: Omit --- Override the ofetch options. The `query` and `headers` will be merged with the options provided by the uploader. ::::: :::: ::: :: #### Return Return a `MultipartUpload` function that can be used to upload a file in parts. ```ts const { completed, progress, abort } = mpu(file) const data = await completed ``` ## Types ### `BlobObject` ```ts interface BlobObject { pathname: string contentType: string | undefined size: number httpEtag: string uploadedAt: Date httpMetadata: Record customMetadata: Record } ``` ### `BlobMultipartUpload` ```ts export interface BlobMultipartUpload { pathname: string uploadId: string uploadPart( partNumber: number, value: string | ReadableStream | ArrayBuffer | ArrayBufferView | Blob ): Promise abort(): Promise complete(uploadedParts: BlobUploadedPart[]): Promise } ``` ### `BlobUploadedPart` ```ts export interface BlobUploadedPart { partNumber: number; etag: string; } ``` ### `MultipartUploader` ```ts export type MultipartUploader = (file: File) => { completed: Promise | undefined> progress: Readonly> abort: () => Promise } ``` ### `BlobListResult` ```ts interface BlobListResult { blobs: BlobObject[] hasMore: boolean cursor?: string folders?: string[] } ``` ## Examples ### List blobs with pagination ::code-group ```ts [server/api/blobs.get.ts] export default eventHandler(async (event) => { const { limit, cursor } = await getQuery(event) return hubBlob().list({ limit: limit ? Number.parseInt(limit) : 10, cursor: cursor ? cursor : undefined }) }) ``` ```vue [pages/blobs.vue] ``` :: ### Create presigned URLs to upload files to R2 Presigned URLs can be used to upload files to R2 from client-side without using an API key. ![NuxtHub presigned URLs to upload files to R2](https://hub.nuxt.com/images/docs/blob-presigned-urls.png){className="rounded" height="515" width="915"} As we use [aws4fetch](https://github.com/mhart/aws4fetch){rel="nofollow"} to sign the request and [zod](https://github.com/colinhacks/zod){rel="nofollow"} to validate the request, we need to install the packages: ```bash [Terminal] npx nypm i aws4fetch zod ``` First, we need to create an API route that will return a presigned URL to the client. ```ts [server/api/blob/sign/[...pathname\\].get.ts] import { z } from 'zod' import { AwsClient } from 'aws4fetch' export default eventHandler(async (event) => { const { pathname } = await getValidatedRouterParams(event, z.object({ pathname: z.string().min(1) }).parse) // Create credentials with the right permission & scope const blob = hubBlob() const { accountId, bucketName, ...credentials } = await blob.createCredentials({ permission: 'object-read-write', pathnames: [pathname] }) // Create the presigned URL const client = new AwsClient(credentials) const endpoint = new URL( pathname, `https://${bucketName}.${accountId}.r2.cloudflarestorage.com` ) const { url } = await client.sign(endpoint, { method: 'PUT', aws: { signQuery: true } }) // Return the presigned URL to the client return { url } }) ``` ::important Make sure to authenticate the request on the server side to avoid letting anyone upload files to your R2 bucket. Checkout [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils){rel="nofollow"} as one of the possible solutions. :: Next, we need to create the Vue page to upload a file to our R2 bucket using the presigned URL: ```vue [pages/upload.vue] ``` At this stage, you will get a CORS error because we did not setup the CORS on our R2 bucket. To setup the CORS on our R2 bucket: - Open the project on NuxtHub Admin with `npx nuxthub manage` - Go to the **Blob tab** (make sure to be on the right environment: production or preview) - Click on the Cloudflare icon on the top right corner - Once on Cloudflare, Go to the `Settings` tab of your R2 bucket - Scroll to **CORS policy** - Click on `Edit CORS policy` - Update the allowed origins with your origins by following this example: ```json [ { "AllowedOrigins": [ "http://localhost:3000", "https://my-app.nuxt.dev" ], "AllowedMethods": [ "GET", "PUT" ], "AllowedHeaders": [ "*" ] } ] ``` - Save the changes That's it! You can now upload files to R2 using the presigned URLs. ::callout Read more about presigned URLs on Cloudflare's [official documentation](https://developers.cloudflare.com/r2/api/s3/presigned-urls/){rel="nofollow"}. :: ## Pricing ::pricing-table{:tabs='["Blob"]'} :: # Browser Rendering ## Getting Started Enable browser rendering in your Nuxt project by enabling the `hub.browser` option: ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { browser: true }, }) ``` Lastly, install the required dependencies by running the following command: ```bash [Terminal] npx nypm i @cloudflare/puppeteer puppeteer ``` ::note [nypm](https://github.com/unjs/nypm){rel="nofollow"} will automatically detect the package manager you are using and install the dependencies. :: ## Usage In your server API routes, you can use the `hubBrowser` function to get a [Puppeteer browser instance](https://github.com/puppeteer/puppeteer){rel="nofollow"}: ```ts const { page, browser } = await hubBrowser() ``` In production, the instance will be from [`@cloudflare/puppeteer`](https://developers.cloudflare.com/browser-rendering/platform/puppeteer/){rel="nofollow"} which is a fork of Puppeteer with version specialized for working within Cloudflare workers. ::tip NuxtHub will automatically close the `page` instance when the response is sent as well as closing or disconnecting the `browser` instance when needed. :: Here are some use cases for using a headless browser like Puppeteer in your Nuxt application: - Take screenshots of pages - Convert a page to a PDF - Test web applications - Gather page load performance metrics - Crawl web pages for information retrieval (extract metadata) ## Limits The Cloudflare limits are: - 3 concurrent browsers per Cloudflare account - 3 new browser instancess per minute - a browser instance gets killed if no activity is detected for 60 seconds (idle timeout) On a Workers Paid plan, you can create up to 10 concurrent browser per account and 10 new browser instances per minute. ::note To improve the performance in production, NuxtHub will reuse browser sessions. This means that the browser will stay open after each request (for 60 seconds), a new request will reuse the same browser session if available or open a new one. :: You can extend the idle timeout by giving the `keepAlive` option when creating the browser instance: ```ts // keep the browser instance alive for 120 seconds const { page, browser } = await hubBrowser({ keepAlive: 120 }) ``` The maximum idle timeout is 600 seconds (10 minutes). ::tip Once NuxtHub supports [Durable Objects](https://github.com/nuxt-hub/core/issues/50){rel="nofollow"}, you will be able to create a single browser instance that will stay open for a long time, and you will be able to reuse it across requests. :: ## Screenshot Capture Taking a screenshot of a website is a common use case for a headless browser. Let's create an API route to capture a screenshot of a website: ```ts [server/api/screenshot.ts] import { z } from 'zod' export default eventHandler(async (event) => { // Get the URL and theme from the query parameters const { url, theme } = await getValidatedQuery(event, z.object({ url: z.string().url(), theme: z.enum(['light', 'dark']).optional().default('light') }).parse) // Get a browser session and open a new page const { page } = await hubBrowser() // Set the viewport to full HD & set the color-scheme await page.setViewport({ width: 1920, height: 1080 }) await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: theme }]) // Go to the URL and wait for the page to load await page.goto(url, { waitUntil: 'domcontentloaded' }) // Return the screenshot as response setHeader(event, 'content-type', 'image/jpeg') return page.screenshot() }) ``` On the application side, we can create a simple form to call our API endpoint: ```vue [pages/capture.vue] ``` That's it! You can now capture screenshots of websites using Puppeteer in your Nuxt application. ### Storing the screenshots You can store the screenshots in the Blob storage: ```ts const screenshot = await page.screenshot() // Upload the screenshot to the Blob storage const filename = `screenshots/${url.value.replace(/[^a-zA-Z0-9]/g, '-')}.jpg` const blob = await hubBlob().put(filename, screenshot) ``` ::note{to="https://hub.nuxt.com/docs/features/blob"} Learn more about the Blob storage. :: ## Metadata Extraction Another common use case is to extract metadata from a website. ```ts [server/api/metadata.ts] import { z } from 'zod' export default eventHandler(async (event) => { // Get the URL from the query parameters const { url } = await getValidatedQuery(event, z.object({ url: z.string().url() }).parse) // Get a browser instance and navigate to the url const { page } = await hubBrowser() await page.goto(url, { waitUntil: 'networkidle0' }) // Extract metadata from the page const metadata = await page.evaluate(() => { const getMetaContent = (name) => { const element = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`) return element ? element.getAttribute('content') : null } return { title: document.title, description: getMetaContent('description') || getMetaContent('og:description'), favicon: document.querySelector('link[rel="shortcut icon"]')?.href || document.querySelector('link[rel="icon"]')?.href, ogImage: getMetaContent('og:image'), origin: document.location.origin } }) return metadata }) ``` Visiting `/api/metadata?url=https://cloudflare.com` will return the metadata of the website: ```json { "title": "Connect, Protect and Build Everywhere | Cloudflare", "description": "Make employees, applications and networks faster and more secure everywhere, while reducing complexity and cost.", "favicon": "https://www.cloudflare.com/favicon.ico", "ogImage": "https://cf-assets.www.cloudflare.com/slt3lc6tev37/2FNnxFZOBEha1W2MhF44EN/e9438de558c983ccce8129ddc20e1b8b/CF_MetaImage_1200x628.png", "origin": "https://www.cloudflare.com" } ``` To store the metadata of a website, you can use the [Key Value Storage](https://hub.nuxt.com/docs/features/kv). Or directly leverage [Caching](https://hub.nuxt.com/docs/features/cache) on this API route: ```ts [server/api/metadata.ts] export default cachedEventHandler(async (event) => { // ... }, { maxAge: 60 * 60 * 24 * 7, // 1 week swr: true, // Use the URL as key to invalidate the cache when the URL changes // We use btoa to transform the URL to a base64 string getKey: (event) => btoa(getQuery(event).url), }) ``` ## PDF Generation You can also generate PDF using `hubBrowser()`, this is useful if you want to generate an invoice or a receipt for example. Let's create a `/_invoice` page with Vue that we will use as template to generate a PDF: ```vue [pages/_invoice.vue] ``` To avoid having any styling issues, we recommend to keep your `app.vue` as minimal as possible: ```vue [app.vue] ``` And move most of your head management, style & HTML structure in [`layouts/default.vue`](https://nuxt.com/docs/guide/directory-structure/layouts#default-layout){rel="nofollow"}. Lastly, we need to create a `layouts/blank.vue` to avoid having any layout on our `_invoice` page: ```vue [layouts/blank.vue] ``` This will ensure that no header, footer or any other layout elements are rendered. Now, let's create our server route to generate the PDF: ```ts [server/routes/invoice.pdf.ts] export default eventHandler(async (event) => { const { page } = await hubBrowser() await page.goto(`${getRequestURL(event).origin}/_invoice`) setHeader(event, 'Content-Type', 'application/pdf') return page.pdf({ format: 'A4' }) }) ``` You can now display links to download or open the PDF in your pages: ```vue [pages/index.vue] ``` # Cache Pages, API & Functions NuxtHub Cache is powered by [Nitro's cache storage](https://nitro.unjs.io/guide/cache#customize-cache-storage){rel="nofollow"} and uses [Cloudflare Workers KV](https://developers.cloudflare.com/kv){rel="nofollow"} as the cache storage. It allows you to cache API routes, server functions, and pages in your application. ## Getting Started Enable the cache storage in your NuxtHub project by adding the `cache` property to the `hub` object in your `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { cache: true } }) ``` ::note This option will configure [Nitro's cache storage](https://nitro.unjs.io/guide/cache#customize-cache-storage){rel="nofollow"} to use [Cloudflare Workers KV](https://developers.cloudflare.com/kv){rel="nofollow"} as well as creating a new storage namespace for your project when you deploy it. :: Once your Nuxt project is deployed, you can manage your cache entries in the `Cache` section of your project in the [NuxtHub admin](https://admin.hub.nuxt.com/){rel="nofollow"}. ![NuxtHub Admin Cache](https://hub.nuxt.com/images/landing/nuxthub-admin-cache.png){dataZoomSrc="/images/landing/nuxthub-admin-cache.png" height="515" width="915"} In development, checkout the Hub Cache section in the Nuxt Devtools. ## API Routes Caching To cache Nuxt API and server routes, use the `cachedEventHandler` function. This function will cache the response of the server route into the cache storage. ```ts [server/api/cached-route.ts] import type { H3Event } from 'h3' export default cachedEventHandler((event) => { return { success: true, date: new Date().toISOString() } }, { maxAge: 60 * 60, // 1 hour getKey: (event: H3Event) => event.path }) ``` The above example will cache the response of the `/api/cached-route` route for 1 hour. The `getKey` function is used to generate the key for the cache entry. ::note{to="https://nitro.unjs.io/guide/cache#options"} Read more about [Nitro Cache options](https://nitro.unjs.io/guide/cache#options){rel="nofollow"}. :: ## Server Functions Caching Using the `cachedFunction` function, You can cache the response of a server function based on the arguments passed to the function. ::tip This is useful to cache the result of a function used in multiple API routes or within authenticated routes. :: ```ts [server/utils/cached-function.ts] import type { H3Event } from 'h3' export const getRepoStarCached = defineCachedFunction(async (event: H3Event, repo: string) => { const data: any = await $fetch(`https://api.github.com/repos/${repo}`) return data.stargazers_count }, { maxAge: 60 * 60, // 1 hour name: 'ghStars', getKey: (event: H3Event, repo: string) => repo }) ``` The above example will cache the result of the `getRepoStarCached` function for 1 hour. ::important It is important to note that the `event` argument should always be the first argument of the cached function. Nitro leverages `event.waitUntil` to keep the instance alive while the cache is being updated while the response is sent to the client. :br [Read more about this in the Nitro docs](https://nitro.unjs.io/guide/cache#edge-workers){rel="nofollow"}. :: ## Routes Caching You can enable route caching in your `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ routeRules: { '/blog/**': { cache: { maxAge: 60 * 60, // other options like name, group, swr... } } } }) ``` ::note Read more about [Nuxt's route rules](https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering){rel="nofollow"}. :: ## Cache Invalidation When using the `defineCachedFunction` or `defineCachedEventHandler` functions, the cache key is generated using the following pattern: ```ts `${options.group}:${options.name}:${options.getKey(...args)}.json` ``` The defaults are: - `group`: `'nitro'` - `name`: `'handlers'` for API routes, `'functions'` for server functions, or `'routes'` for route handlers For example, the following function: ```ts const getAccessToken = defineCachedFunction(() => { return String(Date.now()) }, { maxAge: 60, name: 'getAccessToken', getKey: () => 'default' }) ``` Will generate the following cache key: ```ts nitro:functions:getAccessToken:default.json ``` You can invalidate the cached function entry from your storage using cache key. ```ts await useStorage('cache').removeItem('nitro:functions:getAccessToken:default.json') ``` You can use the `group` and `name` options to invalidate multiple cache entries based on their prefixes. ```ts // Gets all keys that start with nitro:handlers await useStorage('cache').clear('nitro:handlers') ``` ::note{to="https://nitro.unjs.io/guide/cache"} Read more about Nitro Cache. :: ## Cache Expiration As NuxtHub leverages Cloudflare Workers KV to store your cache entries, we leverage the [`expiration` property](https://developers.cloudflare.com/kv/api/write-key-value-pairs/#expiring-keys){rel="nofollow"} of the KV binding to handle the cache expiration. By default, `stale-while-revalidate` behavior is enabled. If an expired cache entry is requested, the stale value will be served while the cache is asynchronously refreshed. This also means that all cache entries will remain in your KV namespace until they are manually invalidated/deleted. To disable this behavior, set `swr` to `false` when defining a cache rule. This will delete the cache entry once `maxAge` is reached. ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { routeRules: { '/blog/**': { cache: { maxAge: 60 * 60, swr: false // other options like name and group... } } } }) ``` ::note If you set an expiration (`maxAge`) lower than `60` seconds, NuxtHub will set the KV entry expiration to `60` seconds in the future (Cloudflare KV limitation) so it can be removed automatically. :: ## Pricing ::pricing-table{:tabs='["KV"]'} :: # SQL Database NuxtHub Database uses [Cloudflare D1](https://developers.cloudflare.com/d1/){rel="nofollow"}, a managed, serverless database built on SQLite to store and retrieve relational data. ## Getting Started Enable the database in your NuxtHub project by adding the `database` property to the `hub` object in your `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { database: true } }) ``` ::note This option will use Cloudflare platform proxy in development and automatically create a [Cloudflare D1](https://developers.cloudflare.com/d1){rel="nofollow"} database for your project when you [deploy it](https://hub.nuxt.com/docs/getting-started/deploy). :: ::tip Checkout our [Drizzle ORM recipe](https://hub.nuxt.com/docs/recipes/drizzle) to get started with the database by providing a schema and migrations. :: During local development, you can view and edit your database in the Nuxt DevTools. Once your project is deployed, you can inspect the database in the NuxtHub Admin Dashboard. ::tabs :::div{label="Nuxt DevTools"} ![Nuxt DevTools Database](https://hub.nuxt.com/images/landing/nuxt-devtools-database.png){dataZoomSrc="/images/landing/nuxt-devtools-database.png" height="515" width="915"} ::: :::div{label="NuxtHub Admin"} ![NuxtHub Admin Database](https://hub.nuxt.com/images/landing/nuxthub-admin-database.png){dataZoomSrc="/images/landing/nuxthub-admin-database.png" height="515" width="915"} ::: :: ## `hubDatabase()` Server composable that returns a [D1 database client](https://developers.cloudflare.com/d1/build-databases/query-databases/){rel="nofollow"}. ```ts const db = hubDatabase() ``` ::callout This documentation is a small reflection of the [Cloudflare D1 documentation](https://developers.cloudflare.com/d1/build-databases/query-databases/){rel="nofollow"}. We recommend reading it to understand the full potential of the D1 database. :: ### `prepare()` Generates a prepared statement to be used later. ```ts const stmt = db.prepare('SELECT * FROM users WHERE name = "Evan You"') ``` ::callout Best practice is to use prepared statements which are precompiled objects used by the database to run the SQL. This is because prepared statements lead to overall faster execution and prevent SQL injection attacks. :: ### `bind()` Binds parameters to a prepared statement, allowing you to pass dynamic values to the query. ```ts const stmt = db.prepare('SELECT * FROM users WHERE name = ?1') stmt.bind('Evan You') // SELECT * FROM users WHERE name = 'Evan You' ``` The `?` character followed by a number (1-999) represents an ordered parameter. The number represents the position of the parameter when calling `.bind(...params)`. ```ts const stmt = db .prepare('SELECT * FROM users WHERE name = ?2 AND age = ?1') .bind(3, 'Leo Chopin') // SELECT * FROM users WHERE name = 'Leo Chopin' AND age = 3 ``` If you instead use anonymous parameters (without a number), the values passed to `bind` will be assigned in order to the `?` placeholders in the query. It's recommended to use ordered parameters to improve maintainable and ensure that removing or reordering parameters will not break your queries. ```ts const stmt = db .prepare('SELECT * FROM users WHERE name = ? AND age = ?') .bind('Leo Chopin', 3) // SELECT * FROM users WHERE name = 'Leo Chopin' AND age = 3 ``` ### `all()` Returns all rows as an array of objects, with each result row represented as an object on the results property (see [Return Object](https://hub.nuxt.com/#return-object)). ```ts const { results } = db.prepare('SELECT name, year FROM frameworks LIMIT 2').all() console.log(results) /* [ { name: "Laravel", year: 2011, }, { name: "Nuxt", year: 2016, } ] */ ``` The method return an object that contains the results (if applicable), the success status and a meta object: ```ts { results: array | null, // [] if empty, or null if it does not apply success: boolean, // true if the operation was successful, false otherwise meta: { duration: number, // duration of the operation in milliseconds rows_read: number, // the number of rows read (scanned) by this query rows_written: number // the number of rows written by this query } } ``` ### `first()` Returns the first row of the results. This does not return metadata like the other methods. Instead, it returns the object directly. ```ts const framework = db.prepare('SELECT * FROM frameworks WHERE year = ?1').bind(2016).first() console.log(framework) /* { name: "Nuxt", year: 2016, } */ ``` Get a specific column from the first row by passing the column name as a parameter: ```ts const total = db.prepare('SELECT COUNT(*) AS total FROM frameworks').first('total') console.log(total) // 23 ``` ### `raw()` Returns results as an array of arrays, with each row represented by an array. The return type is an array of arrays, and does not include query metadata. ```ts const rows = db.prepare('SELECT name, year FROM frameworks LIMIT 2').raw() console.log(rows); /* [ [ "Laravel", 2011 ], [ "Nuxt", 2016 ], ] */ ``` Column names are not included in the result set by default. To include column names as the first row of the result array, use `.raw({ columnNames: true })`. ```ts const rows = db.prepare('SELECT name, year FROM frameworks LIMIT 2').raw({ columnNames: true }) console.log(rows); /* [ [ "name", "year" ], [ "Laravel", 2011 ], [ "Nuxt", 2016 ], ] */ ``` ### `run()` Runs the query (or queries), but returns no results. Instead, `run()` returns the metrics only. Useful for write operations like UPDATE, DELETE or INSERT. ```ts const result = db .prepare('INSERT INTO frameworks (name, year) VALUES ("?1", ?2)') .bind('Nitro', 2022) .run() console.log(result) /* { success: true meta: { duration: 62, } } */ ``` ### `batch()` Sends multiple SQL statements inside a single call to the database. This can have a huge performance impact by reducing latency caused by multiple network round trips to the database. Each statement in the list will execute/commit sequentially and non-concurrently before returning the results in the same order. `batch` acts as a SQL transaction, meaning that if any statement fails, the entire transaction is aborted and rolled back. ```ts const [info1, info2] = await db.batch([ db.prepare('UPDATE frameworks SET version = ?1 WHERE name = ?2').bind(3, 'Nuxt'), db.prepare('UPDATE authors SET age = ?1 WHERE username = ?2').bind(32, 'atinux'), ]) ``` `info1` and `info2` will contain the results of the first and second queries, similar to the results of the [`.all()`](https://hub.nuxt.com/#all) method (see [Return Object](https://hub.nuxt.com/#return-object)). ```ts console.log(info1) /* { results: [], success: true, meta: { duration: 62, rows_read: 0, rows_written: 1 } } */ ``` The object returned is the same as the [`.all()`](https://hub.nuxt.com/#all) method. ### `exec()` Executes one or more queries directly without prepared statements or parameters binding. The input can be one or multiple queries separated by \n. If an error occurs, an exception is thrown with the query and error messages, execution stops, and further queries are not executed. ```ts const result = await hubDatabase().exec(`CREATE TABLE IF NOT EXISTS frameworks (id INTEGER PRIMARY KEY, name TEXT NOT NULL, year INTEGER NOT NULL DEFAULT 0)`) console.log(result) /* { count: 1, duration: 23 } */ ``` ::callout This method can have poorer performance (prepared statements can be reused in some cases) and, more importantly, is less safe. Only use this method for maintenance and one-shot tasks (for example, migration jobs). :: ## Working with JSON Cloudflare D1 supports querying and parsing JSON data. This can improve performance by reducing the number of round trips to your database. Instead of querying a JSON column, extracting the data you need, and using that data to make another query, you can do all of this work in a single query by using JSON functions. JSON columns are stored as `TEXT` columns in your database. ```ts const framework = { name: 'Nuxt', year: 2016, projects: [ 'NuxtHub', 'Nuxt UI' ] } await hubDatabase() .prepare('INSERT INTO frameworks (info) VALUES (?1)') .bind(JSON.stringify(framework)) .run() ``` Then, using D1's [JSON functions](https://developers.cloudflare.com/d1/sql-api/query-json/){rel="nofollow"}, which are built on the [SQLite JSON extension](https://www.sqlite.org/json1.html){rel="nofollow"}, you can make queries using the data in your JSON column. ```ts const framework = await db.prepare('SELECT * FROM frameworks WHERE (json_extract(info, "$.name") = "Nuxt")').first() console.log(framework) /* { "id": 1, "info": "{\"name\":\"Nuxt\",\"year\":2016,\"projects\":[\"NuxtHub\",\"Nuxt UI\"]}" } */ ``` ::callout For an in-depth guide on querying JSON and a list of all supported functions, see [Cloudlare's Query JSON documentation](https://developers.cloudflare.com/d1/sql-api/query-json/#generated-columns){rel="nofollow"}. :: ## Using an ORM Instead of using `hubDatabase()` to make interact with your database, you can use an ORM like [Drizzle ORM](https://hub.nuxt.com/docs/recipes/drizzle). This can improve the developer experience by providing a type-safe API, migrations, and more. ## Database Migrations Database migrations provide version control for your database schema. They track changes and ensure consistent schema evolution across all environments through incremental updates. NuxtHub supports SQL migration files (`.sql`). ### Migrations Directories NuxtHub scans the `server/database/migrations` directory for migrations **for each [Nuxt layer](https://nuxt.com/docs/getting-started/layers){rel="nofollow"}**. If you need to scan additional migrations directories, you can specify them in your `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { // Array of additional migration directories to scan databaseMigrationsDirs: [ 'my-module/db-migrations/' ] } }) ``` ::note NuxtHub will scan both `server/database/migrations` and `my-module/db-migrations` directories for `.sql` files. :: If you want more control to the migrations directories or you are working on a [Nuxt module](https://nuxt.com/docs/guide/going-further/modules){rel="nofollow"}, you can use the `hub:database:migrations:dirs` hook: ::code-group ```ts [modules/auth/index.ts] import { createResolver, defineNuxtModule } from 'nuxt/kit' export default defineNuxtModule({ meta: { name: 'my-auth-module' }, setup(options, nuxt) { const { resolve } = createResolver(import.meta.url) nuxt.hook('hub:database:migrations:dirs', (dirs) => { dirs.push(resolve('db-migrations')) }) } }) ``` ```sql [modules/auth/db-migrations/0001_create-users.sql] CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL ); ``` :: ::tip All migrations files are copied to the `.data/hub/database/migrations` directory when you run Nuxt. This consolidated view helps you track all migrations and enables you to use `npx nuxthub database migrations ` commands. :: ### Automatic Application All `.sql` files in the database migrations directories are automatically applied when you: - Start the development server (`npx nuxt dev` or [`npx nuxt dev --remote`](https://hub.nuxt.com/docs/getting-started/remote-storage)) - Preview builds locally ([`npx nuxthub preview`](https://hub.nuxt.com/changelog/nuxthub-preview)) - Deploy via [`npx nuxthub deploy`](https://hub.nuxt.com/docs/getting-started/deploy#nuxthub-cli) or [Cloudflare Pages CI](https://hub.nuxt.com/docs/getting-started/deploy#cloudflare-pages-ci) ::tip All applied migrations are tracked in the `_hub_migrations` database table. :: ### Creating Migrations Generate a new migration file using: ```bash [Terminal] npx nuxthub database migrations create ``` ::important Migration names must only contain alphanumeric characters and `-` (spaces are converted to `-`). :: Migration files are created in `server/database/migrations/` and are prefixed by an auto-incrementing sequence number. This migration number is used to determine the order in which migrations are run. ```bash [Example] > npx nuxthub database migrations create create-todos ✔ Created ./server/database/migrations/0001_create-todos.sql ``` After creation, add your SQL queries to modify the database schema. For example, migrations should be used to create tables, add/delete/modify columns, and add/remove indexes. ```sql [0001_create-todos.sql] -- Migration number: 0001 2025-01-30T17:17:37.252Z CREATE TABLE `todos` ( `id` integer PRIMARY KEY NOT NULL, `user_id` integer NOT NULL, `title` text NOT NULL, `completed` integer DEFAULT 0 NOT NULL, `created_at` integer NOT NULL ); ``` ::note{to="https://hub.nuxt.com/docs/recipes/drizzle#npm-run-dbgenerate"} With [Drizzle ORM](https://hub.nuxt.com/docs/recipes/drizzle), migrations are automatically created when you run `npx drizzle-kit generate`. :: ### Checking Migration Status View pending and applied migrations across environments: ```bash [Terminal] # Local environment status npx nuxthub database migrations list # Preview environment status npx nuxthub database migrations list --preview # Production environment status npx nuxthub database migrations list --production ``` ```bash [Example output] > npx nuxthub database migrations list --production ℹ Connected to project atidone. ℹ Using https://todos.nuxt.dev to retrieve migrations. ✔ Found 1 migration on atidone... ✅ ./server/database/migrations/0001_create-todos.sql 10/25/2024, 2:43:32 PM 🕒 ./server/database/migrations/0002_create-users.sql Pending ``` ### Marking Migrations as Applied For databases with existing migrations, prevent NuxtHub from rerunning them by marking them as applied: ```bash [Terminal] # Mark applied in local environment npx nuxthub database migrations mark-all-applied # Mark applied in preview environment npx nuxthub database migrations mark-all-applied --preview # Mark applied in production environment npx nuxthub database migrations mark-all-applied --production ``` ::collapsible{name="self-hosting docs"} When [self-hosting](https://hub.nuxt.com/docs/getting-started/deploy#self-hosted), set these environment variables before running commands: :br :br ```bash [Terminal] NUXT_HUB_PROJECT_URL= NUXT_HUB_PROJECT_SECRET_KEY= nuxthub database migrations mark-all-applied ``` :: ### Post-Migration Queries ::important This feature is for advanced use cases. As the queries are run after the migrations process (see [Automatic Application](https://hub.nuxt.com/#automatic-application)), you want to make sure your queries are idempotent. :: Sometimes you need to run additional queries after migrations are applied without tracking them in the migrations table. NuxtHub provides the `hub:database:queries:paths` hook for this purpose: ::code-group ```ts [modules/admin/index.ts] import { createResolver, defineNuxtModule } from 'nuxt/kit' export default defineNuxtModule({ meta: { name: 'my-auth-module' }, setup(options, nuxt) { const { resolve } = createResolver(import.meta.url) nuxt.hook('hub:database:queries:paths', (queries) => { // Add SQL files to run after migrations queries.push(resolve('./db-queries/seed-admin.sql')) }) } }) ``` ```sql [modules/admin/db-queries/seed-admin.sql] INSERT OR IGNORE INTO admin_users (id, email, password) VALUES (1, 'admin@nuxt.com', 'admin'); ``` :: ::note These queries run after all migrations are applied but are not tracked in the `_hub_migrations` table. Use this for operations that should run when deploying your project. :: ### Foreign Key Constraints If you are using [Drizzle ORM](https://hub.nuxt.com/docs/recipes/drizzle) to generate your database migrations, your generated migration files will use `PRAGMA foreign_keys = ON | OFF;`. This is not supported by Cloudflare D1. Instead, they support [defer foreign key constraints](https://developers.cloudflare.com/d1/sql-api/foreign-keys/#defer-foreign-key-constraints){rel="nofollow"}. You need to update your migration file to use `PRAGMA defer_foreign_keys = on|off;` instead: ```diff [Example] -PRAGMA foreign_keys = OFF; +PRAGMA defer_foreign_keys = on; ALTER TABLE ... -PRAGMA foreign_keys = ON; +PRAGMA defer_foreign_keys = off; ``` ## Limits - The maximum database size is 10 GB - The maximum number of columns per table is 100 See all of the [D1 Limits](https://developers.cloudflare.com/d1/platform/limits/){rel="nofollow"} ## Pricing ::pricing-table{:tabs='["DB"]'} :: # Key Value Storage NuxtHub Key Value Storage uses [Unstorage](https://unstorage.unjs.io){rel="nofollow"} with [Cloudflare Workers KV](https://developers.cloudflare.com/kv){rel="nofollow"} to store key-value data. ## Use Cases - **Frequently Read Data** - values are cached in regional data centers closer to the user so multiple requests from the same region will be faster - **Per-Object Expiration** - passing a `ttl` when writing object will delete it after a certain amount of time - **Eventual Consistency** - cached values are eventually consistent and may take up to 60 seconds to update across all regions, allowing for improved performance when strong consistency is not required ## Getting Started Enable the key-value storage in your NuxtHub project by adding the `kv` property to the `hub` object in your `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { kv: true } }) ``` ::note This option will use Cloudflare platform proxy in development and automatically create a [Cloudflare Workers KV](https://developers.cloudflare.com/kv){rel="nofollow"} namespace for your project when you [deploy it](https://hub.nuxt.com/docs/getting-started/deploy). :: You can inspect your KV namespace during local development in the Nuxt DevTools or after a deployment using the NuxtHub Admin Dashboard. ::tabs :::div{label="Nuxt DevTools"} ![Nuxt DevTools KV](https://hub.nuxt.com/images/landing/nuxt-devtools-kv.png){dataZoomSrc="/images/landing/nuxt-devtools-kv.png" height="515" width="915"} ::: :::div{label="NuxtHub Admin"} ![NuxtHub Admin KV](https://hub.nuxt.com/images/landing/nuxthub-admin-kv.png){dataZoomSrc="/images/landing/nuxthub-admin-kv.png" height="515" width="915"} ::: :: ## How KV Works NuxtHub uses [Cloudflare Workers KV](https://developers.cloudflare.com/kv){rel="nofollow"} to store key-value data a few centralized data centers. Then, when data is requested, it will cache the responses in regional data centers closer to the user to speed up future requests coming from the same region. This caching means that KV is optimized for high-read use cases, but it also means that changes like editing or deleting data are **eventually consistent** and may take up to 60 seconds to propagate to all regions. Even if a request is made for a key that does not exist in the KV namespace, that result will be cached for up to 60 seconds. If you need a strongly consistent data model, where changes are immediately visible to all users, a [NuxtHub database](https://hub.nuxt.com/docs/features/database) may be a better fit. To learn more about how KV works, check out the [Cloudflare KV documentation](https://developers.cloudflare.com/kv/concepts/how-kv-works/){rel="nofollow"}. ## `hubKV()` `hubKV()` is a server composable that returns an [Unstorage](https://unstorage.unjs.io){rel="nofollow"} instance with [Cloudflare KV binding](https://unstorage.unjs.io/drivers/cloudflare#cloudflare-kv-binding){rel="nofollow"} as the [driver](https://unstorage.unjs.io/drivers/cloudflare){rel="nofollow"}. ### Set an item Puts an item in the storage. ```ts await hubKV().set('vue', { year: 2014 }) // using prefixes to organize your KV namespace, useful for the `keys` operation await hubKV().set('vue:nuxt', { year: 2016 }) ``` ::note The maximum size of a value is 25 MiB and the maximum length of a key is 512 bytes. :: #### Expiration By default, items in your KV namespace will never expire. You can delete them manually using the [`del()`](https://hub.nuxt.com/#delete-an-item) method or set a TTL (time to live) in seconds. The item will be deleted after the TTL has expired. The `ttl` option maps to Cloudflare's [`expirationTtl`](https://developers.cloudflare.com/kv/api/write-key-value-pairs/#reference){rel="nofollow"} option. Values that have recently been read will continue to return the cached value for up to 60 seconds and may not be immediately deleted for all regions. ```ts await hubKV().set('vue:nuxt', { year: 2016 }, { ttl: 60 }) ``` ### Get an item Retrieves an item from the Key-Value storage. ```ts const vue = await hubKV().get('vue') /* { year: 2014 } */ ``` ### Has an item Checks if an item exists in the storage. ```ts const hasAngular = await hubKV().has('angular') // false const hasVue = await hubKV().has('vue') // true ``` ### Delete an item Delete an item with the given key from the storage. ```ts await hubKV().del('react') ``` ### Clear the KV namespace Deletes all items from the KV namespace.. ```ts await hubKV().clear() ``` To delete all items for a specific prefix, you can pass the prefix as an argument. We recommend using prefixes for better organization in your KV namespace. ```ts await hubKV().clear('react') ``` ### List all keys Retrieves all keys from the KV storage. ```ts const keys = await hubKV().keys() /* [ 'react', 'react:gatsby', 'react:next', 'vue', 'vue:nuxt', 'vue:quasar' ] ``` To get the keys starting with a specific prefix, you can pass the prefix as an argument. We recommend using prefixes for better organization in your KV namespace. ```ts const vueKeys = await hubKV().keys('vue') /* [ 'vue:nuxt', 'vue:quasar' ] */ ``` ## Limits - The maximum size of a value is 25 MiB. - The maximum length of a key is 512 bytes. - The TTL must be at least 60 seconds. - There is a maximum of 1 write to the same key per second ([KV write rate limit](https://developers.cloudflare.com/kv/api/write-key-value-pairs/#limits-to-kv-writes-to-the-same-key){rel="nofollow"}). Learn more about [Cloudflare KV limits](https://developers.cloudflare.com/kv/platform/limits/){rel="nofollow"}. ## Learn More `hubKV()` is an instance of [unstorage](https://unstorage.unjs.io/guide#interface){rel="nofollow"} with the [Cloudflare KV binding](https://unstorage.unjs.io/drivers/cloudflare#cloudflare-kv-binding){rel="nofollow"} driver. ## Pricing ::pricing-table{:tabs='["KV"]'} :: # Open API Documentation ::warning This is currently experimental and subject to change in the future. :: ## Getting Started NuxtHub uses Nitro's OpenAPI generation to access your Nuxt project's API. To enable the API, you need to add enable Nitro's experimental `openAPI` feature. You can do this by adding the `nitro.experimental.openAPI` property to your `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { experimental: { openAPI: true } } }) ``` After you deploy your project, NuxtHub Admin will showcase your API documentation using [Scalar](https://scalar.com){rel="nofollow"}. ![Nuxt Open API Scalar integration](https://hub.nuxt.com/images/landing/nuxthub-admin-open-api.png){dataZoomSrc="/images/landing/nuxthub-admin-open-api.png" height="515" width="915"} You can define route handler meta (at build time) using the `defineRouteMeta` macro: ```ts [pages/api/ok.ts] defineRouteMeta({ openAPI: { description: 'Test route description', parameters: [{ in: "query", name: "test", required: true }], }, }); export default defineEventHandler(() => "OK"); ``` ::note{to="https://swagger.io/specification/v3/"} See swagger specification for available OpenAPI options. :: ## Nuxt DevTools In development, you can use Nuxt DevTools to access your API routes using the `Open API` or `Server Routes` tabs. It list all the API routes in your project as well as providing a playground to send and test your endpoints. Check out the [Nuxt DevTools](https://devtools.nuxt.com/){rel="nofollow"} documentation for more information. ![NuxtHub Admin Cache](https://hub.nuxt.com/images/landing/nuxt-devtools-api-routes.png){dataZoomSrc="/images/landing/nuxt-devtools-api-routes.png" height="515" width="915"} # Realtime & WebSockets ## Getting Started Enable [Nitro's experimental WebSocket](https://nitro.build/guide/websocket){rel="nofollow"} support by adding the following to your `nuxt.config.ts` file: ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { experimental: { websocket: true } }, hub: { workers: true } }) ``` ::note We need to enable the `workers` option to use the WebSocket support as Durable Objects are only supported on Cloudflare Workers. :: ## Example Let's create a simple application that display how many users are connected to the website. First, let's create a websocket handler on `/ws/visitors` route: ```ts [server/routes/ws/visitors.ts] export default defineWebSocketHandler({ open(peer) { // We subscribe to the 'visitors' channel peer.subscribe('visitors') // We publish the number of connected users to the 'visitors' channel peer.publish('visitors', peer.peers.size) // We send the number of connected users to the client peer.send(peer.peers.size) }, close(peer) { peer.unsubscribe('visitors') // Wait 500ms before sending the updated locations to the server setTimeout(() => { peer.publish('visitors', peer.peers.size) }, 500) }, }) ``` Install VueUse if you haven't already: ```bash [Terminal] npx nuxi module add vueuse ``` Let's use the [`useWebSocket`](https://vueuse.org/core/useWebSocket/){rel="nofollow"} composable from VueUse to subscribe to the `visitors` channel and display the number of connected users. ```vue [pages/visitors.vue] ``` See a full open source example on [GitHub](https://github.com/nuxt-hub/multiplayer-globe){rel="nofollow"}, including geolocation tracking. # Vectorize (Vector Database) NuxtHub Vectorize provides configuration, deployment, and management of [Vectorize](https://developers.cloudflare.com/vectorize/best-practices/create-indexes/){rel="nofollow"}, Cloudflare's vector database. A vector database stores numerical representations (embeddings) of data, allowing efficient similarity searches. Machine learning models generate these embeddings by converting text, images, or other data types into numerical arrays that can be compared with Vectorize to find similar vectors. ::note --- to: https://developers.cloudflare.com/vectorize/reference/what-is-a-vector-database/ --- Learn what vector databases are on Cloudflare's documentation :: ::warning Vectorize is only available in local development when using [remote storage](https://hub.nuxt.com/docs/getting-started/remote-storage). :: ## Use Cases Vectorize can be used for: - **Retrieval Augmented Generation (RAG)** - store embeddings for documents that can be used as context for LLMs - **Semantic Search** - query vectors to find results similar to an input - **Recommendation Engines** - query vectors to find similar content ## Getting Started Vectorize indexes are managed in your NuxtHub project within the `hub.vectorize` object in your `nuxt.config.ts` file. Multiple indexes can be created using separate keys. ### Create an index Creating an index can take up to four values: 1. **An index name** (e.g. `prod-search-index` or `recommendations-idx-dev`) :br :br The name of your index must contain only lowercase characters and hyphens (`-`). The index name is limited to 51 characters. :br :br 2. `dimension` - the [dimension size](https://hub.nuxt.com/#dimensions) of each vector (e.g. 384 or 1536) :br :br Dimensions define the size of each vector, which should match the output of the model generating the embeddings. :br :br 3. `metric` - the [distance metric](https://hub.nuxt.com/#distance-metrics) to use for calculating vector similarity :br :br The distance metric is the function that determines how close vectors are to each other. Possible values are `cosine`, `euclidean`, and `dot-product`. :br :br 4. `metadataIndexes` (optional) :br :br A [`metadataIndex`](https://hub.nuxt.com/#create-metadata-indexes) object specifying metadata properties that may be used to [filter queries](https://hub.nuxt.com/#metadata-filtering) :br :br ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { vectorize: { : { dimensions: , // depends on the model used to generate the vectors metric: "dot-product" | "cosine" | "euclidean", metadataIndexes: { : "string" | "number" | "boolean" } } } } }) ``` ::collapsible{name="example"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { vectorize: { products: { dimensions: 768, metric: 'cosine', metadataIndexes: { name: 'string', price: 'number' } }, reviews: { dimensions: 1024, metric: 'euclidean', metadataIndexes: { rating: 'number' } }, } } }) ``` :: ::note [Cloudflare Vectorize](https://developers.cloudflare.com/vectorize){rel="nofollow"} indexes will be created for your project when you [deploy it](https://hub.nuxt.com/docs/getting-started/deploy). Created Vectorize indexes contain a unique 4 character suffix. :: ### Use existing indexes To use an existing index, add it as a binding to your Cloudflare project and configure it in `nuxt.config.ts`. 1. On the Cloudflare dashboard → Workers & Pages → Your Pages project 2. Go to Settings → Bindings → Add 3. Select Vectorize database - Set the variable name to `VECTORIZE_`. The entire variable name should be capitalised. - Select the existing Vectorize index 4. Add the index configuration to `hub.vectorize` in `nuxt.config.ts`. - The index name should match `` used in the variable name, and must be lowercase. - The `dimensions` and `metric` values should match the ones used when creating the index - If your index already includes values, the `metadataIndexes` should include any existing metadata indexes. New `metadataIndexes` added in your configuration will not include any existing vectors. You can upsert these vectors to have them included in new metadata indexes. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { vectorize: { : { dimensions: , // depends on the model used to generate the vectors metric: "dot-product" | "cosine" | "euclidean", metadataIndexes: { : "string" | "number" | "boolean" } } } } }) ``` ### Deployment Vectorize only works in local development when using [remote storage](https://hub.nuxt.com/docs/getting-started/remote-storage) with the `npx nuxt dev --remote` command. This means to begin using Vectorize, you need to [deploy your project](https://hub.nuxt.com/docs/getting-started/deploy) to create the indexes before accessing them through remote storage. Similar to the other NuxtHub offerings, indexes can be created in either preview or production environments. ::important{title="Test"} **Cloudflare Vectorize index configurations, like the dimension size and distance metric, cannot be modified after creation.** :br :br Modifying an index's dimension size or distance metric will create a new empty index without migrating any data, disconnecting your existing index (and its data) from your project. :br :br Consider your index configuration carefully before creating it to avoid data loss and reconnection issues. :: ## `hubVectorize()` Server composable that returns a [Vectorize index](https://developers.cloudflare.com/vectorize/reference/client-api/){rel="nofollow"}. ```ts const index = hubVectorize("") ``` IntelliSense will suggest `` based on the indexes configured in [`hub.vectorize`](https://hub.nuxt.com/#getting-started). ### `insert()` Inserts vectors with new IDs into the index. If a vector with the same vector ID already exists in the index, it will not be updated. If you need to update existing vectors, use the [upsert](https://hub.nuxt.com/#upsert) operation. ```ts // Mock Vectors // These will typically come from a machine-learning model const vectorsToInsert = [ { id: "123", values: [32.4, 6.5, 11.2, 10.3, 87.9] }, { id: "456", values: [2.5, 7.8, 9.1, 76.9, 8.5], metadata: { category: "product" }, }, ]; const inserted = await index.insert(vectorsToInsert); ``` See all available properties on the [Vector object](https://hub.nuxt.com/#vector-object). #### Params ::field-group :::field{required name="vectors" required="true" type="VectorObject[]"} An array of [Vector Objects](https://hub.nuxt.com/#vector-object) to insert into the index. ::: :: #### Return Returns [`VectorizeAsyncMutation`](https://hub.nuxt.com/#vectorizeasyncmutation). ::callout Vectorize Index mutations are processed asynchronously in the background. The `insert` operation returns a mutation identifier unique for that operation, and does not assert that the vector is available in the index. It typically takes a few seconds for inserted vectors to be available for querying in an index. :: ::note Insert vs Upsert - If the same vector id is **inserted** twice in a Vectorize index, the index will contain the vector that was added **first**. - If the same vector id is **upserted** twice in a Vectorize index, the index will contain the vector that was added **second**. - Use the `upsert` operation if you want to overwrite the vector value for a vector id that already exists in an index. :: ### `upsert()` Upserts vectors into an index. An upsert operation will insert vectors into the index if vectors with the same ID do not exist, and overwrite vectors with the same ID. ```ts const vectorsToUpsert = [ { id: "123", values: [32.4, 6.5, 11.2, 10.3, 87.9] }, { id: "456", values: [2.5, 7.8, 9.1, 76.9, 8.5], metadata: { category: "product" }, }, { id: "768", values: [29.1, 5.7, 12.9, 15.4, 1.1] }, ]; const upserted = await index.upsert(vectorsToUpsert); ``` See all available properties on the [Vector object](https://hub.nuxt.com/#vector-object). #### Params ::field-group :::field{required name="vectors" required="true" type="VectorObject[]"} An array of [Vector Objects](https://hub.nuxt.com/#vector-object) to insert into the index. ::: :: #### Return Returns [`VectorizeAsyncMutation`](https://hub.nuxt.com/#vectorizeasyncmutation). ::note Upserting does not merge or combine the values or metadata of an existing vector with the upserted vector: the upserted vector replaces the existing vector in full. :br :br To merge existing metadata, you will have to first query the index for the existing metadata, update the metadata with the new values, and upsert the vector with the merged metadata. :: ::callout Vectorize Index mutations are processed asynchronously in the background. The `upsert` operation returns a mutation identifier unique for that operation, and does not assert that the vector is updated in the index. It typically takes a few seconds for upserted vectors to be available for querying in an index. :: ### `query()` Query an index with the provided vector, which performs a vector search and returns the score(s) of the closest vectors based on the [configured distance metric](https://hub.nuxt.com/#distance-metrics). ```ts const queryVector = [32.4, 6.55, 11.2, 10.3, 87.9]; const matches = await index.query(queryVector); console.log(matches) /* { "count": 5, "matches": [ { "score": 0.999909486, "id": "5" }, { "score": 0.789848214, "id": "4" }, { "score": 0.720476967, "id": "1234" }, { "score": 0.463884663, "id": "6" }, { "score": 0.378282232, "id": "1" } ] } */ ``` Optionally, you can apply [metadata filters](https://hub.nuxt.com/#metadata-filtering) or a [namespace](https://hub.nuxt.com/#namespaces) to narrow the vector search space. ```ts const queryVector = [32.4, 6.55, 11.2, 10.3, 87.9]; const matches = await index.query(queryVector, { namespace: "my-namespace", filter: { rating: { $ne: 5, }, }, }); ``` #### Params ::field-group :::field{required name="vector" required="true" type="array"} Input vector that will be used to drive the similarity search. ::: :::field{name="options" type="object"} Query options. ::::collapsible :::::field{name="topK" type="number"} Number of returned matches. Defaults to `5`. The `topK` can be configured to specify the number of matches returned by the query operation. Vectorize now supports an upper limit of `100` for the `topK` value. However, for a query operation with `returnValues` set to `true` or `returnMetadata` set to `all`, `topK` would be limited to a maximum value of `20`. :br ::::: :::::field{name="returnValues" type="boolean"} Return vector values. See [Precision Vs. Response Times](https://hub.nuxt.com/#precision-vs-response-time). ::::: :::::field{name="returnMetadata" type="'all' | 'indexed' | 'none'"} Return vector metadata. Defaults to `'none'`. The `returnMetadata` field provides three ways to fetch vector metadata while querying: 1. `none`: Do not fetch metadata. 2. `indexed`: Fetched metadata only for the indexed metadata fields. There is no latency overhead with this option, but long text fields may be truncated. 3. `all`: Fetch all metadata associated with a vector. Queries may run slower with this option, and `topK` would be limited to 20. :br ::::: :::::field{name="namespace" type="string"} Only return vectors within a namespace. ::::: :::::field{name="filter" type="object"} Filter by vector metadata. See [metadata filtering](https://hub.nuxt.com/#metadata-filtering) ::::: :::: ::: :: ```ts const matches = await index.query(queryVector, { topK: 3, // return 3 matches returnValues: true, // return the vector values returnMetadata: "all", // return all metadata associated with the matches }); ``` #### Return Returns [`VectorizeMatches`](https://hub.nuxt.com/#vectorizematches). #### Precision vs. Response Time When querying vectors, you can specify whether to use: 1. **High-precision scoring** for increased precision of the query matches scores and the accuracy of the query results 2. **Approximate scoring** for faster response times (default). Using approximate scoring, the returned scores will be an approximation of the real distance (similarity) between your query and the returned vectors. You can enable high-precision scoring by setting `returnValues: true` on your query. This tells Vectorize to compute exact scores for matches, increasing the accuracy of the results. ```ts const matches = await index.query(queryVector, { returnValues: true, }); ``` ### `getByIds()` Retrieves the specified vectors by their ID, including values and metadata. ```ts const ids = ["11", "22", "33", "44"]; const vectors = await index.getByIds(ids); ``` #### Params ::field-group :::field{required name="ids" required="true" type="string[]"} List of vector IDs that should be returned. ::: :: #### Return Returns [`VectorizeVector`](https://hub.nuxt.com/#vectorizevector). ### `deleteByIds()` Deletes the vector IDs provided from the current index. ```ts const idsToDelete = ["11", "22", "33", "44"]; const deleted = await index.deleteByIds(idsToDelete); ``` #### Params ::field-group :::field{required name="ids" required="true" type="string[]"} List of vector IDs that should be deleted. ::: :: #### Return Returns [`VectorizeAsyncMutation`](https://hub.nuxt.com/#vectorizeasyncmutation). ::callout Vectorize Index mutations are processed asynchronously in the background. The `delete` operation returns a mutation identifier unique for that operation, and does not assert that the vector is removed in the index. It typically takes a few seconds for vectors to be removed from the Vectorize index. :: ### `describe()` Retrieves the configuration of a given index directly, including its configured `dimensions` and distance `metric`. ```ts const details = await index.describe(); console.log(details); /* { dimensions: 768, vectorCount: 104, processedUpToDatetime: '2025-02-05T18:07:15.627Z', processedUpToMutation: '7fd632ef-eb54-4788-b788-3cc003f7311a' } */ ``` #### Return Returns [`VectorizeIndexDetails`](https://hub.nuxt.com/#vectorizeindexdetails). ## Vectors A vector represents the vector embedding output from a machine learning model. ### Vector Object The Vector Object contains the id, vector embedding value, and metadata for a given vector. ::field-group :::field{required name="id" required="true" type="string"} A unique `string` identifying the vector in the index. This should map back to the ID of the document, object or database identifier that the vector values were generated from. ::: :::field --- required: true name: values type: number[] | Float32Array | Float64Array --- An array of `number`, `Float32Array`, or `Float64Array` as the vector embedding itself. This must be a dense array, and the length of this array must match the `dimensions` configured on the index. ::: :::collapsible{close-text="Hide optional" open-text="Show optional"} ::::field{name="namespace" type="object"} A partition key within a index. Operations are performed per-namespace, so this can be used to create isolated segments within a larger index. (Optional) :::: ::::field --- name: metadata type: Record --- An optional set of key-value pairs that can be used to store additional metadata alongside a vector. :::: ::: :: ```ts const vectorExample = { id: "12345", values: [32.4, 6.55, 11.2, 10.3, 87.9], namespace: "images", metadata: { key: "value", hello: "world", url: "r2://bucket/some/object.json", }, }; ``` ### Dimensions Dimensions are determined from the output size of the machine learning (ML) model used to generate them, and are a function of how the model encodes and describes features into a vector embedding. The number of output dimensions can determine vector search accuracy, search performance (latency), and the overall size of the index. Smaller output dimensions can be faster to search across, which can be useful for user-facing applications. Larger output dimensions can provide more accurate search, especially over larger datasets and/or datasets with substantially similar inputs. The number of dimensions an index is created for cannot change after an index is created. The following table highlights some example embeddings models and their output dimensions: | Model / Embeddings API | Output dimensions | Use-case | | ---------------------------------------- | ----------------- | -------------------------- | | Workers AI - `@cf/baai/bge-base-en-v1.5` | 768 | Text | | OpenAI - `ada-002` | 1536 | Text | | Cohere - `embed-multilingual-v2.0` | 768 | Text | | Google Cloud - `multimodalembedding` | 1408 | Multi-modal (text, images) | ::note Refer to the [Workers AI documentation](https://developers.cloudflare.com/workers-ai/models/#text-embeddings){rel="nofollow"} to learn about its built-in embedding models. :: If you are using [NuxtHub AI](https://hub.nuxt.com/docs/features/ai) to generate vector embeddings, the text-embedding models currently available are: - [`@cf/baai/bge-base-en-v1.5`](https://developers.cloudflare.com/workers-ai/models/bge-base-en-v1.5){rel="nofollow"} - 768 dimensions - [`@cf/baai/bge-large-en-v1.5`](https://developers.cloudflare.com/workers-ai/models/bge-large-en-v1.5){rel="nofollow"} - 1024 dimensions - [`@cf/baai/bge-small-en-v1.5`](https://developers.cloudflare.com/workers-ai/models/bge-small-en-v1.5){rel="nofollow"} - 384 dimensions ### Distance metrics Distance metrics are functions that determine how close vectors are from each other. Vectorize indexes support the following distance metrics: | Metric | Details | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cosine` | Distance is measured between `-1` (most dissimilar) to `1` (identical). `0` denotes an orthogonal vector. | | `euclidean` | Euclidean (L2) distance. `0` denotes identical vectors. The larger the positive number, the further the vectors are apart. | | `dot-product` | Negative dot product. Larger negative values *or* smaller positive values denote more similar vectors. A score of `-1000` is more similar than `-500`, and a score of `15` more similar than `50`. | Determining the similarity between vectors can be subjective and is determined by how well the machine-learning model can represent features in the resulting vector embeddings. For example, a score of `0.8511` when using a `cosine` metric means that two vectors are close in distance, but whether data they represent is *similar* is a function of how well the model is able to represent the original content. Distance metrics cannot be changed after an index is created. #### Choosing a distance metric Choosing a distance metric depends on the vector embedding model used to generate the vectors. If possible, it's best to use the same distance metric as the model generating the vectors. While it's always good to test the results from different distance metrics, each metric uses different properties of the vectors to determine their similarity. If your embeddings contain data that is related to quantifiable values, such as prices, ratings, or other numerical values, you may want to use a metric that considers both magnitude and direction. | Metric | Vector Properties Considered | | ------------- | ---------------------------- | | `cosine` | Only direction | | `euclidean` | Magnitude and direction | | `dot-product` | Magnitude and direction | ::callout Learn more about [Vector Distance Metrics](https://www.pinecone.io/learn/vector-similarity/){rel="nofollow"}. :: ### Supported vector formats Vectorize supports vectors in three formats: - An array of floating point numbers (converted into a JavaScript `number[]` array). - A [Float32Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array){rel="nofollow"} - A [Float64Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float64Array){rel="nofollow"} In most cases, a `number[]` array is the easiest when dealing with other APIs, and is the return type of most machine-learning APIs. ## Metadata Metadata is an optional set of key-value pairs that can be attached to a vector on [insert](https://hub.nuxt.com/#insert) or [upsert](https://hub.nuxt.com/#upsert), and allows you to embed or co-locate data about the vector itself. Vectorize allows you to add up to 10KiB of metadata per vector into your index. ::callout Metadata keys cannot be empty, contain the dot character (`.`), contain the double-quote character (`"`), or start with the dollar character (`$`). :: Metadata can be used to store: - The object storage key, database UUID or other identifier to look up the content the vector embedding represents. - The raw content (up to the [metadata limits](https://developers.cloudflare.com/vectorize/platform/limits/){rel="nofollow"}), which can allow you to skip additional lookups for smaller content. - Dates, timestamps, or other metadata that describes when the vector embedding was generated or how it was generated. For example, a vector embedding representing an image could include the path to the [blob](https://hub.nuxt.com/docs/features/blob) it was generated from, the format, and a category lookup: ```ts { id: '1', values: [32.4, 74.1, 3.2, ...], metadata: { path: 'r2://bucket-name/path/to/image.png', format: 'png', category: 'profile_image' } } ``` ### Metadata filtering When querying an index, you can filter using [metadata](https://hub.nuxt.com/#metadata). Query results will only include vectors that match the `filter` criteria, meaning that `filter` is applied first, and the `topK` results are taken from the filtered set. Metadata filtering allows you to query specific subsets of your data. You can filter by specific customer IDs, tenant, product category or any other metadata index you have configured. ### Create metadata indexes In order to filter by a specific metadata property, it must be defined in the `metadataIndexes`. Metadata indexes are configured within the `metadataIndexes` object within your index configuration in `nuxt.config.ts`. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { vectorize: { tutorial: { dimensions: 32, metric: "cosine", metadataIndexes: { // : "string" | "number" | "boolean" url: "string", "nested.property": "boolean" } } } } }) ``` Metadata indexes can be added at any time, but they will only contain vectors that have been inserted/upserted after the index has been created. Upserting vectors after an index is created will index it as expected. #### Metadata Index Tips - Supported metadata index property types are `string`, `number` and `boolean` types. - Nested properties can be defined using `.` (dot) like `nested.property`. - For metadata indexes of type `number`, the indexed number precision is that of float64. - For metadata indexes of type `string`, each vector indexes the first 64B of the string data truncated on UTF-8 character boundaries to the longest well-formed UTF-8 substring within that limit, so vectors are filterable on the first 64B of their value for each indexed property. ::callout Vectorize currently supports a maximum of 10 metadata indexes per Vectorize index. Learn more at {rel="nofollow"}. :: ### Supported operations You can use metadata filters with the `query()` method by passing a `filter` option. ```ts const matches = await index.query(queryVector, { filter: { "url": "https://hub.nuxt.com", "nested.property": { "$ne": true } } }); ``` The `filter` property follows rules similar to those of the `metadata` property used when inserting metadata. - `filter` must be non-empty object whose compact JSON representation must be less than 2048 bytes. - `filter` object keys cannot be empty, contain `"` or `.` (dot is reserved for nesting), start with `$`, or be longer than 512 characters. - `filter` object non-nested values can be `string`, `number`, `boolean`, or `null` values. The metadata filter supports the following operators: | Operator | Description | | -------- | ------------------------ | | `$eq` | Equals | | `$ne` | Not equals | | '$in' | In | | '$nin' | Not in | | '$lt' | Less than | | '$lte' | Less than or equal to | | '$gt' | Greater than | | '$gte' | Greater than or equal to | #### Valid `filter` examples ##### Implicit `$eq` operator ```ts const matches = await index.query(queryVector, { filter: { { "streaming_platform": "netflix" } } }); ``` ##### Explicit operator ```ts const matches = await index.query(queryVector, { filter: { { "someKey": { "$ne": true } } } }); ``` ##### Nested Properties ```ts const matches = await index.query(queryVector, { filter: { { "pandas.nice": 42 } } }); // looks for { "pandas": { "nice": 42 } } ``` ##### Implicit logical `AND` with multiple keys ```ts const matches = await index.query(queryVector, { filter: { "pandas.nice": 42, "someKey": { "$ne": true } } }); ``` ### Limits You can store up to 10KiB of metadata per vector, and create up to 10 metadata indexes per Vectorize index. For metadata indexes of type `number`, the indexed number precision is that of float64. For metadata indexes of type `string`, each vector indexes the first 64B of the string data truncated on UTF-8 character boundaries to the longest well-formed UTF-8 substring within that limit, so vectors are filterable on the first 64B of their value for each indexed property. See [Vectorize Limits](https://developers.cloudflare.com/vectorize/platform/limits/){rel="nofollow"} for a complete list of limits. ## Namespaces Namespaces provide a way to segment the vectors within your index. For example, by customer, merchant or store ID. To associate vectors with a namespace, you can optionally provide a `namespace: string` value when performing an insert or upsert operation. When querying, you can pass the namespace to search within as an optional parameter to your query. A namespace can be up to 64 characters (bytes) in length and you can have up to 1,000 namespaces per index. Refer to the [Limits](https://developers.cloudflare.com/vectorize/platform/limits/){rel="nofollow"} documentation for more details. When a namespace is specified in a query operation, only vectors within that namespace are used for the search. Namespace filtering is applied before vector search, not after. #### Insert vectors with a namespace ```ts // Mock vectors // Vectors from a machine-learning model are typically ~100 to 1536 dimensions // wide (or wider still). const sampleVectors: Array = [ { id: "1", values: [32.4, 74.1, 3.2, ...], namespace: "text", }, { id: "2", values: [15.1, 19.2, 15.8, ...], namespace: "images", }, { id: "3", values: [0.16, 1.2, 3.8, ...], namespace: "pdfs", }, ]; // Insert your vectors, returning a count of the vectors inserted and their vector IDs. const inserted = await index.insert(sampleVectors); ``` #### Query vectors within a namespace ```ts // Your queryVector will be searched against vectors within the namespace (only) const matches = await index.query(queryVector, { namespace: "images", }); ``` ### Namespace versus metadata filtering Both [namespaces](https://developers.cloudflare.com/vectorize/best-practices/insert-vectors/#namespaces){rel="nofollow"} and metadata filtering narrow the vector search space for a query. Consider the following when evaluating both filter types: - A namespace filter is applied before metadata filter(s) - A vector can only be part of a single namespace, while vector metadata can contain multiple key-value pairs - A namespace must be a string, while metadata values support different types (`string`, `boolean`, or `number`) ## Types ### `VectorizeVector` See [vector object](https://hub.nuxt.com/#vector-object). ```ts interface VectorizeVector { /** The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents. */ id: string; /** The vector values */ values: VectorFloatArray | number[]; /** The namespace this vector belongs to. */ namespace?: string; /** Metadata associated with the vector. Includes the values of other fields and potentially additional details. */ metadata?: Record; } ``` ### `VectorizeMatches` A set of matching [VectorizeMatch](https://hub.nuxt.com/#vectorizematch) for a particular query. ```ts interface VectorizeMatches { matches: VectorizeMatch[]; count: number; } ``` ### `VectorizeMatch` Represents a matched vector for a query along with its score and (if specified) the matching vector information. ```ts type VectorizeMatch = Pick, "values"> & Omit & { /** The score or rank for similarity, when returned as a result */ score: number; }; ``` ### `VectorizeAsyncMutation` Result type indicating a mutation on the Vectorize Index. Mutations are processed asynchronously in the background and the `mutationId` is the unique identifier for the operation. ```ts interface VectorizeAsyncMutation { /** The unique identifier for the async mutation operation containing the changeset. */ mutationId: string; } ``` ### `VectorizeIndexInfo` Metadata about an existing index. ```ts interface VectorizeIndexInfo { /** The number of records containing vectors within the index. */ vectorsCount: number; /** Number of dimensions the index has been configured for. */ dimensions: number; /** ISO 8601 datetime of the last processed mutation on in the index. All changes before this mutation will be reflected in the index state. */ processedUpToDatetime: number; /** UUIDv4 of the last mutation processed by the index. All changes before this mutation will be reflected in the index state. */ processedUpToMutation: number; } ``` ## Examples ### Vector search In this example: 1. An embeddings vector is generated from the search query. 2. The Vectorize index is queried with the embeddings vector. 3. Then the original source data is retrieved by querying the database for the IDs returned by Vectorize. Learn more at {rel="nofollow"} ```ts [server/api/search.get.ts] import { z } from "zod"; interface EmbeddingResponse { shape: number[]; data: number[][]; } const Query = z.object({ query: z.string().min(1).max(256), limit: z.coerce.number().int().min(1).max(20).default(10), }); export default defineEventHandler(async (event) => { const { query, limit } = await getValidatedQuery(event, Query.parse); // 1. generate embeddings for search query const embeddings: EmbeddingResponse = await hubAI().run("@cf/baai/bge-base-en-v1.5", { text: [query] }); const vectors = embeddings.data[0]; // 2. query vectorize to find similar results const vectorize: VectorizeIndex = hubVectorize('jobs'); const { matches } = await vectorize.query(vectors, { topK: limit }); // 3. get details for matching items const jobMatches = await useDrizzle().query.jobs.findMany({ where: (jobs, { inArray }) => inArray(jobs.id, matches.map((match) => match.id)), with: { department: true, subDepartment: true, }, }); // 4. add score to matches const jobMatchesWithScore = jobMatches.map((job) => { const match = matches.find((match) => match.id === job.id); return { ...job, score: match!.score }; }); // 5. sort by score return jobMatchesWithScore.sort((a, b) => b.score - a.score); }); ``` ### Bulk generation and import This example bulk generates vectors using a text embeddings AI model for all data within a database table, using [Nitro tasks](https://nitro.unjs.io/guide/tasks){rel="nofollow"}. You can run the task via Nuxt DevTools. ```ts [server/tasks/generate-embeddings.ts] import { jobs } from "../database/schema"; import { asc, count } from "drizzle-orm"; export default defineTask({ meta: { name: "vectorize:seed", description: "Generate text embeddings vectors", }, async run() { console.log("Running Vectorize seed task..."); const jobCount = (await useDrizzle().select({ count: count() }).from(tables.jobs))[0].count; // process in chunks of 100 as that's the maximum supported by workers ai const INCREMENT_AMOUNT = 100; const totalBatches = Math.ceil(jobCount / INCREMENT_AMOUNT); console.log(`Total items: ${jobCount} (${totalBatches} batches)`); for (let i = 0; i < jobCount; i += INCREMENT_AMOUNT) { console.log(`⏳ Processing items ${i} - ${i + INCREMENT_AMOUNT}...`); const jobsChunk = await useDrizzle() .select() .from(tables.jobs) .orderBy(asc(jobs.id)) .limit(INCREMENT_AMOUNT) .offset(i); // generate embeddings for job titles const ai = hubAi(); const embeddings = await ai.run( "@cf/baai/bge-base-en-v1.5", { text: jobsChunk.map((job) => job.jobTitle) }, { gateway: { id: "new-role" } }, ); const vectors = embeddings.data; const formattedEmbeddings = jobsChunk.map(({ id, ...metadata }, index) => ({ id, metadata: { ...metadata }, values: vectors[index], })); // save vector embeddings to index const index = hubVectorize('jobs'); await index.upsert(formattedEmbeddings); console.log(`✅ Processed items ${i} - ${i + INCREMENT_AMOUNT}...`); } console.log("Vectorize seed task completed!"); return { result: "success" }; }, }); ``` ## Limits | Feature | Current Limit | | ------------------------------------------------------------- | --------------- | | Indexes per account | 100 indexes | | Maximum dimensions per vector | 1536 dimensions | | Maximum vector ID length | 51 bytes | | Metadata per vector | 10KiB | | Maximum returned results (`topK`) with values or metadata | 20 | | Maximum returned results (`topK`) without values and metadata | 100 | | Maximum upsert batch size (per batch) | 1000 | | Maximum index name length | 64 bytes | | Maximum vectors per index | 5,000,000 | | Maximum namespaces per index | 1000 namespaces | | Maximum namespace name length | 64 bytes | | Maximum vectors upload size | 100 MB | | Maximum metadata indexes per Vectorize index | 10 | | Maximum indexed data per metadata index per vector | 64 bytes | Learn more about [Cloudflare Vectorize limits](https://developers.cloudflare.com/vectorize/platform/limits/){rel="nofollow"}. ## Pricing ::pricing-table{:tabs='["Vectorize"]'} :: # Hooks ## `onHubReady()` Use `onHubReady()` to ensure the execution of some code once NuxtHub environment bindings are set up and database migrations are applied. ::note `onHubReady()` is a shortcut using the [`hubHooks`](https://hub.nuxt.com/#hubhooks) object under the hood that listens to the `bindings:ready` and `database:migrations:done` events. :: ```ts [server/plugins/migrations.ts] export default defineNitroPlugin(() => { // Only run in development if (import.meta.dev) { onHubReady(async () => { console.log('NuxtHub is ready! 🚀') }) } }) ``` ## `hubHooks` The `hubHooks` object is a collection of hooks that can be used to stay synced with NuxtHub. ### Signature ```ts [Signature] export interface HubHooks { 'bindings:ready': () => any | void 'database:migrations:done': () => any | void } ``` ### Usage You can use the `hubHooks` object to listen to `HubHooks` events in your server plugins: ```ts [server/plugins/hub.ts] export default defineNitroPlugin(() => { hubHooks.hook('bindings:ready', () => { console.log('NuxtHub bindings are ready!') const db = hubDatabase() }) // Only run in development and if the database is enabled if (import.meta.dev) { hubHooks.hook('database:migrations:done', () => { console.log('Database migrations are done!') }) } }) ``` ::note Note that `hubHooks` is a [hookable](https://hookable.unjs.io){rel="nofollow"} instance. :: # Drizzle ORM ::callout --- external: "" icon: i-simple-icons-drizzle to: https://orm.drizzle.team --- Learn more about **Drizzle ORM**. :: ## Setup To enhance your Developer Experience with the database, we can create a `useDrizzle()` server composable with few steps. ### Install Drizzle 1. Install the `drizzle-orm` package to your project: ::code-group ```bash [pnpm] pnpm add drizzle-orm ``` ```bash [yarn] yarn add drizzle-orm ``` ```bash [npm] npm install drizzle-orm ``` ```bash [bun] bun add drizzle-orm ``` :: 2. Install `drizzle-kit` development dependency to your project: ::code-group ```bash [pnpm] pnpm add -D drizzle-kit ``` ```bash [yarn] yarn add --dev drizzle-kit ``` ```bash [npm] npm install --save-dev drizzle-kit ``` ```bash [bun] bun add --dev drizzle-kit ``` :: ### `drizzle.config.ts` Add a `drizzle.config.ts` file to your project: ```ts [drizzle.config.ts] import { defineConfig } from 'drizzle-kit' export default defineConfig({ dialect: 'sqlite', schema: './server/database/schema.ts', out: './server/database/migrations' }) ``` ### Database Schema ```ts [server/database/schema.ts] import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core' export const users = sqliteTable('users', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), email: text('email').notNull().unique(), password: text('password').notNull(), avatar: text('avatar').notNull(), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), }) ``` ### `npm run db:generate` Let's add a `db:generate` script to the `package.json`: ```json [package.json] { "scripts": { "db:generate": "drizzle-kit generate" } } ``` When running the `npm run db:generate` command, `drizzle-kit` will generate the migrations based on `server/database/schema.ts` and save them in the `server/database/migrations` directory. ### Migrations Migrations created with `npm run db:generate` are automatically applied during deployment, preview and when starting the development server. ::note{to="https://hub.nuxt.com/docs/features/database#database-migrations"} Learn more about migrations. :: ### `useDrizzle()` Lastly, we can create a `useDrizzle()` server composable to interact with the database: ```ts [server/utils/drizzle.ts] import { drizzle } from 'drizzle-orm/d1' export { sql, eq, and, or } from 'drizzle-orm' import * as schema from '../database/schema' export const tables = schema export function useDrizzle() { return drizzle(hubDatabase(), { schema }) } export type User = typeof schema.users.$inferSelect ``` We are exporting the `tables` object and the `useDrizzle` function to be used in our API handlers without having to import them (Nuxt does it for us as long as it's exported from a `server/utils/` file). This allows you to conveniently reference your tables and interact directly with the [Drizzle API](https://orm.drizzle.team/docs/overview){rel="nofollow"}. ::callout Note that we are also exporting the `User` type, which is inferred from the `users` table. This is useful for type-checking the results of your queries. :: ::callout We also export the `sql`, `eq`, `and`, and `or` functions from `drizzle-orm` to be used in our queries. :: ### Seed the database (Optional) You can add a server task to populate your database with initial data. This uses [Nitro Tasks](https://nitro.unjs.io/guide/tasks){rel="nofollow"}, which is currently an experimental feature. 1. Update your nuxt.config.js: ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { experimental: { tasks: true } } }) ``` 2. Create a new file containing the task: ```ts [server/tasks/seed.ts] export default defineTask({ meta: { name: 'db:seed', description: 'Run database seed task' }, async run() { console.log('Running DB seed task...') const users = [ { name: 'John Doe', email: 'john@example.com', password: 'password123', avatar: 'https://example.com/avatar/john.png', createdAt: new Date() }, { name: 'Jane Doe', email: 'jane@example.com', password: 'password123', avatar: 'https://example.com/avatar/jane.png', createdAt: new Date() } ] await useDrizzle().insert(tables.users).values(users) return { result: 'success' } } }) ``` To run the seed task, start your dev server and open the Nuxt DevTools. Go to *Tasks* and you will see the `db:seed` task ready to run. This will add the seed data to your database and give you the first users to work with. ## Usage ### Select ```ts [server/api/todos/index.get.ts] export default eventHandler(async () => { const todos = await useDrizzle().select().from(tables.todos).all() return todos }) ``` ### Insert ```ts [server/api/todos/index.post.ts] export default eventHandler(async (event) => { const { title } = await readBody(event) const todo = await useDrizzle().insert(tables.todos).values({ title, createdAt: new Date() }).returning().get() return todo }) ``` ### Update ```ts [server/api/todos/[id\\].patch.ts] export default eventHandler(async (event) => { const { id } = getRouterParams(event) const { completed } = await readBody(event) const todo = await useDrizzle().update(tables.todos).set({ completed }).where(eq(tables.todos.id, Number(id))).returning().get() return todo }) ``` ### Delete ```ts [server/api/todos/[id\\].delete.ts] export default eventHandler(async (event) => { const { id } = getRouterParams(event) const deletedTodo = await useDrizzle().delete(tables.todos).where(and( eq(tables.todos.id, Number(id)) )).returning().get() if (!deletedTodo) { throw createError({ statusCode: 404, message: 'Todo not found' }) } return deletedTodo }) ``` # Pre-rendering When using NuxtHub, you need to build your application with the [`nuxt build`](https://nuxt.com/docs/api/commands/build){rel="nofollow"} in order to keep your server when deploying your application. However, you can also pre-render your pages at build time to improve performance and avoid CPU usage on the server. ::tip{to="https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering"} This is possible thanks to **Nuxt's Hybrid Rendering** to allow different caching rules per route. :: ## Route Rules ### Globally You can define route rules in your `nuxt.config.ts` to specify how each route should be rendered: ```ts [nuxt.config.ts] export default defineNuxtConfig({ routeRules: { '/': { prerender: true } } }) ``` When running `nuxt build`, Nuxt will pre-render the `/` route and save the `index.html` file in the output directory. ::callout{icon="i-lucide-rocket"} When deploying with NuxtHub on Cloudflare Pages, it will serve the pre-rendered HTML file directly from the edge for maximum performance and avoid CPU usage on the server. :: ### Page Level It is also possible to define route rules at the page level using the [`defineRouteRules`](https://nuxt.com/docs/api/utils/define-route-rules){rel="nofollow"} util in the page component: ```vue [pages/index.vue] ``` This is equivalent of our example above in the `nuxt.config.ts` file. **Notes:** - A rule defined in `~/pages/foo/bar.vue` will be applied to `/foo/bar` requests. - A rule in `~/pages/foo/[id].vue` will be applied to `/foo/**` requests. ::important --- to: https://nuxt.com/docs/guide/going-further/experimental-features#inlinerouterules --- As this is an experimental feature, you need to enable it in your `nuxt.config.ts` files with `experimental: { inlineRouteRules: true }`. :: ## Dynamic Pre-rendering In some cases, you may want to Nuxt to pre-render other pages when pre-rendering a specific page. This is useful when pre-rendering all the blog posts when pre-rendering the blog index page. To achieve this, we can use the [`prerenderRoutes`](https://nuxt.com/docs/api/utils/prerender-routes){rel="nofollow"} util in the page component: ```vue [pages/blog/index.vue] ``` It is important to tell Nuxt to pre-render the `/blog` using the `defineRouteRules`, we can also do it globally in the `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ routeRules: { // If not using `defineRouteRules` in the page component '/blog': { prerender: true } } }) ``` ### Using a Nuxt Module You can also use a [local Nuxt module](https://nuxt.com/docs/guide/going-further/modules){rel="nofollow"} to pre-render dynamic pages, which is particularly useful if you don't have a single root page (such as `/blog`) but still need to pre-render specific routes, such as `/page-1`, `/parent/page-2`, and so on. ```ts [modules/prerender-routes.ts] import { defineNuxtModule, addPrerenderRoutes } from '@nuxt/kit' export default defineNuxtModule({ meta: { name: 'nuxt-prerender-routes', }, async setup() { const pages = await getDynamicPages() addPrerenderRoutes(pages) }, }) async function getDynamicPages(): string[] { // Replace this function with the logic for retrieving the slugs for your pages. return ['/page-1', '/parent/page-2'] } ``` ## Pre-render All Pages To have the same behavior as [`nuxt generate`](https://nuxt.com/docs/api/commands/generate){rel="nofollow"} while keeping the server part, you can pre-render all pages by configuring the `nitro.prerender` option in the `nuxt.config.ts`: ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { prerender: { // Pre-render the homepage routes: ['/'], // Then crawl all the links on the page crawlLinks: true } } }) ``` When running `nuxt build`, Nuxt will pre-render all pages and save the `index.html` file in the `dist/` directory. ::tip{target="_blank" to="https://nuxt.com/docs/getting-started/prerendering"} Learn more about Nuxt prerendering. :: ### Cloudflare 100 routes limit NuxtHub will generate a [`dist/_routes.json`](https://developers.cloudflare.com/pages/functions/routing/#create-a-_routesjson-file){rel="nofollow"} for Cloudflare Pages, but it has a limit of 100 excluded routes (used for static assets). As each pre-rendered page will be added to the exclude list, we recommend to add your known pre-rendered pattern in the `nitro.cloudflare.pages.routes.exclude` option: ```ts [nuxt.config.ts] export default defineNuxtConfig({ // ... nitro: { cloudflare: { pages: { routes: { exclude: [ // we know that all docs and blog pages are pre-rendered '/docs/*', '/blog/*' ] } } } } }) ``` ## Caveats If you are using authentication in your application such as [`nuxt-auth-utils`](https://github.com/Atinux/nuxt-auth-utils){rel="nofollow"}, you need to remember that the authentication state will not be available during the pre-rendering process. For example, if you have a header component that either display the logged in user or a login button, you need to wrap the logic inside the [``](https://github.com/atinux/nuxt-auth-utils?tab=readme-ov-file#authstate-component){rel="nofollow"} component to display a placeholder while the page is being pre-rendered. ```vue [components/AppHeader.vue] ``` # Debugging Some Nuxt modules or librairies you use in your project may not be compatible with the edge runtime yet. When this happens, your Nuxt server cannot even start in production, producing a 500 error. The current solution to debug is the following. 1. Disable Nitro minification in your `nuxt.config.ts` ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxthub/core'], nitro: { minify: false } }) ``` 2. Build your application for production with the Cloudflare Pages preset ```bash [Terminal] npx nuxt build ``` 3. Preview the production server in the Worker environment ```bash [Terminal] npx nuxthub preview ``` 4. Open the browser by pressing the **b** shortcut (most of the time it starts on {rel="nofollow"}) 5. Go back to your terminal and look at the error and stack trace, then open the file (should be in the `dist/` directory) to know the line causing the error. ::note Most of the time, you will need to find scroll above to know what library is causing the error. :: 6. [Open an issue](https://github.com/nuxt-hub/core){rel="nofollow"} in our repository so we can help making more libraries Edge compatible. # PostgreSQL Database ## Pre-requisites Cloudflare does not host PostgreSQL databases, you need to setup your own PostgreSQL database. ::note --- icon: i-logos-postgresql target: _blank to: https://www.postgresql.org/support/professional_hosting/ --- See a list of professional PostgreSQL hosting providers. :: If you prefer to use Cloudflare services, you can use Cloudflare D1 which is built on SQLite, see our [Database](https://hub.nuxt.com/docs/features/database) section. ## Setup 1. Make sure to use the `@nuxthub/core` module, see the [installation section](https://hub.nuxt.com/docs/getting-started/installation#add-to-a-nuxt-project) for instructions. ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxthub/core'], }); ``` ::note The module ensures that you can connect to your PostgreSQL database using [Cloudflare TCP Sockets](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/.){rel="nofollow"} :: 2. Install the [`postgres`](https://www.npmjs.com/package/postgres){rel="nofollow"} NPM package in your project. ```bash npx nypm i postgres ``` ::tip{icon="i-lucide-rocket"} That's it, you can now use the `postgres` package to connect to your PostgreSQL database. :: ::warning Please note that in order to use [`pg`](https://www.npmjs.com/package/pg){rel="nofollow"} a minimum version of `8.13.0` is required, alongside wrangler `3.78.7` or later and `compatibility_date = "2024-09-23"`. :: ## Usage We can add our PostgreSQL database connection in our `.env` file. ```bash [.env] NUXT_POSTGRES_URL=postgresql://user:password@localhost:5432/database ``` Then, we can create a `usePostgres()` server util to connect to our database in our API route. ```ts [server/utils/postgres.ts] import postgres from 'postgres' export function usePostgres () { if (!process.env.NUXT_POSTGRES_URL) { throw createError('Missing `NUXT_POSTGRES_URL` environment variable') } return postgres(process.env.NUXT_POSTGRES_URL as string, { ssl: 'require' }) } ``` We can now use the `usePostgres()` function to connect to our database in our API route. ```ts [server/api/db.ts] export default eventHandler(async (event) => { const sql = usePostgres() const products = await sql`SELECT * FROM products` // Ensure the database connection is closed after the request is processed event.waitUntil(sql.end()) return products }) ``` ::tip You may notice that we don't import `usePostgres()`. This is because Nuxt auto-imports the exported variables & functions from `server/utils/*.ts` when used. :: ## Hyperdrive [Hyperdrive](https://developers.cloudflare.com/hyperdrive/){rel="nofollow"} is a Cloudflare service that accelerates queries you make to existing databases, making it faster to access your data from across the globe. By maintaining a connection pool to your database within Cloudflare’s network, Hyperdrive reduces seven round-trips to your database before you can even send a query: the TCP handshake (1x), TLS negotiation (3x), and database authentication (3x). ::important --- target: _blank to: https://developers.cloudflare.com/hyperdrive/platform/pricing/ --- Hyperdrive is only available on the Workers Paid plan ($5/month), **learn more**. :: To use Hyperdrive in your Nuxt application: 1. [Create a Hyperdrive configuration](https://dash.cloudflare.com/?to=/\:account/workers/hyperdrive/create){rel="nofollow"} 2. Add your Hyperdrive ID in your `nuxt.config.ts` file ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxthub/core'], hub: { bindings: { hyperdrive: { // : POSTGRES: 'your-hyperdrive-id' } } } }) ``` 3. Update our `usePostgres()` function to use the `POSTGRES` binding when available. ```ts [server/utils/postgres.ts] import type { Hyperdrive } from '@cloudflare/workers-types' import postgres from 'postgres' export function usePostgres() { // @ts-expect-error globalThis.__env__ is not defined const hyperdrive = process.env.POSTGRES || globalThis.__env__?.POSTGRES || globalThis.POSTGRES as Hyperdrive | undefined const dbUrl = hyperdrive?.connectionString || process.env.NUXT_POSTGRES_URL if (!dbUrl) { throw createError('Missing `POSTGRES` hyperdrive binding or `NUXT_POSTGRES_URL` env variable') } return postgres(dbUrl, { ssl: !hyperdrive ? 'require' : undefined }) } ``` ::warning Hyperdrive is currently not available in development mode at the moment. We are working on a solution to make it work in development mode and remote storage with an upcoming `hubHyperdrive()`. :: 4. [Deploy your application](https://hub.nuxt.com/docs/getting-started/deploy) with the NuxtHub CLI or Admin to make sure the Hyperdrive bindings are set. ::tip{icon="i-lucide-rocket"} You can now access your PostgreSQL database from anywhere in the world at maximum speed. :: # Cloudflare Access Integration [Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/){rel="nofollow"} allows you to secure your web applications by restricting who can reach your application by applying configured identity-aware Access policies. Cloudflare Access is part of [Cloudflare's Zero Trust](https://www.cloudflare.com/plans/zero-trust-services/){rel="nofollow"} offerings. NuxtHub fully supports Cloudflare Access across the NuxtHub admin, module and CLI. ## Setup Cloudflare Access These steps covers setting up Cloudflare Access for a deployed NuxtHub project. ::important{to="https://hub.nuxt.com/#nuxtdev-subdomain-with-cloudflare-access"} When using Cloudflare Access with NuxtHub, the nuxt.dev subdomain is unavailable due to a Cloudflare limitation. [Learn more](https://hub.nuxt.com/#nuxtdev-subdomain-with-cloudflare-access). :: 1. Create a Cloudflare Access [service token](https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/){rel="nofollow"} in the [Cloudflare Zero Trust dashboard](https://one.dash.cloudflare.com/){rel="nofollow"} 1. In [Zero Trust](https://one.dash.cloudflare.com/){rel="nofollow"}, go to Access → Service Auth → Service Tokens 2. Select Create Service Token 3. Name the service token. For example, the NuxtHub project's name - The name allows you to easily identify events related to the token in the logs 4. Choose a Service Token Duration. This sets the expiration date for the token 5. Select Generate token. You will see the generated Client ID and Client Secret for the service token :warning[This is the only time Cloudflare Access will display the Client Secret. If you lose the Client Secret, you will need to rotate the service token] 2. Configure the Cloudflare Access integration within [NuxtHub admin](https://admin.hub.nuxt.com/){rel="nofollow"} 1. In the [NuxtHub admin](https://admin.hub.nuxt.com/){rel="nofollow"}, go to Projects → Settings → General → Cloudflare Access 2. Provide the Client ID and Client Secret generated in the previous step 3. Enable Cloudflare Access on the Pages project 1. On the [Cloudflare dashboard](https://dash.cloudflare.com/login?to=/\:account/workers-and-pages){rel="nofollow"} → Workers & Pages → Your Pages project 2. Go to Settings → General → Access Policy 3. Select Enable to create a Cloudflare Access application. :note[The default policy covers the preview environments on the `pages.dev` subdomain, adds an allow policy with all members of the account, and uses the email one-time-pin IdP.] 4. Add an Access policy to permit the service token 1. In [Zero Trust](https://one.dash.cloudflare.com/){rel="nofollow"}, go to Access → Applications and select the application 2. Create a new policy with the name "NuxtHub" and the action Service Auth 3. Enable the 401 Response boolean 4. Within Configure rules, set Selector to Service Token and the value to the service token created earlier 5. Save the policy 5. Optionally edit your Allow Access policy :callout[Learn more about Cloudflare Access policies on Cloudflare's documentation.]{to="https://developers.cloudflare.com/cloudflare-one/policies/access/#allow"} 6. Optionally add additional domains to your Access application 1. In [Zero Trust](https://one.dash.cloudflare.com/){rel="nofollow"}, go to Access → Applications and select the application 2. From the header, select Overview 3. Add additional application domains, such as the production domain, or any custom domains assigned to the project ### Importing Pages projects We plan to directly support importing existing Pages projects that are protected with Cloudflare Access enabled in the future. Currently, you will need to temporarily create an Access application which sets a Bypass policy for Everyone on the project's default pages.dev domain and the path `/api/_hub/manifest`. ## Service token expiry and rotation ### Service token expiry When a service token is expired, it can be rotated from the Cloudflare dashboard. 1. In [Zero Trust](https://one.dash.cloudflare.com/){rel="nofollow"}, go to Access → Service Auth → Service Tokens 2. Click `...` on the expired token 3. Select Rotate ::tip The duration of active service tokens can be extended by refreshing it from the Zero Trust dashboard :: ### Service token rotation If a service token is rotated, the new Client Secret needs to be provided on [NuxtHub admin](https://admin.hub.nuxt.com/){rel="nofollow"}. 1. In the [NuxtHub admin](https://admin.hub.nuxt.com/){rel="nofollow"}, go to Projects → Settings → General → Cloudflare Access 2. Click Disable integration 3. Provide the Client ID and Client Secret generated in the previous step 4. Click Enable integration ## Remote storage These steps will cover using [remote storage](https://hub.nuxt.com/docs/getting-started/remote-storage) with an environment protected by Cloudflare Access. 1. Open `.env` 2. Set the following variables with your service token's Client ID and Client Secret - `NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_ID` - `NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_SECRET` ::note A separate service token can optionally be created only for local development :: ::tip No configuration is required if using [Cloudflare WARP](https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/){rel="nofollow"} with Cloudflare Zero Trust, as WARP handles authenticating with Cloudflare Access :: ## CLI The following environment variables can be passed to the CLI - `NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_ID` - `NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_SECRET` ```bash [Terminal] export NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_ID="" export NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_SECRET="" npx nuxthub database migrations list --preview ``` ## `nuxt.dev` subdomain with Cloudflare Access Due to a technical Cloudflare limitation, when using Cloudflare Access with NuxtHub, the nuxt.dev subdomain is unavailable. If the nuxt.dev subdomain is the primary domain, enabling the Cloudflare Access integration will set the primary domain to the pages.dev subdomain. # Blob Folders We are excited to announce that we have added a new feature to NuxtHub Blobs: **Folders**! ::tip This feature is available on all [NuxtHub plans](https://hub.nuxt.com/pricing). :: It comes with the [v0.6.0](https://github.com/nuxt-hub/core/releases/tag/v0.6.0){rel="nofollow"} release of `@nuxthub/core`. When uploading a blob, you can now specify a folder path with the `prefix`. This will allow you to organize your blobs in a more structured way. ```ts const { put } = hubBlob() await put('my-blob.txt', myFile, { prefix: 'folder/subfolder' }) ``` ::callout{icon="i-lucide-book" to="https://hub.nuxt.com/docs/features/blob"} Learn more about Blob Storage. :: When viewing your blobs in the [NuxtHub admin](https://admin.hub.nuxt.com){rel="nofollow"}, you will now see a folder structure: ![NuxtHub Deployment Details](https://hub.nuxt.com/images/changelog/server-cache.png){height="515" width="915"} ::callout{icon="i-lucide-heart"} Thank you to [Gerben Mulder](https://github.com/Gerbuuun){rel="nofollow"} for suggesting this feature on [nuxt-hub/core#101](https://github.com/nuxt-hub/core/issues/101){rel="nofollow"}. :: # Blob Presigned URLs It is now possible to upload files to your R2 bucket using presigned URLs with zero configuration. ::tip This feature is available on all [NuxtHub plans](https://hub.nuxt.com/pricing) and comes with the [v0.7.32](https://github.com/nuxt-hub/core/releases/tag/v0.7.32){rel="nofollow"} release of `@nuxthub/core`. :: ## Why presigned URLs? By allowing users to upload files to your R2 bucket using presigned URLs, you can: - Reduce the server load - Direct client-to-storage uploads, saving bandwidth and costs - Use a secure and performant way to upload files to your R2 bucket - By-pass the Workers limitation (100MiB max payload size) using regular upload ## How does it work? This is a diagrame of the process of creating a presigned URL and uploading a file to R2: ![NuxtHub presigned URLs to upload files to R2](https://hub.nuxt.com/images/docs/blob-presigned-urls.png){className="rounded" height="515" width="915"} In order to create the presigned URL, we created the [`hubBlob().createCredentials()`](https://hub.nuxt.com/docs/features/blob#createcredentials) method. This method will create temporary access credentials that can be optionally scoped to prefixes or objects in your bucket. ```ts const { accountId, bucketName, accessKeyId, secretAccessKey, sessionToken } = await hubBlob().createCredentials({ permission: 'object-read-write', pathnames: ['only-this-file.png'] }) ``` With these credentials, you can now use the [aws4fetch](https://github.com/mhart/aws4fetch){rel="nofollow"} library to create a presigned URL that can be used to upload a file to R2. ```ts import { AwsClient } from 'aws4fetch' // Create the presigned URL const client = new AwsClient({ accessKeyId, secretAccessKey, sessionToken }) const endpoint = new URL( '/only-this-file.png', `https://${bucketName}.${accountId}.r2.cloudflarestorage.com` ) const { url } = await client.sign(endpoint, { method: 'PUT', aws: { signQuery: true } }) ``` ::callout --- icon: i-lucide-book to: https://hub.nuxt.com/docs/features/blob#create-presigned-urls-to-upload-files-to-r2 --- Checkout the example on how to create a presigned URL with NuxtHub. :: ## Alternative If you don't want to use presigned URLs and want to upload files bigger than 100MiB, you can use the [NuxtHub Multipart Upload](https://hub.nuxt.com/docs/features/blob#handlemultipartuploadd) feature. # Blob Upload Prefix It is now possible to set the prefix before uploading a blob in the NuxtHub admin and in the Nuxt DevTools (NuxtHub Blob tab). :video{controls className="w-full,h-auto" controls="true" poster="https://res.cloudinary.com/nuxt/video/upload/so_0/v1719931201/nuxthub/admin-blob-prefix_akfj3k.jpg"} ::callout{icon="i-lucide-book" to="https://hub.nuxt.com/docs/features/blob"} Learn more about Blob Storage. :: ::tip This feature is available on all [NuxtHub plans](https://hub.nuxt.com/pricing). :: Head over the [NuxtHub admin](https://admin.hub.nuxt.com){rel="nofollow"} to try this feature or locally in the Nuxt DevTools. # Cloudflare Access integration ::tip This feature is available on all [NuxtHub plans](https://hub.nuxt.com/pricing) and comes with the [v0.8.4](https://github.com/nuxt-hub/core/releases/tag/v0.8.4){rel="nofollow"} release of `@nuxthub/core`. :: We now fully support Cloudflare Access across admin, module and CLI. ## What is Cloudflare Access ![Cloudflare Access](https://hub.nuxt.com/images/changelog/cloudflare-access.png){height="515" width="915"} [Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/){rel="nofollow"} allows you to secure your web applications by restricting who can reach your application by applying configured identity-aware Access policies. Cloudflare Access is part of [Cloudflare's Zero Trust](https://www.cloudflare.com/plans/zero-trust-services/){rel="nofollow"} offerings. ## What does this mean for NuxtHub This enables you to create secure internal web applications on NuxtHub, without compromising on features like NuxtHub admin for management and remote storage during development. ::callout --- icon: i-lucide-book to: https://hub.nuxt.com/docs/recipes/cloudflare-access --- Learn more about enabling Cloudflare Access with NuxtHub. :: # Automatic Database Migrations ::tip This feature is available on both [free and pro plans](https://hub.nuxt.com/pricing) starting with [`@nuxthub/core >= v0.8.0`](https://github.com/nuxt-hub/core/releases){rel="nofollow"}. :: We're excited to introduce automatic [database migrations](https://hub.nuxt.com/docs/features/database#database-migrations) in NuxtHub. ![Automatic Database Migrations](https://hub.nuxt.com/images/changelog/database-migrations.png){height="515" width="915"} ## Automatic Migration Application SQL migrations in `server/database/migrations/*.sql` are automatically applied when you: - Start the development server (`npx nuxt dev` or [`npx nuxt dev --remote`](https://hub.nuxt.com/docs/getting-started/remote-storage)) - Preview builds locally ([`npx nuxthub preview`](https://hub.nuxt.com/changelog/nuxthub-preview)) - Deploy via [`npx nuxthub deploy`](https://hub.nuxt.com/docs/getting-started/deploy#nuxthub-cli) or [Cloudflare Pages CI](https://hub.nuxt.com/docs/getting-started/deploy#cloudflare-pages-ci) Starting now, when you clone any of [our templates](https://hub.nuxt.com/templates) with a database, all migrations apply automatically! [→ Deploy the Todo App template](https://admin.hub.nuxt.com/new?template=atidone){rel="nofollow"} ![Database Migrations applied during deployment](https://hub.nuxt.com/images/changelog/database-migration-on-deploy.png){height="515" width="915"} ::note{to="https://hub.nuxt.com/docs/features/database#database-migrations"} Learn more about database migrations in our **full documentation**. :: ## New CLI Commands [`nuxthub@0.7.0`](https://github.com/nuxt-hub/cli){rel="nofollow"} introduces these database migration commands: ```bash [Terminal] # Create a new migration npx nuxthub database migrations create # View migration status npx nuxthub database migrations list # Mark all migrations as applied npx nuxthub database migrations mark-all-applied ``` Learn more about: - [Creating migrations](https://hub.nuxt.com/docs/features/database#creating-migrations) - [Checking migration status](https://hub.nuxt.com/docs/features/database#checking-migration-status) - [Marking migrations as applied](https://hub.nuxt.com/docs/features/database#marking-migrations-as-applied) ## Migrating from Existing ORMs ::important **Current Drizzle ORM users:** Follow these specific migration steps. :: Since NuxtHub doesn't recognize previously applied Drizzle ORM migrations (stored in `__drizzle_migrations`), it will attempt to rerun all migrations in `server/database/migrations/*.sql`. To prevent this: 1. Mark existing migrations as applied in each environment: ```bash [Terminal] # Local environment npx nuxthub database migrations mark-all-applied # Preview environment npx nuxthub database migrations mark-all-applied --preview # Production environment npx nuxthub database migrations mark-all-applied --production ``` 2. Remove `server/plugins/database.ts` as it's no longer needed. That's it! You can keep using `npx drizzle-kit generate` to generate migrations when updating your Drizzle ORM schema. ## Understanding Database Migrations Database migrations provide version control for your database schema. They track changes and ensure consistent schema evolution across all environments through incremental updates. ::note Implemented in [nuxt-hub/core#333](https://github.com/nuxt-hub/core/pull/333){rel="nofollow"} and [nuxt-hub/cli#31](https://github.com/nuxt-hub/cli/pull/31){rel="nofollow"}. :: # Deploy to NuxtHub button The button is designed to be used in the README of a GitHub repository, documentations or any other place where you want to allow users to deploy your project with one click. ![NuxtHub Deploy Button](https://hub.nuxt.com/images/changelog/deploy-button.png){height="515" width="915"} ## Deploy Button 1. The image of the button is on `https://hub.nuxt.com/button.svg` 2. The link to deploy the project is `https://hub.nuxt.com/new?repo=ORG/REPO` (replace `ORG/REPO` with your GitHub repository) 3. Your repository must be public and marked as "Template repository" on GitHub To use the button, you need to add the following markdown to your README: ::code-group ```md [Markdown] [![Deploy to NuxtHub](https://hub.nuxt.com/button.svg)](https://hub.nuxt.com/new?repo=ORG/REPO) ``` ```html [HTML] Deploy to NuxtHub ``` :: You can see an example of the button in action in the [Instadraw repository](https://github.com/atinux/instadraw){rel="nofollow"}. # Deployment Details We are excited to announce that we have added a new page to NuxtHub: **Deployment Details**! ![NuxtHub Deployment Details](https://hub.nuxt.com/images/changelog/deployment-details.png){height="515" width="915"} You can now access more information about each deployment such as: - **Details:** Status, Branch, Commit and more - **Build Logs:** Access your deployment build logs - **Server Logs:** Access your deployment server logs in real-time Based on the deployment status, you can also manage your deployment by: - Retrying the deployment - Cancelling the pending deployment - Rolling back to a previous deployment # Drizzle Studio Remembers ::note We partnered with the Drizzle team to leverage [Drizzle Studio](https://orm.drizzle.team/drizzle-studio/overview){rel="nofollow"} for the admin UI of your databases within NuxtHub. :br Allowing you to explore your production & preview SQL database connected to your project. :: We improved the experience so Drizzle Studio remembers you current state when you navigate away and come back. When opening the database tab, you will be taken back to the same table you were last time with the same filters applied. :video{controls className="w-full,h-auto" controls="true" poster="https://res.cloudinary.com/nuxt/video/upload/so_0/v1717513926/nuxthub/drizzle-studio-remember_m5b6hl.jpg"} # NuxtHub GitHub App & Action We're thrilled to release our brand new [GitHub Application](https://github.com/apps/nuxthub-admin){rel="nofollow"} & [GitHub Action](https://github.com/marketplace/actions/deploy-to-nuxthub){rel="nofollow"} to help you create and deploy Nuxt applications to NuxtHub. ## Pull Request Integration Once setup, NuxtHub will automatically comment on pull requests with branch URLs, permalinks and QR codes for easy preview access. ![GitHub Action](https://hub.nuxt.com/images/changelog/nuxthub-github-app-action.png){height="520" width="926"} ## GitHub Deployments NuxtHub also integrates with GitHub's deployment system, including status updates and environment tracking. ![NuxtHub GitHub Action deployments](https://hub.nuxt.com/images/changelog/nuxthub-github-action-deployment.png){height="521" width="926"} This includes GitHub Deployments support in pull requests. ![NuxtHub GitHub Action deployments](https://hub.nuxt.com/images/changelog/nuxthub-github-action-deployment-pr.png){height="219" width="926"} And many more: - **Deployment Protection**: Support for GitHub's deployment protection rules which enable approval workflows and environment restrictions ([learn more on GitHub's documentation](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment#deployment-protection-rules){rel="nofollow"}) - **Secure**: Our GitHub integration prevents the need for long-lived secrets as it uses OIDC under the hood - **Customizable**: You can create tailored workflows to fit your DevOps requirements using our GitHub Action ## Cloning a Template Thanks to the new GitHub App, you can now clone a template from NuxtHub Admin and deploy it with a single click. ![NuxtHub GitHub App clone template](https://hub.nuxt.com/images/changelog/nuxthub-github-app-clone-template.png){height="1460" width="2596"} ::note The repository will be created in your GitHub account with the GitHub Actions workflow already configured. :: ## Migrating to GitHub Actions Migrating from Cloudflare Pages CI or the legacy GitHub Action is simple and can be done from [NuxtHub Admin](https://admin.hub.nuxt.com/){rel="nofollow"} → Project → Settings → Git. When migrating from Cloudflare Pages CI, please note: - Deployment quotas will shift from [Pages CI limits](https://developers.cloudflare.com/pages/platform/limits/#builds){rel="nofollow"} to your [GitHub Actions usage](https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes){rel="nofollow"} - Environment variables and secrets needed at build time should be managed through [GitHub Environment settings](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-an-environment){rel="nofollow"} (we are working on a way to synchronize them automatically) ::callout --- icon: i-lucide-book to: https://hub.nuxt.com/docs/getting-started/deploy#github-action --- Learn more about deploying with GitHub Actions to NuxtHub. :: P.S. Give our new [GitHub Action](https://github.com/nuxt-hub/action){rel="nofollow"} a star 🌟💚 # Hello World Template ::tip This template is a great starting point for your next Nuxt project. It's simple, fast and ready to be deployed to the Edge. :: ## Minimal by design This template is a minimal Nuxt project **with no storage**, just a simple "Hello from the Edge 👋" message that you can configure with an environment variable: ```bash [.env] NUXT_PUBLIC_HELLO_TEXT="My custom message!" ``` We enabled the Nuxt 4 upcoming directory structure, ESLint and the Nuxt DevTools for a great developer experience. ::callout{icon="i-simple-icons-github"} The source code is open source and available on GitHub at [nuxt-hub/hello-edge](https://github.com/nuxt-hub/hello-edge){rel="nofollow"}. :: ## Deploy with one-click From now on, you can deploy this template to the Edge (globally, all over the world) with a single click directly from the [NuxtHub Admin](https://admin.hub.nuxt.com){rel="nofollow"}: :video{controls className="w-full,h-auto" controls="true" poster="https://res.cloudinary.com/nuxt/video/upload/v1718664649/nuxthub/hello-edge-template_tfmkie.jpg"} ::callout Checkout the demo of the project from the video at [vue.nuxt.dev](https://vue.nuxt.dev){rel="nofollow"}. :: ::tip You can deploy this template on a free NuxtHub account and a free Cloudflare account. :: You can also visit [our templates page](https://hub.nuxt.com/templates) to get started. # Introducing hubAI() ::tip This feature is available on both [free and pro plans](https://hub.nuxt.com/pricing) and in [`@nuxthub/core >= v0.7.2`](https://github.com/nuxt-hub/core/releases/tag/v0.7.2){rel="nofollow"}. :: We are excited to introduce [`hubAI()`](https://hub.nuxt.com/docs/features/ai). This new method allows you to run machine learning models, such as LLMs, directly within your Nuxt application with minimal setup. At NuxtHub, we care about DX and we want to make it easy for you to leverage AI in your application using Cloudflare AI **without having to manage API keys, account ID or using the `wrangler` CLI**. ## How to use hubAI() 1. Update `@nuxthub/core` to the latest version (`v0.7.2` or later) 2. Enable `hub.ai` in your `nuxt.config.ts` ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { ai: true }, }) ``` 3. Run `npx nuxthub link` to link a NuxtHub project or create a new one 4. You can now use [`hubAI()`](https://hub.nuxt.com/docs/features/ai) in your server routes ```ts [server/api/ai-test.ts] export default defineEventHandler(async (event) => { return await hubAI().run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Who is the author of Nuxt?' }) }) ``` Read the [full documentation](https://hub.nuxt.com/docs/features/ai) to learn more about `hubAI()`. ::important **If you already have a NuxtHub account**, make sure to add the `Worker AI` permission on your Cloudflare API token. - Open [Cloudflare User API Tokens](https://dash.cloudflare.com/profile/api-tokens){rel="nofollow"} - Find the NuxtHub token(s) - Add the `Account > Worker AI > Read` permission - Save the changes Another solution is to link again your Cloudflare account from your NuxtHub team settings by clicking on `Link a new account` > `Create a token with required permissions`. :: ::note This feature has been implemented in [nuxt-hub/core#173](https://github.com/nuxt-hub/core/pull/173){rel="nofollow"}. :: # Introducing hubBrowser() ::tip This feature is available on both [free and pro plans](https://hub.nuxt.com/pricing) of NuxtHub but on the [Workers Paid plan](https://www.cloudflare.com/plans/developer-platform/){rel="nofollow"} for your Cloudflare account. :: We are excited to introduce [`hubBrowser()`](https://hub.nuxt.com/docs/features/browser). This new method allows you to run a headless browser directly in your Nuxt application using [Puppeteer](https://github.com/puppeteer/puppeteer){rel="nofollow"}. :video{controls className="w-full,h-auto,rounded" controls="true" poster="https://res.cloudinary.com/nuxt/video/upload/v1725901706/nuxthub/nuxthub-browser_dsn1m1.jpg"} ## How to use hubBrowser() 1. Update `@nuxthub/core` to the latest version (`v0.7.11` or later) 2. Enable `hub.browser` in your `nuxt.config.ts` ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { browser: true } }) ``` 3. Install the required dependencies ```bash [Terminal] npx nypm i @cloudflare/puppeteer puppeteer ``` 4. Start using [`hubBrowser()`](https://hub.nuxt.com/docs/features/browser) in your server routes ```ts [server/api/screenshot.ts] export default eventHandler(async (event) => { const { page } = await hubBrowser() await page.setViewport({ width: 1920, height: 1080 }) await page.goto('https://cloudflare.com') setHeader(event, 'content-type', 'image/jpeg') return page.screenshot() }) ``` 5. Before deploying, make sure you are subscribed to the [Workers Paid plan](https://www.cloudflare.com/plans/developer-platform/){rel="nofollow"} 6. [Deploy your project with NuxtHub](https://hub.nuxt.com/docs/getting-started/deploy) ::note{to="https://hub.nuxt.com/docs/features/browser"} Read the documentation about `hubBrowser()` with more examples. :: # Introducing hubVectorize() ::tip This feature is available on both [free and pro plans](https://hub.nuxt.com/pricing) and in [`@nuxthub/core >= v0.7.24`](https://github.com/nuxt-hub/core/releases/tag/v0.7.24){rel="nofollow"}. :: We are excited to introduce [`hubVectorize()`](https://hub.nuxt.com/docs/features/vectorize), which gives you access to a globally distributed vector database from Nuxt. Paired with [`hubAI()`](https://hub.nuxt.com/docs/features/ai), it makes Nuxt a perfect framework for easily building full-stack AI-powered applications. ![NuxtHub Vectorize command](https://hub.nuxt.com/images/changelog/nuxthub-vectorize.png){height="515" width="915"} ## What is a vector database? Vector databases allows you to querying embeddings, which are representations of values or objects like text, images, audio that are designed to be consumed by machine learning models and semantic search algorithms. Some key use cases of vector databases include: - Semantic search, used to return results similar to the input of the query. - Classification, used to return the grouping (or groupings) closest to the input query. - Recommendation engines, used to return content similar to the input based on different criteria (for example previous product sales, or user history). Vector databases are commonly used for [Retrieval-Augmented Generation (RAG)](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-rag/){rel="nofollow"}, which is a technique that enhances language models by retrieving relevant information from an external knowledge base before generating a response. Learn more about vector databases on [Cloudflare's documentation](https://developers.cloudflare.com/vectorize/reference/what-is-a-vector-database/){rel="nofollow"}. ## How to use hubVectorize() 1. Update `@nuxthub/core` to the latest version (`v0.7.24` or later) 2. Configure a Vectorize index in `hub.vectorize` in your `nuxt.config.ts` ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { vectorize: { tutorial: { dimensions: 768, metric: "cosine" } } } }) ``` 3. Run `npx nuxthub link` to link a NuxtHub project or create a new one 4. Deploy the project to NuxtHub with `npx nuxthub deploy` 5. Start Nuxt and connect to [remote storage](https://hub.nuxt.com/docs/getting-started/remote-storage) by running [`npx nuxt dev --remote`](https://hub.nuxt.com/docs/getting-started/remote-storage) 6. You can now use [`hubVectorize('')`](https://hub.nuxt.com/docs/features/vectorize) in your server routes ```ts [server/api/vectorize-search.ts] export default defineEventHandler(async (event) => { const { query, limit } = getQuery(event) const embeddings = await hubAI().run('@cf/baai/bge-base-en-v1.5', { text: query }) const queryVector = embeddings.data[0] const index = hubVectorize('tutorial') return await vectorize.query(vectors, { topK: limit, }) }) ``` Read the [full documentation](https://hub.nuxt.com/docs/features/vectorize) to learn more about `hubVectorize()`. ::important **If you already have a NuxtHub account**, make sure to add the `Vectorize` permission on your Cloudflare API token. - Open [Cloudflare User API Tokens](https://dash.cloudflare.com/profile/api-tokens){rel="nofollow"} - Find the NuxtHub token(s) - Add the `Account > Vectorize > Edit` permission - Save the changes Another solution is to link again your Cloudflare account from your NuxtHub team settings by clicking on `Link a new account` > `Create a token with required permissions`. :: ::note This feature has been implemented in [nuxt-hub/core#177](https://github.com/nuxt-hub/core/pull/177){rel="nofollow"}. :: # Hyperdrive bindings If you are not comfortable with Cloudflare D1 database (SQLite), you can now use your own PostgreSQL database. We wrote a recipe on [How to use a PostgreSQL database](https://hub.nuxt.com/docs/recipes/postgres) with NuxtHub. This recipe explains how to connect to your PostgreSQL database and how to leverage Hyperdrive bindings in your Nuxt application to accelerate your database responses. ::callout Right now, hyperdrive bindings are not available in development mode, we are working with the Cloudflare team to make them available in development mode as well with remote storage. :: ::tip This feature is available on both [free and pro plans](https://hub.nuxt.com/pricing) and in [`@nuxthub/core >= v0.7.6`](https://github.com/nuxt-hub/core/releases/tag/v0.7.6){rel="nofollow"}. :: # `nuxthub preview` command As developers working with Cloudflare Workers and edge runtimes, we've long grappled with the challenges of accurately previewing our production builds locally. The edge runtime environment differs significantly from Node.js, which is why Cloudflare introduced the `wrangler pages dev` command. However, as NuxtHub doesn't rely on a `wrangler.toml` file, this solution wasn't quite perfect. Today, I'm excited to introduce the `nuxthub preview` command. This new addition to our CLI bridges the gap between local development and edge runtime environments, making it easier than ever to test and refine your NuxtHub projects before deployment. ## Usage With the latest release of the `nuxthub` CLI (v0.6.0), you can now preview your production build locally with a new command. ```bash [Terminal] # 1/ Build your application for production npx nuxt build # 2/ Preview your production build locally npx nuxthub preview ``` This command will: 1. read the `dist/hub.config.json` file and generate a local `dist/wrangler.toml` file 2. start the server using the `wrangler pages dev` command within the `dist/` director ![NuxtHub Preview command](https://hub.nuxt.com/images/changelog/nuxthub-preview.png){height="515" width="915"} ## Limitations At the moment, the `nuxthub preview` command has the following limitations: - It does not work with the `--remote` flag (only local bindings) - [`hubAI()`](https://hub.nuxt.com/docs/features/ai) will ask you connect within the terminal with wrangler - [`hubBrowser()`](https://hub.nuxt.com/docs/features/browser) is not supported as not supported by `wrangler pages dev` ## Open Source The CLI is full open source on GitHub, feel free to contribute and improve it. ::callout --- icon: i-simple-icons-github target: _blank to: https://github.com/nuxt-hub/cli --- Checkout the `nuxthub` CLI on GitHub. :: # Server Cache UI We are excited to announce that we have added a new page to your NuxtHub project: **Server Cache**! ![NuxtHub Deployment Details](https://hub.nuxt.com/images/changelog/server-cache.png){height="515" width="915"} It works with: - [`cachedEventHandler`](https://nitro.unjs.io/guide/cache#cached-event-handlers){rel="nofollow"} - [`cachedFunction`](https://nitro.unjs.io/guide/cache#cached-functions){rel="nofollow"} - [`routeRules`](https://nitro.unjs.io/guide/cache#caching-route-rules){rel="nofollow"} ::callout{icon="i-lucide-book" to="https://hub.nuxt.com/docs/features/cache"} Learn more about server cache. :: # Team Webhooks We are excited to announce that we have added a new feature to NuxtHub: **Team Webhooks**! ::tip This feature is available on all [NuxtHub plans](https://hub.nuxt.com/pricing). :: ## What are Team Webhooks? They allow you to get notified about your project deployments. You can use them to trigger custom actions, like sending notifications to your team's chat, updating your project management tool and more. ![Team Webhooks](https://hub.nuxt.com/images/changelog/team-webhooks.png){height="515" width="915"} ## Creating a Webhook To create a webhook, go to your team settings and click on the "Webhooks" tab. You can then add a new webhook by providing an endpoint and selecting the events you want to trigger the webhook for. ![Creating a NuxtHub Webhook](https://hub.nuxt.com/images/changelog/team-webhooks-new.png){height="515" width="915"} ## Slack & Discord Integration We have also added built-in integrations for [Slack Incoming Webhooks](https://api.slack.com/messaging/webhooks){rel="nofollow"} and [Discord Webhooks](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks){rel="nofollow"}. When pasting a webhook URL from these services, we will automatically send a pre-formatted message to your channel or user. ![NuxtHub Slack Integration](https://hub.nuxt.com/images/changelog/team-webhooks-slack.png){height="515" width="915"} ::callout{icon="i-lucide-heart"} Thank you to [Israel Ortuno](https://github.com/IsraelOrtuno){rel="nofollow"} for suggesting this feature on [nuxt-hub/core#102](https://github.com/nuxt-hub/core/issues/102){rel="nofollow"}. :: # Webhooks Environment Selection ::tip This feature is available on all [NuxtHub plans](https://hub.nuxt.com/pricing). :: It is now possible to select the environment to be notified about when creating a webhook. You can select to be notified about all environments, only the production or preview environment. We also added the possibility to add a name to the webhook for better identification. ![Team Webhooks Env selection](https://hub.nuxt.com/images/changelog/team-webhooks-env.png){className="rounded" height="515" width="915"} ::callout --- icon: i-lucide-webhooks to: https://hub.nuxt.com/changelog/team-webhooks --- Learn more about team webhooks. :: ::callout{icon="i-lucide-heart"} Thank you to [Estéban Soubiran](https://github.com/Barbapapazes){rel="nofollow"} for suggesting this feature on [nuxt-hub/core#127](https://github.com/nuxt-hub/core/issues/127){rel="nofollow"}. :: # NuxtHub on Workers ::tip This feature is available on both [free and pro plans](https://hub.nuxt.com/pricing) and in [`@nuxthub/core >= v0.8.24`](https://github.com/nuxt-hub/core/releases/tag/v0.8.24){rel="nofollow"}. :: After much development (and [many](https://x.com/Atinux/status/1907552625559744865/photo/1){rel="nofollow"} [teasers](https://x.com/Atinux/status/1884315020982657452/video/1){rel="nofollow"}), we're thrilled to announce that NuxtHub now supports deploying to Cloudflare Workers! ## Why Workers For a while, Cloudflare have been releasing exciting new features such as [observability & persistent logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/){rel="nofollow"}, [workflows](https://developers.cloudflare.com/workflows/){rel="nofollow"}, [cron triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/){rel="nofollow"}, and [much more](https://developers.cloudflare.com/workers/static-assets/migrate-from-pages/#compatibility-matrix){rel="nofollow"}. However, these features are only available on Cloudflare Workers, and were not brought to Cloudflare Pages. Now with the introduction of [static assets](https://developers.cloudflare.com/workers/static-assets/){rel="nofollow"}, suddenly it is now viable to deploy Nuxt applications to Cloudflare Workers, and we can benefit from the new features and services available on Cloudflare Workers. ## Real-time Projects deployed to NuxtHub on Workers also fully support [Nitro WebSocket](https://nitro.build/guide/websocket){rel="nofollow"}, allowing you to create real-time experiences on NuxtHub. Simply set `nitro.experimental.websocket` to `true` in your `nuxt.config.ts`, create your websocket server route with crossws, and deploy! Under the hood this is powered by Cloudflare Durable Objects. Nitro and NuxtHub magically takes care of everything related to the Durable Object, from building to deploying. Check out our [multiplayer-globe](https://github.com/nuxt-hub/multiplayer-globe){rel="nofollow"} template for an example of using websockets ([live demo](https://multiplayer-globe.nuxthub.workers.dev/){rel="nofollow"}). ::note --- icon: i-lucide-mouse-pointer-click to: https://hub.nuxt.com/docs/features/realtime --- Learn more about using websockets in NuxtHub. :: ## Deploying to Workers Get started by creating a new project and selecting the Cloudflare Workers type on the [NuxtHub Admin](https://admin.hub.nuxt.com){rel="nofollow"} or with the latest version of the `nuxthub` CLI ([v0.9.0](https://github.com/nuxt-hub/cli/releases){rel="nofollow"}). All our templates are fully compatible with NuxtHub on Workers. We're working on a migration path to help simplify the switch from Cloudflare Pages to Workers. Until then, you can deploy your existing Nuxt apps to Cloudflare Workers by setting `hub.workers` to `true`, and linking them to a separate new project with the Workers type, then manually moving any data stored in your database, KV or blob. ::note While NuxtHub on Workers is in beta, preview environments aren't unavailable. Stay tuned for updates. :: ::important **If you already have a NuxtHub account**, make sure to add the `Workers Scripts` permission on your Cloudflare API token. - Open [Cloudflare User API Tokens](https://dash.cloudflare.com/profile/api-tokens){rel="nofollow"} - Find your NuxtHub token(s) - Add the `Workers Scripts > Edit` permission - Save the changes Another solution is to link again your Cloudflare account from your NuxtHub team settings by clicking on `Connect a different Cloudflare account` > `Create a token`. :: ## What's next Throughout the next few weeks, we'll be enhancing NuxtHub on Workers with new features and integration with more Cloudflare Workers-specific services, including: - **Observability**: Automatically ingest, filter, and analyse logs - **Queues**: Process background tasks effectively at scale - **Cron Tasks**: Run Nitro tasks automatically on a schedule - **Environments**: Bringing the preview environment to NuxtHub on Workers Let us know your feedback by joining our [Discord](https://discord.gg/vW89dsVqBF){rel="nofollow"}, following us on [X](https://x.com/nuxt_hub){rel="nofollow"}, or emailing us at . # Introducing NuxtHub Beta With more than 700 alpha testers and 3,000 project deployments in 3 months, we are excited to announce that NuxtHub is now in public beta ✨ ## What is NuxtHub? NuxtHub is an extension of the [Nuxt framework](https://nuxt.com){rel="nofollow"} to help you build full-stack applications on your Cloudflare account, with zero configuration. ::note{target="_blank" to="https://en.wikipedia.org/wiki/Cloudflare"} According to Wikipedia, Cloudflare is an American company that provides content delivery network services, cloud cybersecurity, DDoS mitigation, Domain Name Service, and ICANN-accredited domain registration services. Cloudflare is used by more than 20 percent of the Internet for its web security services, as of 2022. :: NuxtHub is composed of two elements: - An open source Nuxt module: [@nuxthub/core](https://github.com/nuxt-hub/core){rel="nofollow"} - An admin UI: [admin.hub.nuxt.com](https://admin.hub.nuxt.com){rel="nofollow"} Let’s explain what the module and the admin are responsible for. ### NuxtHub Module **The NuxtHub module** allows you to access a SQL database, blob and KV storage (and more features) with zero configuration. In development, it generates a `.data/hub/wrangler.toml` file and uses [Cloudflare Proxy](https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy){rel="nofollow"} to connect to the desired bindings: - [`hubDatabase()`](https://hub.nuxt.com/docs/features/database) -> [Cloudflare D1](https://www.cloudflare.com/developer-platform/d1/){rel="nofollow"} - [`hubKV()`](https://hub.nuxt.com/docs/features/kv) -> [Cloudflare KV](https://www.cloudflare.com/developer-platform/workers-kv/){rel="nofollow"} - [`hubBlob()`](https://hub.nuxt.com/docs/features/blob) -> [Cloudflare R2](https://www.cloudflare.com/developer-platform/r2/){rel="nofollow"} In production, it reads from the bindings set in your Cloudflare dashboard following a specific naming convention: DB, BLOB, KV. Note that `hubDatabase()`, `hubKV()` and `hubBlob()` server utils are a bit different than the binding itself to provide more features and a better developer experience when used in a Nuxt application. ::callout{icon="i-lucide-rocket"} By leveraging bindings, you never have to add secret keys or tokens to your Nuxt application in order to access the database, KV or blob. The underlying secret is never exposed to your Nuxt server’s code, and therefore can’t be accidentally leaked. :: One of the core features of the `@nuxthub/core` module is the ability to connect to your deployed project and access the bindings used there, we call it [remote storage](https://hub.nuxt.com/docs/getting-started/remote-storage). This feature is useful for working with your production or staging database, KV and blob storage during development. ![NuxtHub remote storage in terminal](https://hub.nuxt.com/images/blog/nuxthub-remote-storage.png){height="515" width="915"} When opening the [Nuxt DevTools](https://devtools.nuxt.com){rel="nofollow"} of your project, you will notice new tabs: ![NuxtHub tabs in Nuxt devtools](https://hub.nuxt.com/images/blog/nuxthub-devtools.png){height="515" width="915"} NuxtHub adds a UI to administrate your SQL database (powered by [Drizzle Studio](https://orm.drizzle.team/drizzle-studio/overview){rel="nofollow"}), KV and Blob storage as well as your server cache. ::callout The module is open source and available at [github.com/nuxt-hub/core](https://github.com/nuxt-hub/core){rel="nofollow"}. :: Once your Nuxt project with the hub module is ready, you can deploy your project on your Cloudflare account by following the [self-hosted guide](https://hub.nuxt.com/docs/getting-started/deploy#self-hosted). If you want a full zero configuration experience, we recommend using the NuxtHub Admin platform. ### NuxtHub Admin The [NuxtHub admin](https://admin.hub.nuxt.com){rel="nofollow"} is a web based dashboard to manage your NuxtHub applications. It helps you deploy your NuxtHub apps with a single command on your Cloudflare account while provisioning all the necessary resources for you. It abstracts the complexity of managing full-stack Nuxt applications on Cloudflare. Some of the features are: - Link your Cloudflare account and stay in control (we never mark-up Cloudflare prices) - Clone one of our full-stack Nuxt templates - Deploy your application with `npx nuxthub deploy` command or link your GitHub or GitLab repository - Relax while it provisions all the necessary resources (database, kv, blob) - Manage your app's resources with an admin panel - Share team member access to manage your app without sharing your Cloudflare account - Monitor your application with logs and analytics [![NuxtHub projects management](https://hub.nuxt.com/images/blog/nuxthub-admin-projects.png){height="515" width="915"}](https://admin.hub.nuxt.com){rel="nofollow"} You can sign up for free and start building full-stack Nuxt applications at [admin.hub.nuxt.com](https://admin.hub.nuxt.com){rel="nofollow"}. ## Why NuxtHub? Starting with version 3, Nuxt is a complete full-stack framework thanks to its open server engine called [Nitro](https://nitro.unjs.io){rel="nofollow"}. It allows you to have hot module replacement on the server without rebuilding your Vue application, outstanding performance as well as deploying to many different hosting providers with zero configuration. Read more about it on the blog post: [Nuxt on the Edge](https://nuxt.com/blog/nuxt-on-the-edge){rel="nofollow"}. With this in mind, we wanted to provide you with a reliable platform to build and deploy your next idea while keeping the best developer experience. NuxtHub lets you build any kind of web application (SaaS, E-Commerce, Blog, you name it!) that can: - Deploy globally on 275+ locations worldwide - Access to a SQL database: user authentication, content-management, etc. - Store files: media upload, invoice storage, etc. - Rank high on search engines with server-side rendering or even hybrid rendering - Support staging environment (preview mode) - Get a nice .nuxt.dev domain 🕶️ All of this, **starting at $0/month** thanks to Cloudflare free plan and ours, learn more on our [pricing page](https://hub.nuxt.com/pricing). ::note Your Nuxt applications are deployed on **Your Cloudflare account** and we **never mark-up Cloudflare pricing**. :: NuxtHub also has the ability to access your remote storage from your local environment thanks to our secured proxy system. This feature is useful for sharing your database, KV, and blob data with your team in development or applying migrations or scripts to your staging/production data. As of today, NuxtHub works like this: ![NuxtHub architecture](https://hub.nuxt.com/images/landing/nuxthub-schema.png){height="594" width="915"} This is only the beginning as we plan to support more Cloudflare primitives in the future: ![Cloudflare primitives](https://hub.nuxt.com/images/blog/cloudflare-primitives.png){height="810" width="915"} ## What’s next? To help you test NuxtHub features, we have released [open source templates](https://hub.nuxt.com/templates). You also have the option to [add to your current Nuxt application](https://hub.nuxt.com/docs/getting-started/installation#add-to-a-nuxt-project). Now that we have the main primitives to build full-stack applications (the admin is built & deployed with NuxtHub), we plan to add business-logic features such as: - `@nuxthub/auth`: Add authentication for user management - `@nuxthub/email`: Send transactional emails to your users - `@nuxthub/analytics`: Understand your traffic and track events within your application and API - `@nuxthub/...`: You name it! Feel free to join us, contribute or give a star on {rel="nofollow"} 💚 Happy Nuxting! # Code, Draw, Deploy: A drawing app with Nuxt & Cloudflare R2 ## Introduction I won't go into each detail of the code, but I'll try to explain the main concepts and how to build a drawing app with Nuxt and Cloudflare R2. Atidraw is a web application that lets you create and share your drawings with the world. Our app uses OAuth for user authentication and Cloudflare R2 to store and list drawings. The application runs with server-side rendering on the edge using Cloudflare Pages on the Workers free plan. :video{controls className="lg:w-2/3,h-auto,border,dark:border-gray-800,rounded" controls="true" poster="https://res.cloudinary.com/nuxt/video/upload/v1723210615/nuxthub/344159247-85f79def-f633-40b7-97c2-3a8579e65af1_xyrfin.jpg"} ::note{icon="i-lucide-rocket" target="_blank" to="https://draw.nuxt.dev"} The demo is available at **draw\.nuxt.dev**. :: ::callout --- icon: i-simple-icons-github target: _blank to: https://github.com/atinux/atidraw --- The source code of the app is available at **github.com/atinux/atidraw**. :: ## Project Dependencies Our Nuxt application uses the following dependencies: - [`nuxt-auth-utils`](https://github.com/atinux/nuxt-auth-utils){rel="nofollow"} for user authentication - [`signature_pad`](https://github.com/szimek/signature_pad){rel="nofollow"} for the drawing canvas - [`@nuxt/ui`](https://ui.nuxt.com/){rel="nofollow"} for the UI components - [`@nuxthub/core`](https://github.com/nuxthub/core){rel="nofollow"} for a zero config experience with Cloudflare R2 In our `nuxt.config.ts` we need to enable the following modules and options: ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: [ '@nuxthub/core', '@nuxt/ui', 'nuxt-auth-utils' ], hub: { // Enable Cloudflare R2 storage blob: true }, }) ``` ::note The `blob` option will use Cloudflare platform proxy in development and automatically create a Cloudflare R2 bucket for your project when you deploy it. It also provides helpers to upload and list files. :: ::tip The project is also using the `future.compatibilityVersion: 4` option to leverage the [new directory structure](https://nuxt.com/docs/getting-started/upgrade#new-directory-structure){rel="nofollow"}. :: ## User Authentication For user authentication, we'll use [`nuxt-auth-utils`](https://github.com/atinux/nuxt-auth-utils){rel="nofollow"}. It provides functions to authenticate users with OAuth providers and stores the user session in encrypted cookies. First, we need to set up a session secret (used to encrypt & decrypt the session cookie) and our OAuth application credentials in the `.env` file: ```bash [.env] NUXT_SESSION_PASSWORD=our_session_secret NUXT_OAUTH_GITHUB_CLIENT_ID=our_github_client_id NUXT_OAUTH_GITHUB_CLIENT_SECRET=our_github_client_secret ``` Then, create a server route to handle the OAuth callback in `server/auth/github.get.ts`: ```ts [server/auth/github.get.ts] export default oauthGitHubEventHandler({ async onSuccess(event, { user }) { await setUserSession(event, { user: { provider: 'github', id: String(user.id), name: user.name || user.login, avatar: user.avatar_url, url: user.html_url, }, }) return sendRedirect(event, '/draw') }, }) ``` ::tip The `.get.ts` suffix indicates that only GET requests will be handled by this route. :: When the user hits `/auth/github`: 1. `oauthGitHubEventHandler` redirects the user to the GitHub OAuth page 2. The user is then redirected back to **/auth/github** 3. `onSuccess()` is called and the user session is set in a cookie 4. The user is finally redirected to **/draw** In [`app/pages/draw.vue`](https://github.com/atinux/atidraw/blob/main/app/pages/draw.vue){rel="nofollow"}, we can leverage [`useUserSession()`](https://github.com/atinux/nuxt-auth-utils?tab=readme-ov-file#vue-composable){rel="nofollow"} to know if the user is authenticated or not. ```vue [app/pages/draw.vue] ``` ::tip --- target: _blank to: https://github.com/atinux/nuxt-auth-utils?tab=readme-ov-file#vue-composable --- Learn more about the `useUserSession()` composable. :: As we use TypeScript, we can type the session object to get autocompletion and type checking by creating a `types/auth.d.ts` file: ```ts [types/auth.d.ts] declare module '#auth-utils' { interface User { provider: 'github' | 'google' id: string name: string avatar: string url: string } } // export is required to avoid type errors export {} ``` ## Drawing Canvas For the drawing interface, we'll use the `signature_pad` library and create a new component in `components/DrawPad.vue`: ```vue [app/components/DrawPad.vue] ``` ::callout --- icon: i-lucide-code-xml target: _blank to: https://github.com/atinux/atidraw/blob/main/app/components/DrawPad.vue --- See the full source code of **app/components/DrawPad.vue**. :: ## Upload Drawings In the `app/pages/draw.vue` page, we need to upload the drawing to our Cloudflare R2 bucket. For this, we want to convert the `dataURL` we receive from the drawing canvas to a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob){rel="nofollow"}, then the Blob to a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File){rel="nofollow"} to specify the file type and name. Finally we create a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData){rel="nofollow"} object with the file and upload it to the **/api/upload** API route. ```vue [app/pages/draw.vue] ``` Let's create the API route to store the drawing in the Cloudflare R2 bucket: ```ts [server/api/upload.post.ts] export default eventHandler(async (event) => { // Make sure the user is authenticated to upload const { user } = await requireUserSession(event) // Read the form data const form = await readFormData(event) const drawing = form.get('drawing') as File // Ensure the file is a jpeg image and is not larger than 1MB ensureBlob(drawing, { maxSize: '1MB', types: ['image/jpeg'], }) // Upload the file to the Cloudflare R2 bucket return hubBlob().put(`${Date.now()}.jpg`, drawing, { addRandomSuffix: true, customMetadata: { userProvider: user.provider, userId: user.id, userName: user.name, userAvatar: user.avatar, userUrl: user.url, }, }) }) ``` ::tip The `requireUserSession()` function is provided by [`nuxt-auth-utils`](https://github.com/atinux/nuxt-auth-utils){rel="nofollow"} and will throw a `401` error if the user is not authenticated. :: As you can see, we don't need a database as we store the user metadata in the Cloudflare R2 bucket custom metadata. ::note Learn more about the [`hubBlob()`](https://hub.nuxt.com/docs/storage/blob) server function to work with the Cloudflare R2 bucket. :: ## List Drawings It's time to list our user drawings! First, however, we need to create a new API route in `server/api/drawings.get.ts`: ```ts [server/api/drawings.get.ts] export default eventHandler(async (event) => { // Return 100 last drawings return hubBlob().list({ limit: 100 }) }) ``` Then, we'll create a new page in `app/pages/index.vue` to list the drawings: ```vue [app/pages/index.vue] ``` ::tip{icon="i-lucide-rocket"} That's it! We have a minimal and fully functional drawing application. :: ## Drawings Order You may have noticed that the last drawing is displayed last, this is because Cloudflare R2 is using alphabetical order to list the files and we use the timestamp (using `Date.now()`) as the file name. Also, R2 doesn't support listing files with a custom order. Even though it's easy to add a Cloudflare D1 database with [`hubDatabase()`](https://hub.nuxt.com/docs/storage/database), I wanted to keep this example as simple as possible. Instead, I had the idea to use the timestamp in 2050 minus the timestamp of the drawing to get a descending order. It's not perfect but it works, until 2050, it's still a long time 😄. Let's update our **/api/upload** route to update the filename: ```ts [server/api/upload.post.ts] export default eventHandler(async (event) => { // ... /** * Create a new pathname to be smaller than the last one uploaded * So the blob listing will send the last uploaded image at first * We use the timestamp in 2050 minus the current timestamp * So this project will start to be buggy in 2050, sorry for that **/ const name = `${new Date('2050-01-01').getTime() - Date.now()}` // Upload the file to the Cloudflare R2 bucket return hubBlob().put(`${name}.jpg`, drawing, { // ... }) }) ``` We now have our last drawing uploaded at the top of our listing 🚀 ## Drawings Pagination What if we have more than 100 drawings? We need to add pagination to our listing. The [`hubBlob().list()`](https://hub.nuxt.com/docs/storage/blob#list) accepts a `cursor` parameter to paginate the results. Let's update our API route to support pagination with a `cursor` query parameter: ```ts [server/api/drawings.get.ts] export default eventHandler(async (event) => { const { cursor } = await getQuery<{ cursor?: string }>(event) return hubBlob().list({ limit: 100, cursor }) }) ``` The API route returns a `BlobListResult` object with a `cursor` and `hasMore` properties: ```ts interface BlobListResult { blobs: BlobObject[] hasMore: boolean cursor?: string folders?: string[] } ``` The returned `cursor` value is used to get the next page of drawings (if `hasMore` is `true`). We can use [VueUse `vInfiniteScroll` directive](https://vueuse.org/core/useInfiniteScroll/#directive-usage){rel="nofollow"} to create an infinite scroll to load more drawings. ```vue [app/pages/index.vue] ``` We now have a pagination system that loads more drawings when the user scrolls to the bottom of the page. ## Deploying the App You can host your drawing application on a **free Cloudflare account** and **free NuxtHub account**. All you have to do is to run one single command: ```bash [Terminal] npx nuxthub deploy ``` This command will: - Build your Nuxt application - Create a new Cloudflare Pages project on your Cloudflare account - Provision a Cloudflare R2 bucket - Deploy your application - Provide you with a URL to access your application with a free `.nuxt.dev` domain. ::tip{to="https://hub.nuxt.com/docs/getting-started/deploy"} Learn more about [deploying Nuxt apps with NuxtHub](https://hub.nuxt.com/docs/getting-started/deploy) (CLI, GitHub action or Cloudflare Pages CI). :: If you prefer, you can also deploy this project using the NuxtHub Admin by clicking on the button below: [![Deploy to NuxtHub](https://hub.nuxt.com/button.svg){height="32" width="174"}](https://hub.nuxt.com/new?repo=atinux/atidraw){rel="nofollow"} ### Remote Storage Once your project is deployed, you can use [NuxtHub Remote Storage](https://hub.nuxt.com/docs/getting-started/remote-storage) to connect to your preview or production Cloudflare R2 bucket in development using the `--remote` flag: ```bash [Terminal] npx nuxt dev --remote ``` ## Manage Drawings Some users may draw inappropriate drawings that we may want to remove. For this, NuxtHub provides a Blob panel in both the Nuxt DevTools and the NuxtHub Admin. ### Development When running your project locally, you can open the Nuxt DevTools: - `Shift + Option + D` shortcut or clicking on the Nuxt logo in the botttom of the screen - The look for the **Hub Blob** tab (you can also use `CTRL + K` to open the search bar and type `Blob`) ![NuxtHub DevTools Blob for Atidraw](https://hub.nuxt.com/images/blog/atidraw-devtools-blob.png){dataZoomSrc="/images/blog/atidraw-devtools-blob.png" height="515" width="915"} ### Production You can manage all the drawings using the Blob panel in the NuxtHub Admin. Once deployed, open the admin panel of your application with: ```bash [Terminal] npx nuxthub manage ``` Or go to {rel="nofollow"} and select your project. ![NuxtHub Admin Blob for Atidraw](https://hub.nuxt.com/images/blog/atidraw-admin-blob.png){dataZoomSrc="/images/blog/atidraw-admin-blob.png" height="515" width="915"} ## Conclusion Congratulations! You've now built a fully functional drawing application using Nuxt and Cloudflare R2 for storage. Users can create drawings, save them to the cloud, and access them from anywhere. Feel free to expand on this foundation and add your own unique features to make Atidraw yours! ::callout --- icon: i-simple-icons-github target: _blank to: https://github.com/atinux/atidraw --- The source code of the app is available at **github.com/atinux/atidraw**. :: ::note{icon="i-lucide-rocket" target="_blank" to="https://draw.nuxt.dev"} The demo is available at **draw\.nuxt.dev**. :: Checkout the next article on how to leverage Cloudflare AI to generate the alternative text for the user drawings (accessibility & SEO) as well as generating an alternative drawing using AI: [Cloudflare AI for User Experience](https://hub.nuxt.com/blog/cloudflare-ai-for-user-experience). # Using Cloudflare AI Models for User Experience ## Introduction Let's improve [Atidraw](https://github.com/atinux/atidraw){rel="nofollow"}, an open source collaborative drawing app made with Nuxt. The application has basic features such as: - Authentication with Google, GitHub or login anonymously based on [`nuxt-auth-utils`](https://github.com/Atinux/nuxt-auth-utils){rel="nofollow"} - Drawing with [`signature_pad`](https://github.com/szimek/signature_pad){rel="nofollow"} - Upload and store drawings with Cloudflare R2 using [`hubBlob()`](https://hub.nuxt.com/docs/features/blob) You can play with it on [draw.nuxt.dev](https://draw.nuxt.dev){rel="nofollow"}. ::tip Checkout how we created Atidraw in the ["Code, Draw, Deploy: A drawing app with Nuxt & Cloudflare R2"](https://hub.nuxt.com/blog/drawing-app-with-nuxt-and-cloudflare-r2) blog post. :: ## Cloudflare AI Pricing This is important to know how Cloudflare AI models are billed. Cloudflare free allocation allows anyone to use a total of 10,000 Neurons per day at no charge. Neurons are Cloudflare's way of measuring AI outputs across different models. To give you a sense of what you can accomplish with 10,000 Neurons, you can generate one of the following (or an equivalent mix): - 100-200 LLM responses - 500 translations - 500 seconds of speech-to-text audio - 10,000 text classifications - 1,500 - 15,000 embeddings Once you reach the free allocation, you can use the AI models with a pay-as-you-go model of $0.011 / 1,000 Neurons. ::tip **AI Models in beta are free to use**, at this time, all the models we use in this tutorial are in beta, **so you we use them for free**. :: ::note --- target: _blank to: https://developers.cloudflare.com/workers-ai/platform/pricing --- Read more about **Cloudflare AI pricing**. :: ## Add AI to Nuxt To add AI to our Nuxt application, we need to enable the AI feature in our `nuxt.config.ts` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { ai: true, // <--- Enable AI blob: true, } }) ``` Then, we want to make sure our project is linked to our NuxtHub account. ```bash [Terminal] npx nuxthub link ``` ::note This will allow the module to call AI models from your linked Cloudflare account in development mode. :: ::tip{icon="i-lucide-rocket"} That's it! We can now use the AI models from Cloudflare using [`hubAI()`](https://hub.nuxt.com/docs/features/ai). :: I've been thinking about two features: - Generate the alternative text for the user drawings, improving the accessibility of the application & SEO. - Generate an image based on the drawing and the alternative text, as a way to make the application more interactive. Before starting, I took a look at [Cloudflare's multi modal playground](https://multi-modal.ai.cloudflare.com/){rel="nofollow"} and played with some models. ![Atidraw AI models](https://hub.nuxt.com/images/blog/atidraw-ai-models.png){dataZoomSrc="/images/blog/atidraw-ai-models.png" height="515" width="915"} This guided me to the following models: - [LLaVA](https://developers.cloudflare.com/workers-ai/models/llava-1.5-7b-hf/){rel="nofollow"} for the alternative text generation - [Stable Diffusion IMG2IMG](https://developers.cloudflare.com/workers-ai/models/stable-diffusion-v1-5-img2img/){rel="nofollow"} for the alternative drawing generation ::note --- target: _blank to: https://developers.cloudflare.com/workers-ai/models/ --- See all **Cloudflare AI models** available. :: ## Image Alternative Text To generate the alternative text for the user drawings, we use the [LLaVA](https://developers.cloudflare.com/workers-ai/models/llava-1.5-7b-hf/){rel="nofollow"} model. Let's see our current `/api/upload` route: ```ts [server/api/upload.post.ts] export default eventHandler(async (event) => { // Make sure the user is authenticated to upload const { user } = await requireUserSession(event) // Read the form data const form = await readFormData(event) const drawing = form.get('drawing') as File // Make sure the drawing is a jpeg image and is not larger than 1MB ensureBlob(drawing, { maxSize: '1MB', types: ['image/jpeg'] }) // Create a new pathname to be smaller than the last one uploaded (for a desc ordering) const name = `${new Date('2050-01-01').getTime() - Date.now()}` // Store the image in the R2 bucket with the `drawings/` prefix return hubBlob().put(`${name}.jpg`, drawing, { prefix: 'drawings/', addRandomSuffix: true, customMetadata: { userProvider: user.provider, userId: user.id, userName: user.name, userAvatar: user.avatar, userUrl: user.url, }, }) }) ``` The `LLaVA` model is expecting a `Uint8Array` of the image and a `prompt` to generate the alternative text: ```ts const { description } = await hubAI().run('@cf/llava-hf/llava-1.5-7b-hf', { prompt: 'Describe this drawing in one sentence.', // Convert the drawing from File to Uint8Array image: [...new Uint8Array(await drawing.arrayBuffer())], })) ``` We can add the description to the custom metadata of the drawing: ```diff [server/api/upload.post.ts] export default eventHandler(async (event) => { // ... + const { description } = await hubAI().run('@cf/llava-hf/llava-1.5-7b-hf', { + prompt: 'Describe this drawing in one sentence.', + image: [...new Uint8Array(await drawing.arrayBuffer())], + })) // ... return hubBlob().put(`${name}.jpg`, drawing, { // ... customMetadata: { // ... userUrl: user.url, + description }, }) }) ``` Lastly, we need to update our listing page to display the description in the `` tag but also in the `title` attribute so the user can see the description when hovering the image. ```vue [app/pages/index.vue] ``` Let's see the result: :video{controls className="lg:w-2/3,h-auto,border,dark:border-gray-800,rounded" controls="true" poster="https://res.cloudinary.com/nuxt/video/upload/v1724609254/nuxthub/nuxt-ai-img-alt-text_zv0sx7.jpg"} ## Alternative Drawing To generate a new drawing based from the drawing and the alternative text, we need to use the [Stable Diffusion IMG2IMG](https://developers.cloudflare.com/workers-ai/models/stable-diffusion-v1-5-img2img/){rel="nofollow"} model. ```ts await hubAI().run('@cf/runwayml/stable-diffusion-v1-5-img2img', { image: imageAsUint8Array, prompt: imageAltText, guidance: 8, strength: 0.5, }) ``` It takes the following inputs: - `image` (as `Uint8Array`) to use as a reference for the image generation - `prompt` to guide the model in generating the image - `guidance` to control how closely the generated image adheres to the given text prompt - `strength` to control how much the model changes the input image, with higher values creating bigger changes. Let's update our `/api/upload` route to generate the alternative drawing and store it our R2 bucket. ```ts [server/api/upload.post.ts] export default eventHandler(async (event) => { // ... const aiImage = await hubAI().run('@cf/runwayml/stable-diffusion-v1-5-img2img', { prompt: description || 'A drawing', guidance: 8, strength: 0.5, image: [...new Uint8Array(await drawing.arrayBuffer())], }) .then((blob: Blob | Uint8Array) => { // Convert Uint8Array to Blob if (blob instanceof Uint8Array) { blob = new Blob([blob]) } // Store the image in the R2 bucket with the `ai/` prefix return hubBlob().put(`${name}.png`, blob, { prefix: 'ai/', addRandomSuffix: true, contentType: 'image/png', }) }) // ... return hubBlob().put(`${name}.jpg`, drawing, { // ... customMetadata: { // ... aiImage: aiImage.pathname, }, }) }) ``` As you can see, we store the `pathname` of the AI generated image in the custom metadata of the drawing. Now, we can display the AI generated image in our listing page when hovering the user's drawing: ```vue [app/pages/index.vue] ``` I am quite happy with the result: :video{controls className="lg:w-2/3,h-auto,border,dark:border-gray-800,rounded" controls="true" poster="https://res.cloudinary.com/nuxt/video/upload/v1724609261/nuxthub/nuxt-ai-generate-img_tdnyfq.jpg"} ::note Sometimes the AI generated image is black, this is because the model is not able to generate an image from the description, most of the time because it is sensitive content or it misunderstood the description. :: ## Conclusion This is the end of this tutorial on how to use Cloudflare AI models in a Nuxt application. I hope you enjoyed it and that it gave you some ideas on how to use AI for improving accessibility, SEO or User Experience. Feel free to expand on this foundation and add your own unique features to make Atidraw yours! ::callout --- icon: i-simple-icons-github target: _blank to: https://github.com/atinux/atidraw --- The source code of the app is available at **github.com/atinux/atidraw**. :: ::note{icon="i-lucide-rocket" target="_blank" to="https://draw.nuxt.dev"} The demo is available at **draw\.nuxt.dev**. :: If you prefer, you can also deploy this project on your Cloudflare account by clicking on the button below: [![Deploy to NuxtHub](https://hub.nuxt.com/button.svg){height="32" width="174"}](https://hub.nuxt.com/new?repo=atinux/atidraw){rel="nofollow"} Happy coding & drawing! # How to use Libsodium in Cloudflare Workers While working on the NuxtHub's GitHub app and action, we decided to synchronise the environment variables and secrets to the repository so both the runtime and build environments would be similar. ## The problem When creating repository secrets with the GitHub REST API, you need to [encrypt them using the Libsodium library](https://docs.github.com/en/rest/guides/encrypting-secrets-for-the-rest-api?apiVersion=2022-11-28){rel="nofollow"}. ```ts import libsodium from 'libsodium-wrappers' // Check if libsodium is ready and then proceed. await sodium.ready const publicKey = 'repositoryProductionPublicKeyAsBase64' // Convert base64 key to Uint8Array const binaryPublicKey = Uint8Array.from(Buffer.from(publicKey.data.key, 'base64')) // Convert string value to Uint8Array const binarySecretValue = new TextEncoder().encode('my-secret-value') // Encrypt with libsodium const encryptedBytes = libsodium.crypto_box_seal(binaryValue, binaryKey) // Convert to base64 const encryptedBase64 = Buffer.from(encryptedBytes).toString('base64') // Call GitHub API with the encryptedBase64 value ``` By trying the [libsodium-wrappers](https://github.com/jedisct1/libsodium.js){rel="nofollow"} NPM library, we quickly hit the [TypeError: Cannot read properties of undefined (reading 'href')](https://github.com/jedisct1/libsodium.js/issues/323){rel="nofollow"} error when awaiting `.ready` . We decided to not hack around it like [suggested here](https://github.com/jedisct1/libsodium.js/issues/212#issuecomment-1181238495){rel="nofollow"}. ## Inside Libsodium: NaCl I decided to checkout the [Libsodium documentation]() which mentions that the library is a portable, cross-compilable, installable, and package-able fork of [NaCl](https://nacl.cr.yp.to/){rel="nofollow"}, with a compatible but extended API to improve usability even further. As we only use the `crypto_box_seal` function from Libsodium, I believed that we could directly use a compatible NaCl library that would work on edge runtimes. This is how I found the [TweetNaCl](https://tweetnacl.js.org/){rel="nofollow"} library that works perfectly in Cloudflare Workers. One problem: `tweetnacl` does not have a `crypto_box_seal` method, so what to do? ## Learning about Sealed Boxes Libsodium did a great job explaining the [Sealed boxes](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes){rel="nofollow"} concept. ::callout Sealed boxes are designed to anonymously send messages to a recipient given their public key. :br :br Only the recipient can decrypt these messages using their private key. While the recipient can verify the integrity of the message, they cannot verify the identity of the sender. :br :br A message is encrypted using an ephemeral key pair, with the secret key being erased right after the encryption process.\:br :br Without knowing the secret key used for a given message, the sender cannot decrypt the message later. Furthermore, without additional data, a message cannot be correlated with the identity of its sender. :: The documentation also gives us the algorithm implementation details: ```text ephemeral_pk ‖ box(m, recipient_pk, ephemeral_sk, nonce=blake2b(ephemeral_pk ‖ recipient_pk)) ``` We can re-create the crypto box seal algorithm with NaCL: 1. Generate an ephemeral key pair using `nacl.box.keyPair()` 2. Derive the nonce using Blake2b cryptographic hash function 3. Encrypt the message with `nacl.box()` by combining the ephemeral secret key and the repository public key 4. Combine the ephemeral public key and our encrypted message ## The Solution To create our edge-compatible `crypto_box_seal` function using `tweetnacl` and `blakejs`. First, we need to install the dependencies: ```bash [Terminal] npm i tweetnacl blakejs ``` Then create the `cryptoBoxSeal` and `deviceNonce` methods in a Nuxt server util: ```ts [server/utils/crypto.ts] import nacl from 'tweetnacl' import { blake2b } from 'blakejs' // Function to derive the nonce using Blake2b function deriveNonce(ephemeralPublicKey: Uint8Array, recipientPublicKey: Uint8Array) { // Concatenate ephemeralPublicKey ‖ recipientPublicKey const input = new Uint8Array( ephemeralPublicKey.length + recipientPublicKey.length ) input.set(ephemeralPublicKey, 0) input.set(recipientPublicKey, ephemeralPublicKey.length) // Derive the nonce using Blake2b return blake2b(input, undefined, nacl.box.nonceLength) } export function cryptoBoxSeal(message: Uint8Array, recipientPublicKey: Uint8Array) { // Generate ephemeral key pair const ephemeralKeyPair = nacl.box.keyPair() // Derive the nonce using Blake2b const nonce = deriveNonce(ephemeralKeyPair.publicKey, recipientPublicKey) // Encrypt the message using the ephemeral secret key and recipient's public key const encryptedMessage = nacl.box( message, nonce, recipientPublicKey, ephemeralKeyPair.secretKey ) // Combine the ephemeral public key and encrypted message const sealedBox = new Uint8Array( ephemeralKeyPair.publicKey.length + encryptedMessage.length ) // Similar signature from libsodium // ephemeral_pk ‖ box(m, recipient_pk, ephemeral_sk, nonce=blake2b(ephemeral_pk ‖ recipient_pk)) sealedBox.set(ephemeralKeyPair.publicKey, 0) sealedBox.set(encryptedMessage, ephemeralKeyPair.publicKey.length) return sealedBox } ``` Finally, we can replace our example above to use our new helper: ```diff [example.ts] - // Encrypt with libsodium - const encryptedBytes = libsodium.crypto_box_seal(binaryValue, binaryKey) + // Encrypt using cryptoBoxSeal + const encryptedBytes = cryptoBoxSeal(binaryValue, binaryPublicKey) ``` That's it! This was a good opportunity to learn more on how encrypting secrets work for GitHub and not be afraid on diving into how librairies work under the hood.