# What is NuxtHub? NuxtHub aims to provide a complete backend experience on top of the framework that can deploy on many cloud providers, allowing developers to build full-stack applications. To be widely-compatible with many cloud providers, NuxtHub leverages agnostic design of Nuxt, Nitro and unstorage, db0 and more. ## Features NuxtHub provides optional features to help you build full-stack applications: ::card-group :::card --- icon: i-lucide-database title: SQL database to: https://hub.nuxt.com/docs/database --- Store your application's data in a SQL database. ::: :::card{icon="i-lucide-shapes" title="Blob" to="https://hub.nuxt.com/docs/blob"} Store static assets, such as images, videos and more ::: :::card{icon="i-lucide-zap" title="Cache" to="https://hub.nuxt.com/docs/cache"} Caching system for your Nuxt pages, API routes or server functions ::: :::card --- icon: i-lucide-list title: Key-Value to: https://hub.nuxt.com/docs/kv --- Key-Value to store JSON data accessible ::: :: ## Nuxt DevTools NuxtHub also integrates with the [Nuxt DevTools](https://devtools.nuxt.com/){rel=""nofollow""} to provide a complete development experience. ::tabs :::tabs-item{label="Database"} ![Nuxt DevTools Database](https://hub.nuxt.com/images/landing/nuxt-devtools-database.png){height="515" width="915"} ::: :: # 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. :: ## 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`. That's it! You can now use NuxtHub features in your Nuxt project. ::note NuxtHub will create a `.data` 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="blob" type="boolean | BlobConfig"} Default to `false` \- Enables blob storage to store static assets, such as images, videos and more. Set to `true` for auto-configuration based on your hosting provider (Cloudflare R2, Vercel Blob, AWS S3, etc.), or provide a `BlobConfig` object with `driver` and driver-specific options. ::: :::field{name="cache" type="boolean | CacheConfig"} Default to `false` \- Enables cache storage to cache your server route responses or functions using Nitro's `cachedEventHandler` and `cachedFunction` . Set to `true` for auto-configuration based on your hosting provider, or provide a `CacheConfig` object with `driver` and driver-specific options. ::: :::field{name="db" type="'postgresql' | 'sqlite' | 'mysql' | DatabaseConfig"} Default to `false` \- Enables SQL database to store your application's data. Set to a dialect string ( `'postgresql'` , `'sqlite'` , or `'mysql'` ) for zero-config setup, or provide a `DatabaseConfig` object with `dialect` , `driver` , and `connection` details. ::: :::field{name="kv" type="boolean | KVConfig"} Default to `false` \- Enables Key-Value storage to store JSON data. Set to `true` for auto-configuration based on your hosting provider (Cloudflare KV, Vercel Redis, Deno KV), or provide a `KVConfig` object with `driver` and driver-specific options. ::: :::field{name="dir" type="string"} Default to `'.data'` \- The directory used for storage (database, kv, cache, etc.) during local development. ::: :: ::tip{icon="i-lucide-rocket"} You're all set! Now, let's dive into [deploying it](https://hub.nuxt.com/docs/getting-started/deploy) . :: # Deploy Nuxt on a cloud provider ## Vercel ::tip You can deploy your NuxtHub project on Vercel with almost no configuration. :: You can use the Vercel Marketplace to add compatible storage to your project (Postgres, Turso, Redis, Vercel Blob, etc.). ::callout{to="https://nuxt.com/deploy/vercel"} Read more about deploying on Vercel on the Nuxt documentation. :: ## Cloudflare You can deploy your project on your own Cloudflare account, you need to create the necessary resources in your 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. :: ### Configure Bindings NuxtHub auto-generates the `wrangler.json` file at build time when you provide resource IDs in your config. No manual `wrangler.jsonc` is required. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { // D1 database db: { dialect: 'sqlite', driver: 'd1', connection: { databaseId: '' } }, // KV namespace (binding defaults to 'KV') kv: { driver: 'cloudflare-kv-binding', namespaceId: '' }, // Cache KV namespace (binding defaults to 'CACHE') cache: { driver: 'cloudflare-kv-binding', namespaceId: '' }, // R2 bucket (binding defaults to 'BLOB') blob: { driver: 'cloudflare-r2', bucketName: '' } } }) ``` ::tip See a working example at [onmax/repros/nuxthub-716](https://github.com/onmax/repros/tree/main/nuxthub-716){rel=""nofollow""} — deployed without a `wrangler.toml` file. :: #### Config Mapping Reference The following table shows how NuxtHub configuration options map to Wrangler configuration: | NuxtHub Config | Wrangler Key | Generated Value | | ------------------------------ | ---------------------------- | ----------------- | | `hub.db.connection.databaseId` | `d1_databases[].database_id` | From config | | - | `d1_databases[].binding` | `DB` (hardcoded) | | `hub.kv.namespaceId` | `kv_namespaces[].id` | From config | | `hub.kv.binding` | `kv_namespaces[].binding` | `KV` (default) | | `hub.cache.namespaceId` | `kv_namespaces[].id` | From config | | `hub.cache.binding` | `kv_namespaces[].binding` | `CACHE` (default) | | `hub.blob.bucketName` | `r2_buckets[].bucket_name` | From config | | `hub.blob.binding` | `r2_buckets[].binding` | `BLOB` (default) | #### Environment Resolution During the build process, NuxtHub resolves the target environment using the following steps: 1. Checks the `CLOUDFLARE_ENV` environment variable 2. If set, merges environment-specific configuration from `wrangler.jsonc` 3. Generates `.output/server/wrangler.json` with the resolved bindings 4. Wrangler uses this configuration during deployment ### Deploy Create a [Cloudflare Workers project](https://dash.cloudflare.com/?to=/\:account/workers-and-pages/create){rel=""nofollow""} and link your GitHub or GitLab repository. NuxtHub auto-configures bindings from your `nuxt.config.ts` during build. ::warning{to="https://hub.nuxt.com/docs/guides/ci-cd#d1-migrations-in-cicd"} For Cloudflare D1, NuxtHub also generates `migrations_table` and `migrations_dir` in `.output/server/wrangler.json` . This enables Wrangler-managed migrations, but `wrangler deploy` and Cloudflare Git / Workers Builds do not apply D1 migrations automatically. Run a separate migration step before deploy. :: ::collapsible{title="Alternative: Manual wrangler.jsonc"} You can also create a `wrangler.jsonc` file manually in the root of your project: ```jsonc [wrangler.jsonc] { "$schema": "node_modules/wrangler/config-schema.json", // D1 database "d1_databases": [ { "binding": "DB", "database_name": "", "database_id": "" } ], // R2 bucket "r2_buckets": [ { "binding": "BLOB", "bucket_name": "" } ], // KV namespaces "kv_namespaces": [ { "binding": "KV", "id": "" }, { "binding": "CACHE", "id": "" } ] } ``` :: ### Environments Wrangler supports [environments](https://developers.cloudflare.com/workers/wrangler/environments/){rel=""nofollow""} to manage different configurations for staging, preview, or other deployment targets. You can define environment-specific bindings in your `wrangler.jsonc`: ```jsonc [wrangler.jsonc] { "$schema": "node_modules/wrangler/config-schema.json", // Production bindings (top-level) "d1_databases": [ { "binding": "DB", "database_id": "" } ], // Preview environment "env": { "preview": { "d1_databases": [ { "binding": "DB", "database_id": "" } ] } } } ``` To deploy to a specific environment, set the `CLOUDFLARE_ENV` environment variable during build: ```bash [Terminal] CLOUDFLARE_ENV=preview nuxt build ``` #### Non-Inheritable Keys The following bindings do **not** inherit from the top-level configuration and must be specified explicitly in each environment: | Key | Description | | ----------------- | ----------------------- | | `d1_databases` | D1 database bindings | | `kv_namespaces` | KV namespace bindings | | `r2_buckets` | R2 bucket bindings | | `vars` | Environment variables | | `durable_objects` | Durable Object bindings | | `services` | Service bindings | ::warning If you forget to specify bindings in an environment, they will be `undefined` at runtime. This does not produce an error during deployment, which can lead to unexpected behavior in production. :: ::callout{to="https://developers.cloudflare.com/workers/wrangler/environments/"} Learn more about Wrangler environments in the Cloudflare documentation. :: ### Cache When self-hosting and using devtools for cache management (viewing/deleting cache entries), you need to configure additional environment variables: In your Cloudflare project, go to **Settings** → **Environment Variables** and add: ::field-group :::field{name="NUXT_HUB_CLOUDFLARE_ACCOUNT_ID"} Your Cloudflare account ID (if not already set for blob storage) ::: :::field{name="NUXT_HUB_CLOUDFLARE_API_TOKEN"} Your Cloudflare API token with **Workers KV Storage Read and Write** permissions (if not already set for blob storage) ::: :::field{name="NUXT_HUB_CLOUDFLARE_CACHE_NAMESPACE_ID"} Your KV namespace ID for the cache storage (found in Cloudflare Dashboard → Workers & Pages → KV) ::: :: ::note These environment variables are only required if you use the Nuxt DevTools for cache management. Normal cache operations (reading/writing cache) work directly through the KV binding. :: ## Workers Builds If deploying to Cloudflare Workers, you can use [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/){rel=""nofollow""} to automatically deploy your project on every commit. ::note{to="https://hub.nuxt.com/docs/guides/ci-cd#d1-migrations-in-cicd"} If your project uses Cloudflare D1, add an explicit migration step before deploy. Workers Builds does not apply D1 migrations automatically. :: ## Pages CI If deploying to Cloudflare Pages, you can use [Pages CI](https://developers.cloudflare.com/pages/configuration/git-integration/github-integration/){rel=""nofollow""} to automatically deploy your project on every commit. # NuxtHub Admin (Deprecated) ::warning NuxtHub Admin is being sunset on 31st December 2025. We recommend switching to self-hosting and using Pages CI, Workers CI or Wrangler to deploy. :: 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. ## 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 (Deprecated) 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{.w-full.h-auto.rounded controls 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 (Deprecated) 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 permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 24 cache: 'pnpm' - name: Install dependencies run: pnpm install - name: Build & Deploy to NuxtHub uses: nuxt-hub/action@v2 ``` :: ### 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{name="directory" type="string"} The directory of the Nuxt application to build. ::: :::field{default="dist" name="output-directory"} The output directory containing the built Nuxt application to deploy, relative to the directory specified in `directory` . ::: :::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) ::: :::field{name="environment-url" type="string"} The permanent URL of the environment. Examples: - {rel=""nofollow""} ('staging' environment) ::: :: ### Environment Variables & Secrets The NuxtHub GitHub Action builds the Nuxt application using build-time environment variables and secrets specified within NuxtHub Admin. ::tip You can scope variables to only be available during build-time, This is useful in cases such as `NUXT_UI_PRO_LICENSE` . :: ### 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 (Deprecated) 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. :: # Migrating from v0.9 to v0.10 NuxtHub v0.10 introduces a significant shift toward **multi-cloud support** and **self-hosting**, moving away from the Cloudflare-exclusive approach of older versions. This guide will help you migrate your existing v0.9 project to v0.10. ::important This guide covers migration from **v0.9 to v0.10** . If you're using the NuxtHub Admin migration wizard, note that some instructions may reference older versions and can be disregarded in favor of this documentation. :: ## Overview of Changes ::callout You can visit [legacy.hub.nuxt.com](https://legacy.hub.nuxt.com){rel=""nofollow""} to read the documentation for **v0.9** . :: ### Philosophy Shift - **Multi-cloud first**: NuxtHub now supports Cloudflare, Vercel, AWS, and other providers - **Self-hosting recommended**: Direct integration with your cloud provider instead of NuxtHub Admin (sunset Dec 31, 2025) - **Drizzle ORM integration**: Type-safe database access with PostgreSQL, MySQL, and SQLite support - **Cloud-agnostic storage**: Blob, KV, and Cache work across multiple providers ### Breaking Changes Summary | Feature | v0.x | v0.10 | | ------------------ | -------------------- | ---------------------------------------------------------------------- | | Database config | `hub.database: true` | `hub.db: 'sqlite'` (or `'postgresql'`, `'mysql'`) | | Database directory | `server/database/` | `server/db/` | | Database access | `hubDatabase()` | `db` from `hub:db` (Drizzle ORM) | | Blob access | `hubBlob()` | `blob` from `hub:blob` | | KV access | `hubKV()` | `kv` from `hub:kv` | | AI & AutoRAG | `hubAI()` | Removed (use [AI SDK](https://ai-sdk.dev){rel=""nofollow""}) | | NuxtHub Admin | Supported | Deprecated (sunset Dec 31, 2025) | | `nuxthub deploy` | Supported | Deprecated (sunset Jan 31, 2026) | ## Configuration Migration ### Database The `database` option has been renamed to `db` and now requires specifying the SQL dialect: ```diff [nuxt.config.ts] export default defineNuxtConfig({ hub: { - database: true + db: 'sqlite' // or 'postgresql', 'mysql' } }) ``` For advanced configuration with explicit driver and connection details: ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'postgresql', driver: 'postgres-js', // Optional: auto-detected from env vars connection: { connectionString: process.env.DATABASE_URL } } } }) ``` ### Directory Structure Move your database files from `server/database/` to `server/db/`: ```bash # Move schema files mv server/database/schema.ts server/db/schema.ts # Move migrations mv server/database/migrations/ server/db/migrations/ ``` ### Blob, KV & Cache These features remain largely the same but now support multiple providers: ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { blob: true, // Auto-configures based on provider kv: true, // Auto-configures based on provider cache: true // Auto-configures based on provider } }) ``` ## Code Migration ### Database Access Replace `hubDatabase()` with Drizzle ORM: **Before (v0.x):** ```ts export default eventHandler(async () => { const db = hubDatabase() const users = await db.prepare('SELECT * FROM users').all() return users.results }) ``` **After (v0.10):** ```ts import { db, schema } from 'hub:db' export default eventHandler(async () => { return await db.select().from(schema.users) }) ``` ::tip The `db` instance is auto-imported on server-side, so you can use it directly without importing. :: ### Schema Definition Create your schema using Drizzle ORM syntax: ::tabs{sync="database-dialect"} :::tabs-item{icon="i-simple-icons-sqlite" label="SQLite"} ```ts [server/db/schema.ts] import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core' export const users = sqliteTable('users', { id: integer().primaryKey({ autoIncrement: true }), name: text().notNull(), email: text().notNull().unique(), createdAt: integer({ mode: 'timestamp' }).notNull() }) ``` ::: :::tabs-item{icon="i-simple-icons-postgresql" label="PostgreSQL"} ```ts [server/db/schema.ts] import { pgTable, text, serial, timestamp } from 'drizzle-orm/pg-core' export const users = pgTable('users', { id: serial().primaryKey(), name: text().notNull(), email: text().notNull().unique(), createdAt: timestamp().notNull().defaultNow() }) ``` ::: :::tabs-item{icon="i-simple-icons-mysql" label="MySQL"} ```ts [server/db/schema.ts] import { mysqlTable, text, serial, timestamp } from 'drizzle-orm/mysql-core' export const users = mysqlTable('users', { id: serial().primaryKey(), name: text().notNull(), email: text().notNull().unique(), createdAt: timestamp().notNull().defaultNow() }) ``` ::: :: Then generate migrations: ```bash [Terminal] npx nuxt db generate ``` ### Blob Access Replace `hubBlob()` with the new `blob` import: **Before (v0.x):** ```ts export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) return hubBlob().serve(event, pathname) }) ``` **After (v0.10):** ```ts import { blob } from 'hub:blob' export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) return blob.serve(event, pathname) }) ``` ### KV Access Replace `hubKV()` with the new `kv` import: **Before (v0.x):** ```ts export default eventHandler(async () => { const kv = hubKV() return await kv.get('my-key') }) ``` **After (v0.10):** ```ts import { kv } from 'hub:kv' export default eventHandler(async () => { return await kv.get('my-key') }) ``` ## Self-Hosting Setup With NuxtHub v0.10, you deploy your project directly to your cloud provider using their tooling. ::note Since v0.10.3, NuxtHub auto-generates Cloudflare bindings from your `nuxt.config.ts` at build time. Manual `wrangler.jsonc` configuration is optional. :: ### Cloudflare NuxtHub supports two approaches for Cloudflare deployment. Choose based on your preference. #### Option A: Auto-Configuration (Recommended) NuxtHub auto-generates the `wrangler.json` file at build time when you provide resource IDs in your config. No manual `wrangler.jsonc` is required. 1. Create the necessary resources in your [Cloudflare dashboard](https://dash.cloudflare.com){rel=""nofollow""}: - [D1 Database](https://dash.cloudflare.com/?to=/\:account/workers/d1){rel=""nofollow""} (if using `hub.db`) - [KV Namespace](https://dash.cloudflare.com/?to=/\:account/workers/kv/namespaces){rel=""nofollow""} (if using `hub.kv` or `hub.cache`) - [R2 Bucket](https://dash.cloudflare.com/?to=/\:account/r2/new){rel=""nofollow""} (if using `hub.blob`) 2. Configure bindings in `nuxt.config.ts`: ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { // D1 database db: { dialect: 'sqlite', driver: 'd1', connection: { databaseId: '' } }, // KV namespace (binding defaults to 'KV') kv: { driver: 'cloudflare-kv-binding', namespaceId: '' }, // Cache KV namespace (binding defaults to 'CACHE') cache: { driver: 'cloudflare-kv-binding', namespaceId: '' }, // R2 bucket (binding defaults to 'BLOB') blob: { driver: 'cloudflare-r2', bucketName: '' } } }) ``` ::warning{to="https://hub.nuxt.com/docs/guides/ci-cd#d1-migrations-in-cicd"} For Cloudflare D1, NuxtHub generates the Wrangler migration metadata for you, but `wrangler deploy` and Cloudflare Git / Workers Builds do not apply D1 migrations automatically. Add a separate migration step before deploy. :: 3. Deploy using [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/){rel=""nofollow""} or [Pages CI](https://developers.cloudflare.com/pages/configuration/git-integration/github-integration/){rel=""nofollow""}. If you use D1, follow the [CI/CD D1 migration workflow](https://hub.nuxt.com/docs/guides/ci-cd#d1-migrations-in-cicd). #### Option B: Manual wrangler.jsonc Alternatively, you can create a `wrangler.jsonc` file manually in your project root: ```jsonc [wrangler.jsonc] { "$schema": "node_modules/wrangler/config-schema.json", "d1_databases": [ { "binding": "DB", "database_name": "", "database_id": "" } ], "r2_buckets": [ { "binding": "BLOB", "bucket_name": "" } ], "kv_namespaces": [ { "binding": "KV", "id": "" }, { "binding": "CACHE", "id": "" } ] } ``` ::callout{to="https://hub.nuxt.com/docs/getting-started/deploy#cloudflare"} See the deployment documentation for more details on Cloudflare configuration, environments, and CI/CD setup. :: ### Vercel 1. Install required packages based on features used: ```bash [Terminal] # For blob storage npm install @vercel/blob # For KV/Cache (Upstash Redis) npm install @upstash/redis # For PostgreSQL database npm install drizzle-orm drizzle-kit postgres @electric-sql/pglite ``` 2. Add storage from the [Vercel dashboard](https://vercel.com){rel=""nofollow""} → Project → Storage 3. Deploy as usual - NuxtHub auto-detects Vercel environment variables ## Deprecated Features The following features have been removed in v0.10 as part of the multi-cloud strategy: ### AI & AutoRAG Use the [AI SDK](https://ai-sdk.dev){rel=""nofollow""} with the [Workers AI Provider](https://ai-sdk.dev/providers/community-providers/cloudflare-workers-ai){rel=""nofollow""} instead: ```ts // Before (v0.x) const ai = hubAI() const result = await ai.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hello' }) // After (v0.10) - use AI SDK import { generateText } from 'ai' import { workersai } from 'workers-ai-provider' const result = await generateText({ model: workersai('@cf/meta/llama-2-7b-chat-int8'), prompt: 'Hello' }) ``` ### Browser (Puppeteer) Access Cloudflare's Browser Rendering directly via `process.env.BROWSER` binding. ### Vectorize Use Cloudflare's Vectorize binding directly or consider alternatives like [Pinecone](https://www.pinecone.io/){rel=""nofollow""} or [Weaviate](https://weaviate.io/){rel=""nofollow""}. ### NuxtHub CLI Replace `npx nuxthub deploy` with your provider's deployment method: - **Cloudflare**: Use `wrangler deploy` or Workers/Pages CI - **Vercel**: Use `vercel deploy` or Git integration - **Other**: Use your provider's CLI or CI/CD ## Migration Checklist - Update `nuxt.config.ts`: Change `database: true` to `db: ''` - Move files from `server/database/` to `server/db/` - Install Drizzle ORM and appropriate database driver - Update schema files to use Drizzle ORM syntax - Generate migrations with `npx nuxt db generate` - Replace `hubDatabase()` calls with `db` from `hub:db` - Replace `hubBlob()` calls with `blob` from `hub:blob` - Replace `hubKV()` calls with `kv` from `hub:kv` - Remove AI/AutoRAG usage or migrate to AI SDK - For Cloudflare: Configure resource IDs in `nuxt.config.ts` (v0.10.3+) OR create manual `wrangler.jsonc` - For Vercel: Add storage from dashboard and install required packages - Update CI/CD from NuxtHub GitHub Action to provider's deployment (Workers/Pages CI, Vercel Git integration, etc.) ## Getting Help If you encounter issues during migration: - Use the [Claude Code migration skill](https://github.com/onmax/nuxthub-v1-skill){rel=""nofollow""} for AI-assisted migration - Check the [NuxtHub documentation](https://hub.nuxt.com/docs){rel=""nofollow""} - Join the [Nuxt Discord](https://discord.com/invite/nuxt){rel=""nofollow""} for community support - Open an issue on [GitHub](https://github.com/nuxt-hub/core/issues){rel=""nofollow""} # Environments NuxtHub supports multiple deployment environments, each with isolated resources such as databases, KV stores, and buckets. This guide explains how to configure environments across different hosting providers. ## Environment Types | Environment | Purpose | Trigger | | -------------- | ------------------------------------------ | ---------------------------------- | | **Production** | Live application serving end users | Push to main branch | | **Preview** | Testing pull requests and feature branches | Push to non-main branches | | **Staging** | Pre-production testing environment | Named environment in configuration | | **Local** | Development on your machine | Running `nuxt dev` | ## Local Development During local development, NuxtHub stores data in the `.data/` directory: | Resource | Local Path | | ---------- | -------------------- | | SQLite/D1 | `.data/db/sqlite.db` | | PostgreSQL | `.data/db/` | | KV | `.data/kv/` | | Blob | `.data/blob/` | | Cache | `.data/cache/` | ::tip The `.data/` directory is included in `.gitignore` by default in Nuxt projects. :: ### Connecting to Remote Resources To develop against production or preview data, set environment variables that point to your remote resources: ```bash [.env.local] # Connect to a remote Turso database TURSO_DATABASE_URL=libsql://.turso.io TURSO_AUTH_TOKEN= # Or connect to a remote PostgreSQL database POSTGRES_URL=postgresql://:@/ ``` ::warning Connecting to production databases during development risks accidental data modifications. Use a preview or staging environment instead. :: ### Cloudflare Dev Emulation Nitro runs Cloudflare's local dev emulation when you use the `cloudflare_module` preset. This replaces NuxtHub's default local storage and requires a `wrangler.jsonc` with bindings: ```jsonc [wrangler.jsonc] { "$schema": "node_modules/wrangler/config-schema.json", "d1_databases": [{ "binding": "DB" }], "kv_namespaces": [{ "binding": "KV" }, { "binding": "CACHE" }], "r2_buckets": [{ "binding": "BLOB" }] } ``` ::note Without this file, Nitro throws binding errors such as `R2 binding "BLOB" not found` or `D1 binding "DB" not found` . :: ::tip Cloudflare dev emulation is only used when you explicitly set `nitro.preset: 'cloudflare_module'` in your `nuxt.config.ts` . :: ## Production Environments ::tabs{sync="provider"} :::tabs-item{icon="i-simple-icons-cloudflare" label="Cloudflare"} Cloudflare Workers supports [named environments](https://developers.cloudflare.com/workers/wrangler/environments/){rel=""nofollow""} for managing staging, preview, and other deployment targets. ### Configuration Define environment-specific bindings in your `wrangler.jsonc` file: ```jsonc [wrangler.jsonc] { "$schema": "node_modules/wrangler/config-schema.json", // Production configuration (default) "d1_databases": [ { "binding": "DB", "database_id": "" } ], "kv_namespaces": [ { "binding": "KV", "id": "" } ], "r2_buckets": [ { "binding": "BLOB", "bucket_name": "" } ], // Named environments "env": { "preview": { "d1_databases": [ { "binding": "DB", "database_id": "" } ], "kv_namespaces": [ { "binding": "KV", "id": "" } ], "r2_buckets": [ { "binding": "BLOB", "bucket_name": "" } ] }, "staging": { "d1_databases": [ { "binding": "DB", "database_id": "" } ], "kv_namespaces": [ { "binding": "KV", "id": "" } ], "r2_buckets": [ { "binding": "BLOB", "bucket_name": "" } ] } } } ``` ::::warning The `d1_databases` , `kv_namespaces` , `r2_buckets` , `vars` , `durable_objects` , and `services` properties are **non-inheritable** . You must specify them explicitly in each environment configuration. :::: ### Deploying to an Environment Set the `CLOUDFLARE_ENV` environment variable during the build process: ```bash [Terminal] # Deploy to the preview environment CLOUDFLARE_ENV=preview nuxt build # Deploy to the staging environment CLOUDFLARE_ENV=staging nuxt build ``` ::::note When `CLOUDFLARE_ENV` is empty or unset, the default production environment is used. :::: ::::callout{to="https://hub.nuxt.com/docs/guides/ci-cd#github-actions"} See the CI/CD guide for complete GitHub Actions workflows with environment handling. :::: ### Creating Resources Create separate resources for each environment using the Wrangler CLI: **D1 Databases:** ```bash [Terminal] wrangler d1 create my-app-production wrangler d1 create my-app-preview wrangler d1 create my-app-staging ``` **KV Namespaces:** ```bash [Terminal] wrangler kv namespace create KV wrangler kv namespace create KV --env preview wrangler kv namespace create KV --env staging ``` **R2 Buckets:** ```bash [Terminal] wrangler r2 bucket create my-app-production wrangler r2 bucket create my-app-preview wrangler r2 bucket create my-app-staging ``` ::::callout --- to: https://developers.cloudflare.com/workers/wrangler/environments/ --- Learn more about Wrangler environments in the Cloudflare documentation. :::: ::: :::tabs-item{icon="i-simple-icons-vercel" label="Vercel"} Vercel automatically manages environments based on branch and deployment type. ### Configuration Configure environment variables in the Vercel dashboard under **Project Settings → Environment Variables**: | Variable | Production | Preview | Development | | ----------------------- | --------------------- | ------------------ | ----------------- | | `DATABASE_URL` | Production connection | Preview connection | Local connection | | `BLOB_READ_WRITE_TOKEN` | Production token | Preview token | Development token | ::::tip Add storage through the [Vercel Marketplace](https://vercel.com/marketplace){rel=""nofollow""} . By default, marketplace integrations connect to all environments—create separate storage instances per environment for isolation. :::: ### Branch-Specific Variables For preview deployments, you can configure variables that apply only to specific branches: 1. Navigate to **Project Settings → Environment Variables** 2. Add a variable and select **Preview** 3. Click **Add Branch** to target specific branches ### CI/CD Vercel handles CI/CD automatically when you connect your repository. Every push triggers a deployment: - Pushes to the production branch deploy to production - Pushes to other branches create preview deployments ::::callout{to="https://vercel.com/docs/deployments/environments"} Learn more about Vercel environments in the Vercel documentation. :::: ::: :: # Nuxt SQL Database NuxtHub Database provides a type-safe SQL database powered by [Drizzle ORM](https://orm.drizzle.team){rel=""nofollow""}, supporting PostgreSQL, MySQL, and SQLite with smart detection and automatic migrations at build time. ## Getting started ::steps{level="3"} ### Install dependencies Install Drizzle ORM, Drizzle Kit, and the appropriate driver(s) for the database you are using: :::tabs{sync="database-dialect"} ::::tabs-item{icon="i-simple-icons-postgresql" label="PostgreSQL"} :pm-install{name="drizzle-orm drizzle-kit postgres @electric-sql/pglite"} :::::callout NuxtHub automatically detects your database connection using environment variables: - Uses `PGlite` (embedded PostgreSQL) if no environment variables are set. - Uses `postgres-js` driver if you set `DATABASE_URL`, `POSTGRES_URL`, or `POSTGRESQL_URL` environment variable. - Use `neon-http` driver with `@neondatabase/serverless` for [Neon](https://neon.com){rel=""nofollow""} serverless PostgreSQL. ::::: :::: ::::tabs-item{icon="i-simple-icons-mysql" label="MySQL"} :pm-install{name="drizzle-orm drizzle-kit mysql2"} :::::callout NuxtHub automatically detects your database connection using environment variables: - Uses `mysql2` driver if you set `DATABASE_URL` or `MYSQL_URL` environment variable. - Requires environment variable (no local fallback). ::::: :::: ::::tabs-item{icon="i-simple-icons-sqlite" label="SQLite"} :pm-install{name="drizzle-orm drizzle-kit @libsql/client"} :::::callout NuxtHub automatically detects your database connection using environment variables: - Uses `libsql` driver for [Turso](https://turso.tech){rel=""nofollow""} if you set `TURSO_DATABASE_URL` and `TURSO_AUTH_TOKEN` environment variables. - Uses `libsql` locally with file at `.data/db/sqlite.db` if no environment variables are set. ::::: :::::tip{to="https://hub.nuxt.com/docs/getting-started/deploy#cloudflare"} For Cloudflare D1, configure the database ID in your `nuxt.config.ts` and NuxtHub auto-generates the wrangler bindings. ::::: :::: ::: :::tip{to="https://hub.nuxt.com/#dockerkubernetes-deployments"} For containerized deployments, you can defer environment variables to runtime. ::: ### Set SQL dialect Enable the database in your `nuxt.config.ts` by setting the `db` property to your desired SQL dialect: :::tabs{sync="database-dialect"} ::::tabs-item{icon="i-simple-icons-postgresql" label="PostgreSQL"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: 'postgresql' } }) ``` :::: ::::tabs-item{icon="i-simple-icons-mysql" label="MySQL"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: 'mysql' } }) ``` :::: ::::tabs-item{icon="i-simple-icons-sqlite" label="SQLite"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: 'sqlite' } }) ``` :::: ::: ### Database schema Create your database schema with full TypeScript support using Drizzle ORM: :::tabs{sync="database-dialect"} ::::tabs-item{icon="i-simple-icons-postgresql" label="PostgreSQL"} ```ts [server/db/schema.ts] import { pgTable, text, serial, timestamp } from 'drizzle-orm/pg-core' export const users = pgTable('users', { id: serial().primaryKey(), name: text().notNull(), email: text().notNull().unique(), password: text().notNull(), avatar: text().notNull(), createdAt: timestamp().notNull().defaultNow(), }) ``` :::: ::::tabs-item{icon="i-simple-icons-mysql" label="MySQL"} ```ts [server/db/schema.ts] import { mysqlTable, text, serial, timestamp } from 'drizzle-orm/mysql-core' export const users = mysqlTable('users', { id: serial().primaryKey(), name: text().notNull(), email: text().notNull().unique(), password: text().notNull(), avatar: text().notNull(), createdAt: timestamp().notNull().defaultNow(), }) ``` :::: ::::tabs-item{icon="i-simple-icons-sqlite" label="SQLite"} ```ts [server/db/schema.ts] import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core' export const users = sqliteTable('users', { id: integer().primaryKey({ autoIncrement: true }), name: text().notNull(), email: text().notNull().unique(), password: text().notNull(), avatar: text().notNull(), createdAt: integer({ mode: 'timestamp' }).notNull(), }) ``` :::: ::: :::callout{to="https://hub.nuxt.com/docs/database/schema"} Learn more about defining the **database schema files** . ::: ### Generate migrations Generate the database migrations from your schema: ```bash [Terminal] npx nuxt db generate ``` This creates SQL migration files in `server/db/migrations/{dialect}/` which are automatically applied during deployment and development. :::tip{icon="i-lucide-rocket"} That's it! You can now start your development server and query your database using the `db` instance from `@nuxthub/db` . ::: :::important Make sure to run `npx nuxt db generate` to generate the database migrations each time you change your database schema and restart the development server. ::: :: ## Local development During local development, view and edit your database from [Nuxt DevTools](https://devtools.nuxt.com){rel=""nofollow""} using the [Drizzle Studio](https://orm.drizzle.team/drizzle-studio/overview){rel=""nofollow""}: ![Nuxt DevTools Database](https://hub.nuxt.com/images/landing/nuxt-devtools-database.png){height="515" width="915"} ::warning At the moment, Drizzle Studio does not support SQLite. :: ## Build-time Hooks ::field-group :::field --- name: "'hub:db:migrations:dirs'" type: "(dirs: string[]) => void | Promise" --- Add additional directories to scan for database migration files (.sql). ```ts [nuxt.config.ts] export default defineNuxtConfig({ hooks: { 'hub:db:migrations:dirs': (dirs) => { dirs.push('my-module/db/migrations') } } }) ``` ::: :::field --- name: "'hub:db:queries:paths'" type: "(queries: string[], dialect: string) => void | Promise" --- Add queries that are not tracked in the `_hub_migrations` table which are applied after the database migrations complete. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hooks: { 'hub:db:queries:paths': (queries, dialect) => { queries.push('my-module/db/queries') } } }) ``` ::: :::field --- name: "'hub:db:schema:extend'" type: "({ paths: string[], dialect: string }) => void | Promise" --- Extend the database schema with additional files. ```ts [modules/my-module/index.ts] import { createResolver, defineNuxtModule } from '@nuxt/kit' export default defineNuxtModule({ setup(options, nuxt) { const { resolve } = createResolver(import.meta.url) nuxt.hook('hub:db:schema:extend', ({ paths, dialect }) => { paths.push(resolve(`./db/schema/pages.${dialect}.ts`)) }) } }) ``` ::: :: ::note --- to: https://nuxt.com/docs/4.x/guide/going-further/hooks#nuxt-hooks-build-time --- Learn more about Nuxt server hooks on the **Nuxt documentation** . :: ## Advanced configuration For advanced use cases, you can explicitly configure the database connection: ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'postgresql', driver: 'postgres-js', // Optional: explicitly choose driver connection: { connectionString: process.env.DATABASE_URL } } } }) ``` ### Column casing Database model names often use `snake_case` conventions, while in TypeScript, it is common to use `camelCase` for naming models. To address this, you can use the `casing` option to automatically map camelCase JavaScript keys to snake\_case in the database: ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'postgresql', casing: 'snake_case' } } }) ``` This allows you to use camelCase in your TypeScript schema while Drizzle automatically maps them to snake\_case in the database: ```ts [server/db/schema.ts] import { pgTable, text, serial, timestamp } from 'drizzle-orm/pg-core' export const users = pgTable('users', { id: serial().primaryKey(), firstName: text().notNull(), // Maps to first_name in the database lastName: text().notNull(), // Maps to last_name in the database createdAt: timestamp().notNull().default(sql`CURRENT_TIMESTAMP`) // Maps to created_at }) ``` ::callout --- external: true to: https://orm.drizzle.team/docs/sql-schema-declaration#camel-and-snake-casing --- Learn more about [camel and snake casing](https://orm.drizzle.team/docs/sql-schema-declaration#camel-and-snake-casing){rel=""nofollow""} in Drizzle ORM. :: ### D1 over HTTP Use the `d1-http` driver to access a Cloudflare D1 database over HTTP when hosting on other platforms. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'sqlite', driver: 'd1-http' } } }) ``` This driver requires the following environment variables: | Variable | Description | | --------------------------------- | ------------------------------------------ | | `NUXT_HUB_CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID | | `NUXT_HUB_CLOUDFLARE_API_TOKEN` | A Cloudflare API token with D1 permissions | | `NUXT_HUB_CLOUDFLARE_DATABASE_ID` | The ID of your D1 database | ::callout{icon="i-lucide-info"} You can find your Cloudflare account ID and create API tokens in the [Cloudflare dashboard](https://dash.cloudflare.com){rel=""nofollow""} . The API token needs `D1:Edit` permissions. :: ### Neon Serverless Use the `neon-http` driver to connect to [Neon](https://neon.com){rel=""nofollow""} serverless PostgreSQL with HTTP protocol optimized for serverless environments. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'postgresql', driver: 'neon-http' } } }) ``` Install the required dependency: :pm-install{name="@neondatabase/serverless"} This driver requires the following environment variable: | Variable | Description | | -------------- | --------------------------------------------------------------------------- | | `DATABASE_URL` | Your Neon database connection string (or `POSTGRES_URL` / `POSTGRESQL_URL`) | ::callout{icon="i-lucide-info"} You can find your Neon connection string in the [Neon dashboard](https://console.neon.tech){rel=""nofollow""} . The connection string format is `postgresql://user:password@hostname/database` . :: ### Docker/Kubernetes Deployments Build container images without database credentials by deferring environment resolution to runtime. Set the driver explicitly and disable automatic migrations during build and dev: ::tabs{sync="database-dialect"} :::tabs-item{icon="i-simple-icons-sqlite" label="SQLite"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'sqlite', driver: 'libsql', applyMigrationsDuringBuild: false, applyMigrationsDuringDev: false } } }) ``` ::: :::tabs-item{icon="i-simple-icons-postgresql" label="PostgreSQL"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'postgresql', driver: 'postgres-js', // or 'neon-http' applyMigrationsDuringBuild: false, applyMigrationsDuringDev: false } } }) ``` ::: :::tabs-item{icon="i-simple-icons-mysql" label="MySQL"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'mysql', driver: 'mysql2', applyMigrationsDuringBuild: false, applyMigrationsDuringDev: false } } }) ``` ::: :: Set these environment variables at runtime: ::tabs{sync="database-dialect"} :::tabs-item{icon="i-simple-icons-sqlite" label="SQLite"} | Variable | Description | | -------------------- | ------------------------------------------------------------------------------------ | | `TURSO_DATABASE_URL` | Connection URL for Turso or libSQL. Falls back to `LIBSQL_URL`, then `DATABASE_URL`. | | `TURSO_AUTH_TOKEN` | Authentication token for remote databases. Falls back to `LIBSQL_AUTH_TOKEN`. | ::: :::tabs-item{icon="i-simple-icons-postgresql" label="PostgreSQL"} | Variable | Description | | -------------- | ----------------------------------------------------------------------------------- | | `POSTGRES_URL` | Connection URL for PostgreSQL. Falls back to `POSTGRESQL_URL`, then `DATABASE_URL`. | ::: :::tabs-item{icon="i-simple-icons-mysql" label="MySQL"} | Variable | Description | | ----------- | ------------------------------------------------------- | | `MYSQL_URL` | Connection URL for MySQL. Falls back to `DATABASE_URL`. | ::: :: ::tip Apply migrations manually after deployment using `npx nuxt db migrate` or your CI/CD pipeline. :: ### Read Replicas NuxtHub supports read replicas using Drizzle ORM's [`withReplicas()`](https://orm.drizzle.team/docs/read-replicas){rel=""nofollow""} feature for `postgres-js` and `mysql2` drivers. When configured, read queries are automatically routed to replicas while write queries go to the primary database. Configure replicas in your `nuxt.config.ts`: ::tabs{sync="database-dialect"} :::tabs-item{icon="i-simple-icons-postgresql" label="PostgreSQL"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'postgresql', replicas: [ process.env.DATABASE_URL_REPLICA_1, process.env.DATABASE_URL_REPLICA_2 ].filter(Boolean) } } }) ``` ::: :::tabs-item{icon="i-simple-icons-mysql" label="MySQL"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'mysql', replicas: [ process.env.DATABASE_URL_REPLICA_1, process.env.DATABASE_URL_REPLICA_2 ].filter(Boolean) } } }) ``` ::: :: ## AI Agents If you work with an IDE that supports AI agents, you can add the following text in your `Agents.md` or `.cursor/rules` file: ```md # Agent Instructions /** ... your agent instructions ... */ ## Database - **Database Dialect**: The database dialect is set in the `nuxt.config.ts` file, within the `hub.db` option or `hub.db.dialect` property. - **Drizzle Config**: Don't generate the `drizzle.config.ts` file manually, it is generated automatically by NuxtHub. - **Generate Migrations**: Use `npx nuxt db generate` to automatically generate database migrations from schema changes - **Never Write Manual Migrations**: Do not manually create SQL migration files in the `server/db/migrations/` directory - **Workflow**: 1. Create or modify the database schema in `server/db/schema.ts` or any other schema file in the `server/db/schema/` directory 2. Run `npx nuxt db generate` to generate the migration 3. Run `npx nuxt db migrate` to apply the migration to the database, or run `npx nuxt dev` to apply the migration during development - **Access the database**: Use the `db` instance from `@nuxthub/db` (or `hub:db` for backwards compatibility) to query the database, it is a Drizzle ORM instance. ``` ## Migrating from v0.9 ::important **Breaking changes in NuxtHub v0.10:** If you're upgrading from a previous version that used `hubDatabase()` , follow this migration guide. :: ### Configuration changes The `database` option has been renamed to `db` and now accepts a SQL dialect instead of a boolean. ```diff [nuxt.config.ts] export default defineNuxtConfig({ hub: { - database: true + db: 'sqlite' } }) ``` Valid dialects are `sqlite`, `postgresql` and `mysql`. ### Directory changes The database directory has been renamed from `server/database/` to `server/db/`: ```diff - server/database/schema.ts + server/db/schema.ts - server/database/migrations/ + server/db/migrations/ ``` Make sure to move your schema and migration files to the new location. ### API changes The old `hubDatabase()` function has been removed. You must now use Drizzle ORM. **Before:** ```ts const db = hubDatabase() const result = await db.prepare('SELECT * FROM users').all() ``` **After:** ```ts const result = await db.select().from(tables.users) ``` ### Migration files Your existing SQL migration files continue to work! Just move them to `server/db/migrations/`. # Database Schema NuxtHub supports defining the database schema in multiple files and directories, allowing you to organize your schema files in a way that makes sense for your project, but also open the possibility to Nuxt modules to extend the database schema. ### Schema files Database schema can be defined in a single file or in multiple files, these files are scanned and automatically imported following this glob pattern: - `server/db/schema.ts` - `server/db/schema.{dialect}.ts` - `server/db/schema/*.ts` - `server/db/schema/*.{dialect}.ts` The merged schema is exported and can be imported in multiple ways: ```ts // Import entire schema import * as schema from '@nuxthub/db/schema' // Also possible: Import schema object from main module import { schema } from '@nuxthub/db' // Legacy: Virtual module (backwards compatibility) import * as schema from 'hub:db:schema' ``` ::callout{icon="i-lucide-lightbulb"} You can locate the generated schema file at `.nuxt/hub/db/schema.mjs` . :: ::note{external to="https://orm.drizzle.team/docs/sql-schema-declaration"} Learn more about [Drizzle ORM schema](https://orm.drizzle.team/docs/sql-schema-declaration){rel=""nofollow""} on the Drizzle documentation. :: ### Nuxt layers Database schema is scanned and automatically imported for each [Nuxt layer](https://nuxt.com/docs/getting-started/layers){rel=""nofollow""}. This means that you can also define schema in the `layers` directory: ```bash [Directory structure] layers/cms/server/db/schema.ts layers/products/server/db/schema/products.ts ``` ### Nuxt modules If you are a Nuxt module developer, you can also extend the database schema by using the `hub:db:schema:extend` hook: ```ts [modules/cms/index.ts] import { defineNuxtModule, createResolver } from '@nuxt/kit' export default defineNuxtModule({ setup(options, nuxt) { const { resolvePath } = createResolver(import.meta.url) nuxt.hook('hub:db:schema:extend', async ({ dialect, paths }) => { // Add your module drizzle schema files for the given dialect // e.g. ./schema/pages.postgresql.ts if hub.db is 'postgresql' paths.push(await resolvePath(`./schema/pages.${dialect}`)) }) } }) ``` ### Sharing types with Vue Types inferred from your database schema are only available on the server-side by default. To share these types with your Vue application, you can use the [`shared/`](https://nuxt.com/docs/guide/directory-structure/shared){rel=""nofollow""} directory which is auto-imported across both server and client. Create a types file in the `shared/types/` directory: ```ts [shared/types/db.ts] import { users, posts } from '@nuxthub/db/schema' // Select types (for reading data) export type User = typeof users.$inferSelect export type Post = typeof posts.$inferSelect // Insert types (for creating data) export type NewUser = typeof users.$inferInsert export type NewPost = typeof posts.$inferInsert ``` These types are now auto-imported and available in your Vue components, composables, and API routes: ::code-group ```vue [pages/users.vue] ``` ```ts [server/api/users.post.ts] import { db, schema } from '@nuxthub/db' export default eventHandler(async (event) => { const body = await readBody(event) return await db.insert(schema.users).values(body).returning() }) ``` :: ::tip You can also create more specific types by using `Pick` and `Omit` TypeScript's built-in utility types. ```ts [shared/types/db.ts] // User without password for public API responses export type PublicUser = Omit // Only the fields needed for user creation form export type UserForm = Pick ``` :: ## Database seed You can populate your database with initial data using [Nitro Tasks](https://nitro.build/guide/tasks){rel=""nofollow""}: ::steps{level="3"} ### Enable Nitro tasks ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { experimental: { tasks: true } } }) ``` ### Create a seed task ```ts [server/tasks/seed.ts] import { db, schema } from '@nuxthub/db' export default defineTask({ meta: { name: 'db:seed', description: 'Seed database with initial data' }, async run() { console.log('Seeding database...') const users = [ { name: 'John Doe', email: 'john@example.com', password: 'hashed_password', avatar: 'https://i.pravatar.cc/150?img=1', createdAt: new Date() }, { name: 'Jane Doe', email: 'jane@example.com', password: 'hashed_password', avatar: 'https://i.pravatar.cc/150?img=2', createdAt: new Date() } ] await db.insert(schema.users).values(users) return { result: 'Database seeded successfully' } } }) ``` ### Execute the task Open the `Tasks` tab in Nuxt DevTools and click on the `db:seed` task. :: # Database Queries Now that you have your database schema and migrations set up, you can start querying your database. ## Importing the database NuxtHub provides two ways to import the database client: ### Recommended: `@nuxthub/db` Use `@nuxthub/db` to import the database client. This works everywhere, including in Nuxt server routes and external bundlers like [Workflow](https://useworkflow.dev){rel=""nofollow""}: ```ts import { db, schema } from '@nuxthub/db' ``` ::tip{icon="i-lucide-sparkles"} This is the **recommended approach** as it works with both Nuxt and external tools that bundle your code independently. :: ### Legacy: `hub:db` The virtual module `hub:db` is still supported for backwards compatibility (Nuxt only): ```ts import { db, schema } from 'hub:db' ``` ::tip `db` and `schema` are auto-imported on the server-side, so you can use them directly without importing. :: ## SQL Select ```ts [server/api/users.get.ts] import { db, schema } from '@nuxthub/db' export default eventHandler(async (event) => { return await db.query.users.findMany() // or return await db.select().from(schema.users) }) ``` ::callout{external to="https://orm.drizzle.team/docs/select"} Learn more about [Drizzle ORM select](https://orm.drizzle.team/docs/select){rel=""nofollow""} on the Drizzle documentation. :: ## SQL Insert ```ts [server/api/users.post.ts] import { db, schema } from '@nuxthub/db' export default eventHandler(async (event) => { const { name, email } = await readBody(event) return await db .insert(schema.users) .values({ name, email, createdAt: new Date() }) .returning() }) ``` ::callout{external to="https://orm.drizzle.team/docs/insert"} Learn more about [Drizzle ORM insert](https://orm.drizzle.team/docs/insert){rel=""nofollow""} on the Drizzle documentation. :: ## SQL Update ```ts [server/api/users/[id\\].patch.ts] import { db, schema } from '@nuxthub/db' export default eventHandler(async (event) => { const { id } = getRouterParams(event) const { name } = await readBody(event) return await db .update(schema.users) .set({ name }) .where(eq(tables.users.id, Number(id))) .returning() }) ``` ::callout{external to="https://orm.drizzle.team/docs/update"} Learn more about [Drizzle ORM update](https://orm.drizzle.team/docs/update){rel=""nofollow""} on the Drizzle documentation. :: ## SQL Delete ```ts [server/api/users/[id\\].delete.ts] import { db, schema } from '@nuxthub/db' export default eventHandler(async (event) => { const { id } = getRouterParams(event) const deletedUser = await db .delete(schema.users) .where(eq(schema.users.id, Number(id))) .returning() if (!deletedUser) { throw createError({ statusCode: 404, message: 'User not found' }) } return { deleted: true } }) ``` ::callout{external to="https://orm.drizzle.team/docs/delete"} Learn more about [Drizzle ORM delete](https://orm.drizzle.team/docs/delete){rel=""nofollow""} on the Drizzle documentation. :: ## Using with external bundlers The `@nuxthub/db` import works seamlessly with external tools that bundle your code independently, such as [Workflow](https://useworkflow.dev){rel=""nofollow""}. ### Example with Workflow [Workflow](https://useworkflow.dev){rel=""nofollow""} is a library for building and running durable, reliable, and scalable background jobs in TypeScript. Here's how to use NuxtHub Database with Workflow: ```ts [workflow/tasks/cleanup-inactive-users.ts] import { db, schema } from '@nuxthub/db' import { lt } from 'drizzle-orm' export default async function cleanupInactiveUsers() { // Delete users who haven't logged in for 90 days const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) const deleted = await db .delete(schema.users) .where(lt(schema.users.lastLoginAt, ninetyDaysAgo)) .returning() return { deletedCount: deleted.length, deletedUserIds: deleted.map(u => u.id) } } ``` ### How it works When you build your Nuxt application: 1. NuxtHub generates the database client at `node_modules/@nuxthub/db/` 2. This physical module contains your compiled database schema and client 3. External bundlers like Workflow can import and bundle this module directly 4. The `hub:db` virtual module remains available for Nuxt server routes (backwards compatibility) ::tip{icon="i-lucide-info"} The `@nuxthub/db` package is auto-generated during development and build. You don't need to add it to your `package.json` . :: ### Benefits - **Universal compatibility**: Works with Nuxt server routes, Workflow tasks, and any other TypeScript code - **Type-safe**: Full TypeScript support with auto-generated types - **No configuration**: Automatically synced when you run `nuxt dev` or `nuxt build` - **Single source of truth**: Your `server/db/schema.ts` files are the only place you define your schema # Database Migrations Database migrations provide version control for your database schema. NuxtHub supports SQL migration files (`.sql`) and automatically applies them during development and deployment. Making them fully compatible with Drizzle Kit generated migrations. ::note Create dialect-specific migrations with `..sql` suffix (e.g., `0001_create-todos.postgresql.sql` ). :: ### Migrations Directories NuxtHub scans `server/db/migrations` for migrations in each [Nuxt layer](https://nuxt.com/docs/getting-started/layers){rel=""nofollow""}. To scan additional directories, specify them in your config: ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'postgresql', migrationsDirs: [ 'server/db/custom-migrations/' ] } } }) ``` For more control (e.g., in Nuxt modules), use the `hub:db: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:db: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 migration files are copied to `.data/db/migrations` when you run Nuxt, giving you a consolidated view. :: ## Automatic migrations Migrations are automatically applied when you: - Start the development server (`npx nuxt dev`) - Build the application (`npx nuxt build`) Applied migrations are tracked in the `_hub_migrations` database table. ::warning{to="https://hub.nuxt.com/docs/getting-started/deploy#cloudflare"} For Cloudflare D1, migrations cannot run during build since there is no database connection in CI. Run `npx nuxt db migrate` locally or set up a CI step to apply migrations before deployment. :: ::note To disable automatic migrations during `nuxt build`, you can set the `applyMigrationsDuringBuild` option to `false`. To disable automatic migrations during `nuxt dev`, set `applyMigrationsDuringDev` to `false` (useful when running dev with production env vars): ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { applyMigrationsDuringBuild: false, applyMigrationsDuringDev: false } } }) ``` :: ## Generating migrations Once you have updates your database schema, you can generate new migrations using the following command: ```bash [Terminal] npx nuxt db generate ``` This will generate new migrations files in `server/db/migrations/{dialect}/` which are automatically applied during development and deployment. ## Applying migrations Once you have generated new migrations, you can apply them using the following command: ```bash [Terminal] npx nuxt db migrate ``` This will apply the new migrations to your database. ::tip When running the development server, NuxtHub will automatically apply the migrations for you. :: ::callout{to="https://hub.nuxt.com/docs/guides/ci-cd"} See the CI/CD guide for production migration workflows, including D1-specific handling. :: ## Post-migration queries ::important Advanced use case: These queries run after migrations but aren't tracked in `_hub_migrations` . Ensure they're idempotent. :: Use the `hub:db:queries:paths` hook to run additional queries after migrations: ::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:db:queries:paths', (paths, dialect) => { paths.push(resolve(`./db-queries/seed-admin.${dialect}.sql`)) }) } }) ``` ```sql [modules/admin/db-queries/seed-admin.sql] INSERT OR IGNORE INTO admin_users (id, email, password_hash) VALUES (1, '', ''); ``` :: ::tip All migrations queries are copied to `.data/db/queries` when you run Nuxt, giving you a consolidated view. :: ## Foreign-key constraints For Cloudflare D1 with Drizzle ORM migrations, replace: ```diff [Example] -PRAGMA foreign_keys = OFF; +PRAGMA defer_foreign_keys = on; ALTER TABLE ... -PRAGMA foreign_keys = ON; +PRAGMA defer_foreign_keys = off; ``` ::callout --- external: true to: https://developers.cloudflare.com/d1/sql-api/foreign-keys/#defer-foreign-key-constraints --- Learn more about [defer foreign key constraints](https://developers.cloudflare.com/d1/sql-api/foreign-keys/#defer-foreign-key-constraints){rel=""nofollow""} in Cloudflare D1. :: # Database CLI NuxtHub provides a CLI for managing your database migrations and running SQL queries accessible from the `npx nuxt db` command. ## `nuxt db generate` Generate database migrations from the schema. ```bash [Terminal] USAGE db generate [OPTIONS] OPTIONS --custom Whether to generate an empty migration file for custom SQL. --name Custom name for the migration file. --cwd The directory to run the command in. -v, --verbose Show verbose output. ``` Example usage: ```bash [Terminal] # Generate a migration with default name npx nuxt db generate # Generate a migration based on schema changes npx nuxt db generate --name add_new_column # Generate an empty migration file for custom SQL npx nuxt db generate --custom --name seed_initial_data ``` ::tip Read more about generating migrations in the [Drizzle Kit's official documentation](https://orm.drizzle.team/docs/drizzle-kit-generate){rel=""nofollow""} . :: ## `nuxt db migrate` Apply database migrations to the database. ```bash [Terminal] USAGE db migrate [OPTIONS] OPTIONS --cwd The directory to run the command in. --dotenv Point to another .env file to load. -v, --verbose Show verbose output. ``` ## `nuxt db mark-as-migrated` Mark local database migration(s) as applied to the database. ```bash [Terminal] USAGE db mark-as-migrated [OPTIONS] [NAME] ARGUMENTS NAME The name of the migration to mark as applied. If not provided, marks all pending migrations as applied. (optional) OPTIONS --cwd The directory to run the command in. --dotenv Point to another .env file to load. -v, --verbose Show verbose output. ``` ## `nuxt db drop` Drop a table from the database. ```bash [Terminal] USAGE db drop [OPTIONS] ARGUMENTS TABLE The name of the table to drop. OPTIONS --cwd The directory to run the command in. --dotenv Point to another .env file to load. -v, --verbose Show verbose output. ``` ## `nuxt db drop-all` Drop all tables from the database. ```bash [Terminal] USAGE db drop-all [OPTIONS] OPTIONS --cwd The directory to run the command in. --dotenv Point to another .env file to load, relative to the root directory. -v, --verbose Show verbose output. ``` ::warning This is a destructive operation that will permanently delete all data in your database. Take a backup of your database before using this command. :: ## `nuxt db squash` Squash several migrations into a single migration. This is useful for cleaning up your migration history during development. ```bash [Terminal] USAGE db squash [OPTIONS] OPTIONS --last Number of migrations to squash starting from most recently applied. If not specified migrations can be interactively selected. --cwd The directory to run the command in. -v, --verbose Show verbose output. ``` Example usage: ```bash [Terminal] # Squash the last 3 migrations into one npx nuxt db squash --last 3 # Interactive mode - select which migrations to squash npx nuxt db squash ``` After squashing, you'll be prompted to mark the new migration as already applied. This is useful when your database already has the schema from the squashed migrations applied. ::note When using interactive selection, all migrations after the oldest selected one will automatically be included, since migrations must be squashed sequentially. :: ## `nuxt db sql` Execute a SQL query against the database. ```bash [Terminal] USAGE db sql [OPTIONS] [QUERY] ARGUMENTS QUERY The SQL query to execute. If not provided, reads from stdin. OPTIONS --cwd The directory to run the command in. --dotenv Point to another .env file to load, relative to the root directory. -v, --verbose Show verbose output. ``` Example usage: ```bash [Terminal] npx nuxt db sql "SELECT * FROM users" # or npx nuxt db sql < dump.sql ``` # Nuxt Blob Storage ## Getting Started ::steps{level="3"} ### Enable blob storage Enable blob storage in your project by setting `blob: true` in the NuxtHub config. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { blob: true } }) ``` ### Set a driver NuxtHub automatically configures the blob storage driver based on your environment variables or hosting provider. :::note By default, if NuxtHub cannot detect a driver, files are stored locally in the `.data/blob` directory. ::: :::tabs{sync="blob-provider"} ::::tabs-item{icon="i-simple-icons-nodedotjs" label="FS (default)"} To set the storage directory, you can set the `dir` option. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { blob: { driver: 'fs', dir: '.data/my-blob-directory' // Defaults to `.data/blob` } } }) ``` :::::important The local filesystem driver is not suitable for production environments. ::::: :::: ::::tabs-item{icon="i-simple-icons-amazons3" label="S3"} To configure [Amazon S3](https://aws.amazon.com/s3/){rel=""nofollow""} as a blob storage driver. 1. Install the `aws4fetch` package :pm-install{name="aws4fetch"} 2. Set the following environment variables: ```bash [.env] S3_ACCESS_KEY_ID=your-access-key-id S3_SECRET_ACCESS_KEY=your-secret-access-key S3_BUCKET=your-bucket-name S3_REGION=your-region S3_ENDPOINT=your-endpoint # (optional) ``` :::: ::::tabs-item{icon="i-simple-icons-vercel" label="Vercel Blob"} When deploying to Vercel, it automatically configures [Vercel Blob Storage](https://vercel.com/docs/storage/vercel-blob){rel=""nofollow""}. 1. Install the `@vercel/blob` package :pm-install{name="@vercel/blob"} 2. Assign a Vercel Blob Store to your project from the [Vercel dashboard](https://vercel.com/){rel=""nofollow""} -> Project -> Storage :::::important Files stored in Vercel Blob are always public. Manually configure a different storage driver if storing sensitive files. ::::: 3. When running locally, you can set the `BLOB_READ_WRITE_TOKEN` environment variable to enable the Vercel Blob driver: ```bash [.env] BLOB_READ_WRITE_TOKEN=your-token ``` :::: ::::tabs-item{icon="i-simple-icons-cloudflare" label="Cloudflare R2"} When deploying to Cloudflare, configure [Cloudflare R2](https://developers.cloudflare.com/r2/){rel=""nofollow""} by providing the bucket name. NuxtHub auto-generates the wrangler bindings at build time. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { blob: { driver: 'cloudflare-r2', bucketName: '' } } }) ``` :::::callout --- to: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ --- Learn more about R2 bindings on Cloudflare's documentation. ::::: :::::note To use Cloudflare R2 without hosting on Cloudflare Workers, use the [Cloudflare R2 via S3 API](https://developers.cloudflare.com/r2/api/s3/api/){rel=""nofollow""} . ::::: :::: ::: ### Upload a file To upload a file, you can use the `blob.put()` method. ```ts import { blob } from 'hub:blob' const file = await blob.put('avatars/user-1.png', imageData) ``` :::tip{to="https://hub.nuxt.com/docs/blob/upload"} Learn how to upload files with validation and progress tracking. ::: :: ### Driver options You can set a custom driver by providing a configuration object to `blob`. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { blob: { driver: 'fs', dir: '.data/blob' } }, // or overwrite only in production $production: { hub: { blob: { driver: 'vercel-blob' } } } }) ``` Available drivers: ::tabs{sync="blob-provider"} :::tabs-item{icon="i-simple-icons-nodedotjs" label="FS (default)"} ```ts export default defineNuxtConfig({ hub: { blob: { driver: 'fs', dir: '.data/files' // defaults to '.data/blob' } } }) ``` ::: :::tabs-item{icon="i-simple-icons-amazons3" label="S3"} ```ts export default defineNuxtConfig({ hub: { blob: { driver: 's3', accessKeyId: 'your-access-key-id', // defaults to S3_ACCESS_KEY_ID secretAccessKey: 'your-secret-access-key', // defaults to S3_SECRET_ACCESS_KEY bucket: 'your-bucket-name', // defaults to S3_BUCKET region: 'your-region', // defaults to S3_REGION or 'auto' endpoint: 'your-endpoint' // optional, defaults to S3_ENDPOINT } } }) ``` ::: :::tabs-item{icon="i-simple-icons-vercel" label="Vercel Blob"} ```ts export default defineNuxtConfig({ hub: { blob: { driver: 'vercel-blob', token: 'your-token' // optional, defaults to BLOB_READ_WRITE_TOKEN env var } } }) ``` ::: :::tabs-item{icon="i-simple-icons-cloudflare" label="Cloudflare R2"} ```ts export default defineNuxtConfig({ hub: { blob: { driver: 'cloudflare-r2', binding: 'BLOB' // optional, defaults to 'BLOB' } } }) ``` ::: :: ## Nuxt Image Integration [`@nuxt/image`](https://image.nuxt.com){rel=""nofollow""} provides automatic image optimization. You can combine it with blob storage to serve optimized images from your storage. ::steps{level="3"} ### Install @nuxt/image ```bash [Terminal] npx nuxt module add image ``` :::tip This command will install `@nuxt/image` and add it to your `modules` section of your `nuxt.config.ts` . ::: ### Expose your blobs on a route Create a server route (for example `/images/**`) that serves blobs using `blob.serve()`: This route should exist in both development and production. In production, providers like Cloudflare and Vercel optimize by generating URLs that wrap this route. ```ts [server/routes/images/[...pathname\\].get.ts] import { blob } from 'hub:blob' import { createError, eventHandler, getRouterParam } from 'h3' export default eventHandler(async (event) => { const pathname = getRouterParam(event, 'pathname') if (!pathname) { throw createError({ statusCode: 404, statusMessage: 'Not Found' }) } return blob.serve(event, pathname) }) ``` :::tip{to="https://hub.nuxt.com/docs/blob/usage#serve-a-blob"} Learn more about serving blobs (headers, CSP, etc.) in the **Serve a blob** section. ::: ### Configure the image provider Configure `@nuxt/image` to use the appropriate provider for your hosting platform: :::tabs{sync="image-provider"} ::::tabs-item{icon="i-simple-icons-cloudflare" label="Cloudflare"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ image: { provider: 'none' }, $production: { image: { provider: 'cloudflare' } } }) ``` :::::callout{to="https://image.nuxt.com/providers/cloudflare"} See Cloudflare provider docs for requirements and options. ::::: :::: ::::tabs-item{icon="i-simple-icons-vercel" label="Vercel"} ```ts [nuxt.config.ts] export default defineNuxtConfig({ image: { provider: 'none' }, $production: { image: { provider: 'vercel' } } }) ``` :::::callout{to="https://image.nuxt.com/providers/vercel"} See Vercel provider docs for requirements and options. ::::: :::: ::: :::warning{title="Development Limitation"} Cloudflare and Vercel providers generate URLs that only exist in production (`/cdn-cgi/image/` for Cloudflare, `/_vercel/image` for Vercel), so enable them only in `$production`. In development, `@nuxt/image` defaults to the `ipx` provider if you don't configure it. IPX generates `/_ipx/...` URLs and tries to read the source from the filesystem, so it won't work with blob routes like `/images/**` (you may see `IPX_FILE_NOT_FOUND` errors). Use `provider: 'none'` in development to keep `src="/images/..."` working. ::: ### Use NuxtImg or NuxtPicture Use the components to display optimized images from blob storage: ```vue [pages/gallery.vue] ``` The `src` path combines your route prefix (`/images`) with the blob pathname (`photo.jpg`). :: # File Uploads This page covers different methods to upload files to your Blob Storage, from simple file uploads to large file multipart uploads. ## Simple Upload For most use cases, you can use `handleUpload()` on the server and `useUpload()` on the client for a streamlined file upload experience. This example create an API route to handle file uploads on `/api/upload`. It validates the files to be images of less than 10MB, and uploads them to Blob Storage with a random suffix and the `images` prefix. ```ts [server/api/upload.post.ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { // Make sure to check if the user can upload files before calling this function return blob.handleUpload(event, { formKey: 'files', multiple: true, ensure: { maxSize: '10MB', types: ['image/jpeg', 'image/png', 'image/webp'], }, put: { addRandomSuffix: true, prefix: 'images', }, }) }) ``` ### `handleUpload()` A server function to handle file uploads. It validates the files and uploads them to Blob Storage. ```ts await blob.handleUpload(event, options) ``` #### Params ::field-group :::field{required name="event" type="H3Event"} The event handler's event. ::: :::field{name="options" type="Object"} The upload options. ::::collapsible :::::field{name="formKey" type="string"} The form key to read the file from. Defaults to `'files'` . ::::: :::::field{name="multiple" type="boolean"} When `true` , accepts multiple files and returns an array of `BlobObject` . Defaults to `true` . ::::: :::::field{name="ensure" type="BlobEnsureOptions"} Validation options passed to [`ensureBlob()`](https://hub.nuxt.com/docs/blob/usage#ensureblob) . ::::: :::::field{name="put" type="BlobPutOptions"} Options passed to `blob.put()` . ::::: :::: ::: :: #### 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 the file doesn't meet the requirements. ### `useUpload()` A Vue composable to handle file uploads on the client. ```vue [pages/upload.vue] ``` #### Params ::field-group :::field{required name="apiBase" type="string"} The base URL of the upload API endpoint. ::: :::field{name="options" type="Object"} Fetch options for the upload request. ::::collapsible :::::field{name="formKey" type="string"} The key to add the file(s) to the request form. Defaults to `'files'` . ::::: :::::field{name="multiple" type="boolean"} Whether to allow multiple files to be uploaded. Defaults to `true` . ::::: :::::field{name="method" type="string"} The HTTP method to use. Defaults to `'POST'` . ::::: :::: ::: :: #### Return Returns a function that accepts an `HTMLInputElement` and returns a `Promise` resolving to the uploaded blob data. ### Full Example ::code-group ```ts [server/api/upload.post.ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { return blob.handleUpload(event, { formKey: 'files', multiple: true, ensure: { maxSize: '10MB', types: ['image'], }, put: { addRandomSuffix: true, prefix: 'avatars', }, }) }) ``` ```vue [app/pages/upload.vue] ``` :: ## Multipart Upload For large files (typically > 10MB), use multipart uploads to split the file into smaller chunks. This provides better reliability and allows you to track upload progress. ### `handleMultipartUpload()` A server function to handle multipart upload requests. It handles creating, uploading parts, completing, and aborting multipart uploads. The route pattern depends on your blob driver: ::tabs{sync="blob-provider"} :::tabs-item{icon="i-lucide-hard-drive" label="R2/S3/FS"} The route must include `[action]` and `[...pathname]` params. The `action` param handles the multipart operations (`create`, `upload`, `complete`, `abort`). ```ts [server/api/files/multipart/[action\\]/[...pathname\\].ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { return blob.handleMultipartUpload(event) }) ``` ::: :::tabs-item{icon="i-simple-icons-vercel" label="Vercel Blob"} Use `[...pathname]` only. Vercel Blob uses its own client SDK protocol and does not require the `[action]` parameter. ```ts [server/api/files/multipart/[...pathname\\].ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { return blob.handleMultipartUpload(event) }) ``` ::: :: #### Params ::field-group :::field{required name="event" type="H3Event"} The event handler's event. ::: :::field{name="options" type="Object"} The multipart upload options. ::::collapsible :::::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` . ::::: :::::field{name="prefix" type="string"} The prefix to use for the blob pathname. ::::: :::: ::: :: #### Return Returns a response based on the action: - `create`: Returns `{ pathname, uploadId }` - `upload`: Returns `{ partNumber, etag }` - `complete`: Returns a [`BlobObject`](https://hub.nuxt.com/#blobobject) - `abort`: Returns nothing ### `useMultipartUpload()` A Vue composable to handle multipart file uploads on the client with progress tracking. ```vue [pages/upload-large.vue] ``` ::important Multipart uploads are supported on **Cloudflare R2** , **S3** , **Vercel Blob** , and **filesystem** drivers. :: #### Params ::field-group :::field{required name="baseURL" type="string"} The base URL of the multipart upload API handled by `handleMultipartUpload()` . ::: :::field{name="options" type="Object"} The options for the multipart upload helper. ::::collapsible :::::field{name="partSize" type="number"} The size of each part. Defaults to `10MB` . ::::: :::::field{name="concurrent" type="number"} The maximum number of concurrent part uploads. Defaults to `1` . ::::: :::::field{name="maxRetry" type="number"} The maximum number of retry attempts. Defaults to `3` . ::::: :::::field{name="prefix" type="string"} The prefix to use for the blob pathname. ::::: :::::field{name="fetchOptions" type="FetchOptions"} Override the fetch options. The `query` and `headers` will be merged. ::::: :::: ::: :: #### Return Returns a function that accepts a `File` and returns: ```ts { completed: Promise // Resolves when upload completes progress: Readonly> // Upload progress (0 to 1) abort: () => Promise // Function to cancel the upload } ``` ::note When using the **Vercel Blob** driver, `useMultipartUpload()` automatically uses the [Vercel Blob Client SDK](https://vercel.com/docs/vercel-blob/client-upload){rel=""nofollow""} for uploads. :: ### Full Example ::tabs{sync="blob-provider"} :::tabs-item{icon="i-lucide-hard-drive" label="R2/S3/FS"} ::::code-group ```ts [server/api/files/multipart/[action\\]/[...pathname\\].ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { return blob.handleMultipartUpload(event, { addRandomSuffix: true, prefix: 'uploads', }) }) ``` ```vue [pages/upload-large.vue] ``` :::: ::: :::tabs-item{icon="i-simple-icons-vercel" label="Vercel Blob"} ::::code-group ```ts [server/api/files/multipart/[...pathname\\].ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { return blob.handleMultipartUpload(event, { addRandomSuffix: true, prefix: 'uploads', }) }) ``` ```vue [pages/upload-large.vue] ``` :::: ::: :: ## Advanced Multipart For more control over the multipart upload process, you can use the low-level APIs directly. ::note We recommend using `handleMultipartUpload()` and `useMultipartUpload()` for most use cases. Use these low-level APIs only when you need custom behavior. :: ### `createMultipartUpload()` Start a new multipart upload. ```ts [server/api/files/multipart/[...pathname\\].post.ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) const mpu = await blob.createMultipartUpload(pathname, { addRandomSuffix: true, }) return { uploadId: mpu.uploadId, pathname: mpu.pathname, } }) ``` #### Params ::field-group :::field{required name="pathname" type="string"} The pathname for the blob. ::: :::field{name="options" type="Object"} ::::collapsible :::::field{name="contentType" type="string"} The content type of the blob. ::::: :::::field{name="addRandomSuffix" type="boolean"} If `true` , a random suffix will be added to the pathname. Defaults to `false` . ::::: :::::field{name="prefix" type="string"} The prefix to use for the blob pathname. ::::: :::: ::: :: #### Return Returns a [`BlobMultipartUpload`](https://hub.nuxt.com/#blobmultipartupload) object. ### `resumeMultipartUpload()` Resume an existing multipart upload to upload parts, complete, or abort it. **Upload a part:** ```ts [server/api/files/multipart/[...pathname\\].put.ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) const { uploadId, partNumber } = getQuery(event) const body = await readRawBody(event) const mpu = blob.resumeMultipartUpload(pathname, uploadId as string) return mpu.uploadPart(Number(partNumber), body) }) ``` **Complete the upload:** ```ts [server/api/files/multipart/complete.post.ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { const { pathname, uploadId } = getQuery(event) const parts = await readBody(event) const mpu = blob.resumeMultipartUpload(pathname as string, uploadId as string) return mpu.complete(parts) }) ``` **Abort the upload:** ```ts [server/api/files/multipart/[...pathname\\].delete.ts] import { blob } from 'hub:blob' export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) const { uploadId } = getQuery(event) const mpu = blob.resumeMultipartUpload(pathname, uploadId as string) await mpu.abort() return sendNoContent(event) }) ``` #### Params ::field-group :::field{required name="pathname" type="string"} The pathname of the multipart upload. ::: :::field{required name="uploadId" type="string"} The upload ID from `createMultipartUpload()` . ::: :: #### Return Returns a [`BlobMultipartUpload`](https://hub.nuxt.com/#blobmultipartupload) object with methods: - `uploadPart(partNumber, body)`: Upload a single part - `complete(parts)`: Complete the upload with all uploaded parts - `abort()`: Cancel the upload ### Client Implementation Example ```ts [app/utils/multipart-upload.ts] async function uploadLargeFile(file: File) { const chunkSize = 10 * 1024 * 1024 // 10MB per part // 1. Create the multipart upload const { pathname, uploadId } = await $fetch(`/api/files/multipart/${file.name}`, { method: 'POST', }) // 2. Upload parts const totalParts = Math.ceil(file.size / chunkSize) const uploadedParts = [] for (let i = 0; i < totalParts; i++) { const start = i * chunkSize const end = Math.min(start + chunkSize, file.size) const chunk = file.slice(start, end) const partNumber = i + 1 const part = await $fetch(`/api/files/multipart/${pathname}`, { method: 'PUT', query: { uploadId, partNumber }, body: chunk, }) uploadedParts.push(part) console.log(`Uploaded part ${partNumber}/${totalParts}`) } // 3. Complete the upload const blob = await $fetch('/api/files/multipart/complete', { method: 'POST', query: { pathname, uploadId }, body: uploadedParts, }) return blob } ``` ## Types ### `BlobObject` ```ts-type { pathname: string contentType: string | undefined size: number httpEtag: string | undefined uploadedAt: Date httpMetadata: Record customMetadata: Record url: string | undefined } ``` ### `BlobMultipartUpload` ```ts-type { pathname: string uploadId: string uploadPart( partNumber: number, value: string | ReadableStream | ArrayBuffer | ArrayBufferView | Blob ): Promise abort(): Promise complete(uploadedParts: BlobUploadedPart[]): Promise } ``` ### `BlobUploadedPart` ```ts-type { partNumber: number etag: string } ``` ### `MultipartUploader` ```ts-type (file: File) => { completed: Promise progress: Readonly> abort: () => Promise } ``` # Using the Blob SDK The Blob SDK provides access to Blob Storage through a unified API that works across all storage providers (Cloudflare R2, S3, Vercel Blob, filesystem). ## Importing the Blob storage ### Recommended: `@nuxthub/blob` Use `@nuxthub/blob` to import the Blob storage. This works everywhere (Nuxt server routes, Nitro server routes, Workflows, etc.): ```ts import { blob } from '@nuxthub/blob' ``` ::tip `blob` is auto-imported on server-side in Nuxt, you can directly use it without importing it. :: ### Legacy: `hub:blob` The virtual module `hub:blob` is still supported for backwards compatibility: ```ts import { blob } from 'hub:blob' ``` ::note While `hub:blob` still works in Nuxt projects, we recommend using `@nuxthub/blob` for better compatibility with external bundlers and workflows. :: ## List blobs This example create an API route to list the first 10 blobs in the Blob storage. ```ts [server/api/files.get.ts] export default eventHandler(async () => { const { blobs } = await blob.list({ limit: 10 }) return blobs }) ``` ### `list()` Returns a paginated list of blobs (metadata only). ```ts await blob.list(options) ``` ::note When using the local filesystem driver, the `limit` option is ignored and all blobs are returned. :: #### 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 a json object with the following structure: ```ts-type { blobs: { pathname: string contentType: string | undefined size: number httpEtag: string | undefined uploadedAt: Date httpMetadata: Record customMetadata: Record url?: string }[] hasMore: boolean cursor?: string folders?: string[] } ``` ### List with prefix Filter blobs by a prefix to organize files into directories: ```ts // List all files in the "images/" directory const { blobs } = await blob.list({ prefix: 'images/' }) ``` ### List with folders Use `folded: true` to get a folder-like structure: ```ts const { blobs, folders } = await blob.list({ folded: true }) // folders: ['images/', 'documents/', 'videos/'] ``` ### Pagination Fetch all blobs using cursor-based pagination: ```ts let allBlobs = [] let cursor = null do { const result = await blob.list({ cursor }) allBlobs.push(...result.blobs) cursor = result.cursor } while (cursor) ``` ## Serve a blob This example create a server route on `/images/[...pathname]` to serve a blob by its pathname. ```ts [server/routes/images/[...pathname\\].get.ts] import { blob } from 'hub:blob' import { createError, eventHandler, getRouterParam } from 'h3' export default eventHandler(async (event) => { const pathname = getRouterParam(event, 'pathname') if (!pathname) { throw createError({ statusCode: 404, statusMessage: 'Not Found' }) } return blob.serve(event, pathname) }) ``` To display the image in your application, you can use the `` tag with the pathname of the blob. ```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 for additional security: ```ts [server/routes/images/[...pathname\\].get.ts] export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) setHeader(event, 'Content-Security-Policy', 'default-src \'none\';') return blob.serve(event, pathname) }) ``` ### `serve()` Returns a blob's data and sets `Content-Type`, `Content-Length`, and `ETag` headers. ```ts await blob.serve(event, 'images/my-image.jpg') ``` #### Params ::field-group :::field{required name="event" type="H3Event"} Handler's event, needed to set headers. ::: :::field{required name="pathname" type="String"} The pathname of the blob to serve. ::: :: #### Return Returns the blob's raw data and sets `Content-Type`, `Content-Length`, and `ETag` headers. ## Get blob metadata This example create an API route to get a blob's metadata by its pathname. ```ts [server/api/files/[...pathname\\].get.ts] import { blob } from '@nuxthub/blob' import { eventHandler, getRouterParams } from 'h3' export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) return blob.head(pathname) }) ``` ### `head()` Returns a blob's metadata without fetching the content. ```ts await blob.head('images/avatar.png') // { pathname, contentType, size, uploadedAt, ... } ``` #### Params ::field-group :::field{required name="pathname" type="String"} The pathname of the blob. ::: :: #### Return Returns a json object with the following structure: ```ts-type { pathname: string contentType: string | undefined size: number httpEtag: string | undefined uploadedAt: Date httpMetadata: Record customMetadata: Record url?: string } ``` ## Get blob body This example show a server code to get a blob's content by its pathname, and convert it to a text or buffer. ```ts const file = await blob.get('documents/report.pdf') if (file) { const text = await file.text() // or: const buffer = await file.arrayBuffer() } ``` ### `get()` Returns a blob's content as a `Blob` object. ```ts const file = await blob.get('documents/report.pdf') ``` #### Params ::field-group :::field{required name="pathname" type="String"} The pathname of the blob. ::: :: #### Return Returns a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob){rel=""nofollow""} or `null` if not found. ## Upload a blob This example create an API route to upload an image of 1MB maximum to the Blob storage. ```ts [server/api/files.post.ts] import { ensureBlob, blob } from '@nuxthub/blob' import { eventHandler, readFormData, createError } from 'h3' 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 blob.put(file.name, file, { addRandomSuffix: false, prefix: 'images', }) }) ``` ::tip{to="https://hub.nuxt.com/docs/blob/upload"} For a higher-level upload API with validation and client composables, see the **File Uploads** guide. :: ### `put()` ```ts await blob.put('images/1.jpg', file) await blob.put(file.name, file, { access: 'public', addRandomSuffix: true, prefix: 'images/', customMetadata: { userId: '123', category: 'reports', }, }) ``` #### Params ::field-group :::field{required name="pathname" type="String"} The pathname for the blob. ::: :::field --- required: true 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="access" type="'public' | 'private'"} The access level of the blob. Note that only S3 driver supports this option. ::::: :::::field{name="contentType" type="String"} The content type of the blob. If not given, it will be inferred from the Blob or 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 pathname. Defaults to `false` . ::::: :::::field{name="prefix" type="string"} The prefix to use for the blob pathname. ::::: :::::field{name="customMetadata" type="Record"} Custom metadata to store with the blob. *(not supported in Vercel Blob driver)* ::::: :::: ::: :: #### Return Returns a json object with the following structure: ```ts-type { pathname: string contentType: string | undefined size: number httpEtag: string | undefined uploadedAt: Date httpMetadata: Record customMetadata: Record url?: string } ``` #### Upload Examples **Upload from a URL:** ```ts const response = await fetch('https://example.com/image.png') const imageBlob = await response.blob() await blob.put('downloads/image.png', imageBlob) ``` **Upload with custom metadata:** ```ts await blob.put('documents/report.pdf', pdfFile, { customMetadata: { userId: '123', category: 'reports', }, }) ``` **Upload to a specific folder:** ```ts await blob.put('avatar.png', file, { prefix: `users/${userId}`, }) // Stored as: users/123/avatar.png ``` ## Deleting blobs To delete a file, you can use the `blob.del()` method. This example create an API route to delete a blob by its pathname. ```ts [server/api/files/[...pathname\\].delete.ts] import { blob } from '@nuxthub/blob' import { eventHandler, getRouterParams, sendNoContent } from 'h3' export default eventHandler(async (event) => { const { pathname } = getRouterParams(event) await blob.del(pathname) return sendNoContent(event) }) ``` ### `del()` The `del()` method deletes one or multiple blob objects from the Blob storage. You can also delete multiple blobs at once: ```ts await blob.del('images/1.jpg') await blob.del(['images/1.jpg', 'images/2.jpg', 'images/3.jpg']) ``` ::note You can also use `delete()` as an alias for `del()` . :: #### Params ::field-group :::field{required name="pathname" type="String | String[]"} The pathname(s) of the blob(s) to delete. ::: :: #### Return Returns nothing. ## Validation ### `ensureBlob()` A server-side utility to validate a file by checking its size and type before uploading. ```ts import { ensureBlob } from '@nuxthub/blob' // Will throw an error if the file is not an image or is larger than 1MB ensureBlob(file, { maxSize: '1MB', types: ['image'] }) ``` ::tip This utility is automatically used by [`handleUpload()`](https://hub.nuxt.com/#handleupload) when you provide the `ensure` option. :: #### Params ::field-group :::field{required name="file" type="Blob"} The file to validate. ::: :::field{required name="options" 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']` , `['image']` , `['video']` , `['pdf']` , etc. ::::: :::: ::: :: #### Return Returns nothing. Throws an error if the file doesn't meet the requirements. # Setup NuxtHub Key Value Storage automatically configures [Nitro Storage](https://nitro.build/guide/storage){rel=""nofollow""}, which is built on [unstorage](https://unstorage.unjs.io/){rel=""nofollow""}. ## 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 } }) ``` ### Automatic Configuration When building the Nuxt app, NuxtHub automatically configures the key-value storage driver on many providers. ::tabs{sync="provider"} :::tabs-item{icon="i-simple-icons-upstash" label="Upstash"} 1. Install the `@upstash/redis` package :pm-install{name="@upstash/redis"} 2. Set the `UPSTASH_REDIS_REST_URL` environment variable to your Upstash Redis REST URL. ```bash [.env] UPSTASH_REDIS_REST_URL=https://... UPSTASH_REDIS_REST_TOKEN=... ``` ::::tip When deploying to Vercel, we automatically detect if `KV_REST_API_URL` and `KV_REST_API_TOKEN` environment variables are set, and use them to configure the Upstash Redis connection. :::: ::: :::tabs-item{icon="i-simple-icons-redis" label="Redis"} 1. Install the `ioredis` package :pm-install{name="ioredis"} 2. Set the `REDIS_URL` environment variable to your Redis connection URL. ```bash [.env] REDIS_URL=redis://localhost:6379 ``` ::::tip When deploying to Vercel, we automatically detect if `REDIS_URL` or `KV_URL` environment variable are set, and use one of them to configure the Redis connection. :::: ::: :::tabs-item{icon="i-simple-icons-cloudflare" label="Cloudflare KV"} When deploying to Cloudflare, configure [Cloudflare Workers KV](https://developers.cloudflare.com/kv/){rel=""nofollow""} by providing the namespace ID. NuxtHub auto-generates the wrangler bindings at build time. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { kv: { driver: 'cloudflare-kv-binding', namespaceId: '' } } }) ``` ::::callout{to="https://developers.cloudflare.com/kv/concepts/kv-bindings/"} Learn more about KV bindings on Cloudflare's documentation. :::: ::: :::tabs-item{icon="i-simple-icons-deno" label="Deno KV"} When deploying to Deno Deploy, it automatically configures [Deno KV](https://deno.com/kv){rel=""nofollow""} . ::: :::tabs-item{icon="i-simple-icons-nodedotjs" label="Other"} When deploying to other providers, Nitro Storage `kv` is configured to use the [filesystem](https://unstorage.unjs.io/drivers/fs#nodejs-filesystem-lite){rel=""nofollow""}. ::::tip{to="https://hub.nuxt.com/#manual-configuration"} You can manually configure the `kv` mount to use a different storage driver. :::: Or directly set the `REDIS_URL` environment variable. ```bash [.env] REDIS_URL=redis://localhost:6379 ``` ::: :: ::important If no automatic configuration is found, it will default to [filesystem](https://unstorage.unjs.io/drivers/fs#nodejs-filesystem-lite){rel=""nofollow""} and store the data in the `.data/kv` . :: ### Manual Configuration You can use any [unstorage](https://unstorage.unjs.io/drivers){rel=""nofollow""} driver by configuring the `hub.kv` option with a driver and its options. ::callout{to="https://unstorage.unjs.io/drivers"} You can find the driver list on [unstorage documentation](https://unstorage.unjs.io/drivers){rel=""nofollow""} with their configuration. :: ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { kv: { driver: 'redis', url: 'redis://localhost:6379', /* any additional driver options */ } } }) ``` ::callout{to="https://unstorage.unjs.io/drivers"} You can find the driver list on [unstorage documentation](https://unstorage.unjs.io/drivers){rel=""nofollow""} with their configuration. :: # Using KV SDK NuxtHub provides access to the Key-Value storage through an [unstorage](https://unstorage.unjs.io){rel=""nofollow""} instance. ## Importing the KV storage NuxtHub provides two ways to import the KV storage: ### Recommended: `@nuxthub/kv` Use `@nuxthub/kv` to import the KV storage. This works everywhere, including in Nuxt server routes and external bundlers like [Workflow](https://useworkflow.dev){rel=""nofollow""}: ```ts import { kv } from '@nuxthub/kv' ``` ::tip{icon="i-lucide-sparkles"} This is the **recommended approach** as it works with both Nuxt and external tools that bundle your code independently. :: ### Legacy: `hub:kv` The virtual module `hub:kv` is still supported for backwards compatibility (Nuxt only): ```ts import { kv } from 'hub:kv' ``` ::tip `kv` is auto-imported on the server-side, so you can use it directly without importing. :: ## Set an item Puts an item in the storage. ```ts import { kv } from '@nuxthub/kv' await kv.set('vue', { year: 2014 }) // using prefixes to organize your KV namespace, useful for the `keys` operation await kv.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 import { kv } from '@nuxthub/kv' await kv.set('vue:nuxt', { year: 2016 }, { ttl: 60 }) ``` ## Get an item Retrieves an item from the Key-Value storage. ```ts import { kv } from '@nuxthub/kv' const vue = await kv.get('vue') /* { year: 2014 } */ ``` ## Has an item Checks if an item exists in the storage. ```ts import { kv } from '@nuxthub/kv' const hasAngular = await kv.has('angular') // false const hasVue = await kv.has('vue') // true ``` ## Delete an item Delete an item with the given key from the storage. ```ts import { kv } from '@nuxthub/kv' await kv.del('react') ``` ## Clear the KV namespace Deletes all items from the KV namespace.. ```ts import { kv } from '@nuxthub/kv' await kv.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 import { kv } from '@nuxthub/kv' await kv.clear('react') ``` ## List all keys Retrieves all keys from the KV storage. ```ts import { kv } from '@nuxthub/kv' const keys = await kv.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 import { kv } from '@nuxthub/kv' const vueKeys = await kv.keys('vue') /* [ 'vue:nuxt', 'vue:quasar' ] */ ``` # Setup NuxtHub Cache automatically configures [Nitro's cache storage](https://nitro.build/guide/cache#customize-cache-storage){rel=""nofollow""}. It allows you to cache API routes, server functions, and pages in your application. ## Getting Started Enable cache storage in your project by setting `cache: true` in the NuxtHub config. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { cache: true } }) ``` ## Cache vs KV NuxtHub provides both Cache and KV storage. Understanding when to use each helps you make the right architectural decisions. | Use Case | Recommendation | | ------------------------ | -------------- | | Response caching | **Cache** | | API route caching | **Cache** | | Computed data caching | **Cache** | | User sessions | **KV** | | Feature flags | **KV** | | Rate limiting counters | **KV** | | Persistent configuration | **KV** | ### When to Use Cache Cache is ideal for data that can be recomputed and benefits from automatic expiration: - **TTL-based expiration**: Cached data automatically expires after a specified duration - **Response caching**: Use `cachedEventHandler` or `cachedFunction` for API responses - **Computed data**: Cache expensive computations with automatic invalidation - **No manual cleanup**: Expired entries are removed automatically ```ts [server/api/posts.ts] export default cachedEventHandler(async () => { const posts = await fetchPosts() return posts }, { maxAge: 60 * 60 }) // Cache for 1 hour ``` ### When to Use KV KV is designed for data that must persist until explicitly deleted: - **Persistent data**: Information that should not expire automatically - **Session storage**: User sessions and authentication tokens - **Application state**: Feature flags and configuration values - **Counters**: Rate limiting and analytics counters ```ts [server/api/session.ts] import { kv } from 'hub:kv' export default defineEventHandler(async (event) => { const sessionId = getCookie(event, 'session') const session = await kv.get(`sessions:${sessionId}`) return session }) ``` ::tip As a general rule, use **Cache** for data that can be recomputed, and use **KV** for data that must persist. :: ### Automatic Configuration NuxtHub automatically configures the cache storage driver based on your hosting provider. ::tabs{sync="provider"} :::tabs-item{.p-4 icon="i-simple-icons-vercel" label="Vercel"} When deploying to Vercel, it automatically configures [Vercel Runtime Cache](https://vercel.com/changelog/introducing-the-runtime-cache-api){rel=""nofollow""}. No configuration is necessary to enable the Vercel Runtime Cache. ::: :::tabs-item{.p-4 icon="i-simple-icons-cloudflare" label="Cloudflare"} When deploying to Cloudflare, configure [Cloudflare Workers KV](https://developers.cloudflare.com/kv/){rel=""nofollow""} for caching by providing the namespace ID. NuxtHub auto-generates the wrangler bindings at build time. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { cache: { driver: 'cloudflare-kv-binding', namespaceId: '' } } }) ``` ::::callout{to="https://developers.cloudflare.com/kv/concepts/kv-bindings/"} Learn more about KV bindings on Cloudflare's documentation. :::: ::: :::tabs-item{.p-4 icon="i-simple-icons-nodedotjs" label="Other"} When deploying to other providers, it automatically configures the [filesystem](https://unstorage.unjs.io/drivers/fs#nodejs-filesystem-lite){rel=""nofollow""}. ::::tip{to="https://hub.nuxt.com/#custom-driver"} You can configure the `cache` to use a different storage driver. :::: ::: :: ### Custom Driver You can use any [unstorage](https://unstorage.unjs.io/drivers){rel=""nofollow""} driver by providing a configuration object to `cache`. ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { cache: { driver: 'redis', url: 'redis://localhost:6379' } } }) ``` ::callout{to="https://unstorage.unjs.io/drivers"} You can find the driver list on [unstorage documentation](https://unstorage.unjs.io/drivers){rel=""nofollow""} with their configuration. :: # Caching in Nuxt ## 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.build/guide/cache#options"} Read more about [Nitro Cache options](https://nitro.build/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.build/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.build/guide/cache"} Read more about Nitro Cache. :: ### Normalizing Cache Keys ::important **Cache keys are automatically normalized** using an internal utility that removes non-alphanumeric characters such as `/` and `-` . This behavior helps ensure compatibility across various storage backends (e.g., `file systems` , `key-value` stores) that might have restrictions on characters in `keys` , and also prevents potential path traversal vulnerabilities. :: For example: ```ts getKey: () => '/api/products/sale-items' ``` Would generate a key like: ```ts api/productssaleitems.json ``` This behavior may result in keys that look different from the original route or identifier. ::tip To manually reproduce the same normalized key pattern used by Nitro (e.g., when invalidating cache entries), you can use the `escapeKey` utility function provided below: :: ```ts function escapeKey(key: string | string[]) { return String(key).replace(/\W/g, ""); } ``` It's recommended to use `escapeKey()` when invalidating manually using route paths or identifiers to ensure consistency with Nitro's internal key generation. For example, if your `getKey` function is: ```ts getKey: (id: string) => `product/${id}/details` ``` And you want to invalidate `product/123/details`, you would do: ```ts const normalizedKey = escapeKey('product/123/details') await useStorage('cache').removeItem(`nitro:functions:getProductDetails:${normalizedKey}.json`) ``` # 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] ``` # 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 } } }) ``` ## Supported Providers Nitro WebSocket is currently supported with the following Nitro presets: - Node.js - Deno - Bun - Cloudflare - We recommend using the `cloudflare_durable` preset ::callout{to="https://github.com/nitrojs/nitro/issues/2171"} See [nitrojs/nitro#2171](https://github.com/nitrojs/nitro/issues/2171){rel=""nofollow""} for platform support status. :: ## 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. # CI/CD Deployment NuxtHub applications deploy through standard CI/CD pipelines. Database migrations require special attention, particularly for Cloudflare D1. ## Cloudflare Deployment ### Workers Builds The simplest approach is to use [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/){rel=""nofollow""}, which automatically deploys your application on every commit. 1. Create a Workers project in the [Cloudflare dashboard](https://dash.cloudflare.com/?to=/\:account/workers-and-pages/create){rel=""nofollow""} 2. Connect your GitHub or GitLab repository 3. Configure the build settings: - **Build command**: `npm run build` - **Build output directory**: `dist` ::tip NuxtHub automatically generates wrangler bindings from your `nuxt.config.ts` during the build process. No manual `wrangler.jsonc` file is required. :: ::note If you use a custom `wrangler.jsonc` with named environments, set `CLOUDFLARE_ENV` as a build-time variable for non-production environments in **Workers Builds → Build configuration → Environment variables**: - `CLOUDFLARE_ENV=preview` for preview deployments - `CLOUDFLARE_ENV=staging` for staging deployments - Leave unset for production (uses default environment) :: ::warning{to="https://hub.nuxt.com/#d1-migrations-in-cicd"} If your project uses Cloudflare D1, Workers Builds still requires an explicit migration step before deploy. The default `npm run build` and `wrangler deploy` flow does not apply D1 migrations automatically. :: ### GitHub Actions For more control over your deployment process, you can use GitHub Actions: ```yaml [.github/workflows/deploy.yml] name: Deploy to Cloudflare on: push: branches: [main] pull_request: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - name: Install dependencies run: pnpm install - name: Build run: pnpm build env: CLOUDFLARE_ENV: ${{ github.ref == 'refs/heads/main' && '' || 'preview' }} - name: Deploy run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} ``` ## D1 Migrations in CI/CD ::important Cloudflare D1 databases require special handling because they are not accessible during build time in CI environments. :: ### The Problem Unlike PostgreSQL or MySQL, where build-time migrations work because the database is reachable over the network, Cloudflare D1 presents unique challenges: - It requires Worker bindings to access the database - These bindings are only available at runtime - CI build environments have no database connection ### Solution: Pre-Deployment Migration Step Run migrations before deployment using Wrangler or the NuxtHub CLI. NuxtHub generates `migrations_table` and `migrations_dir` in `.output/server/wrangler.json` for D1 projects. This enables Wrangler-managed migrations, but it does not execute them automatically during `wrangler deploy`. ::warning If you applied migrations with NuxtHub before v0.10 and you use Cloudflare D1, some rows in `_hub_migrations` may be missing the `.sql` suffix. Wrangler requires `.sql` in order to recognize the migration files. :: Fix: Update the migration names in the `_hub_migrations` table to include the `.sql` suffix. ```sql -- Preview affected rows SELECT id, name FROM _hub_migrations WHERE name NOT LIKE '%.sql'; -- Append .sql UPDATE _hub_migrations SET name = name || '.sql' WHERE name NOT LIKE '%.sql'; ``` #### 1. Disable Build-Time Migrations ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'sqlite', driver: 'd1', connection: { databaseId: '' }, applyMigrationsDuringBuild: false } } }) ``` #### 2. Add the Migration Step to Your CI/CD Pipeline **Option A: Using Wrangler (recommended)** Use Wrangler's built-in D1 migrations command. This requires your migrations to follow [Wrangler's migration format](https://developers.cloudflare.com/d1/build-with-d1/d1-migrations/){rel=""nofollow""}. NuxtHub already generates the required metadata in `.output/server/wrangler.json`, so you can run migrations directly against the generated config: ```bash [Terminal] npx wrangler d1 migrations apply DB --remote --config .output/server/wrangler.json && npx wrangler deploy ``` If you maintain a manual `wrangler.jsonc`, configure it to use NuxtHub's migration table and output directory: ```jsonc [wrangler.jsonc] { "d1_databases": [{ "binding": "DB", "database_id": "", "migrations_table": "_hub_migrations", "migrations_dir": ".output/server/db/migrations/sqlite/" }] } ``` ```yaml [.github/workflows/deploy.yml] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - name: Install dependencies run: pnpm install - name: Build run: pnpm build - name: Apply D1 Migrations run: npx wrangler d1 migrations apply DB --remote --config .output/server/wrangler.json env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - name: Deploy run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} ``` **Option B: Using `nuxt db migrate`** If you have the D1 HTTP credentials configured, you can use the Nuxt CLI to apply migrations: ```yaml [.github/workflows/deploy.yml] - name: Apply D1 Migrations run: npx nuxt db migrate env: NUXT_HUB_CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} NUXT_HUB_CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} NUXT_HUB_CLOUDFLARE_DATABASE_ID: ${{ secrets.CLOUDFLARE_DATABASE_ID }} ``` ::note This uses the D1 HTTP API to apply migrations remotely. Make sure your `nuxt.config.ts` has `driver: 'd1'` or `driver: 'd1-http'` configured. :: ## PostgreSQL / MySQL CI/CD PostgreSQL and MySQL migrations can run at build time because the database is reachable via a connection string. ```yaml [.github/workflows/deploy.yml] - name: Build with migrations run: pnpm build env: DATABASE_URL: ${{ secrets.DATABASE_URL }} ``` ::tip Build-time migrations are enabled by default. NuxtHub automatically applies them during `nuxt build` . :: ### Running Migrations in a Dedicated Job For more control over the migration process, you can run migrations in a separate job: ```yaml [.github/workflows/deploy.yml] jobs: migrate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install - name: Run migrations run: npx nuxt db migrate env: DATABASE_URL: ${{ secrets.DATABASE_URL }} deploy: needs: migrate runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # ... build and deploy steps ``` ## Vercel Deployment Vercel handles CI/CD automatically when you connect your repository. ### Setup 1. Import your project in the [Vercel dashboard](https://vercel.com/new){rel=""nofollow""} 2. Configure environment variables under **Project Settings → Environment Variables** 3. Vercel will deploy automatically on every push ### Database Migrations For Vercel deployments with PostgreSQL or MySQL, migrations run during the build process because Vercel's build environment has network access: ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: { dialect: 'postgresql', // Migrations run during build since Vercel's build environment has network access } } }) ``` ::callout{to="https://vercel.com/docs/deployments/git"} Learn more about Vercel Git integrations in the Vercel documentation. :: ## Environment-Specific Secrets ### GitHub Actions Store your secrets in **Repository Settings → Secrets and variables → Actions**: | Secret | Description | | ------------------------ | ------------------------------------------------------------- | | `CLOUDFLARE_API_TOKEN` | Cloudflare API token with Workers, D1, KV, and R2 permissions | | `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID | | `CLOUDFLARE_DATABASE_ID` | The D1 database ID | | `DATABASE_URL` | PostgreSQL or MySQL connection string | ### Per-Environment Secrets Use GitHub Environments to separate production and staging secrets: ```yaml [.github/workflows/deploy.yml] jobs: deploy-production: environment: production # This job uses secrets from the production environment deploy-preview: environment: preview # This job uses secrets from the preview environment ``` ::callout --- to: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment --- Learn more about GitHub Environments in the GitHub documentation. :: # Environment Variables NuxtHub automatically detects and configures resources based on environment variables. The following reference covers all supported variables, organized by feature. ## Database ### PostgreSQL | Variable | Description | Priority | | ---------------- | ---------------------------------- | -------- | | `POSTGRES_URL` | PostgreSQL connection string | 1st | | `POSTGRESQL_URL` | PostgreSQL connection string | 2nd | | `DATABASE_URL` | Generic database connection string | 3rd | ### MySQL | Variable | Description | Priority | | -------------- | ---------------------------------- | -------- | | `MYSQL_URL` | MySQL connection string | 1st | | `DATABASE_URL` | Generic database connection string | 2nd | ### SQLite / Turso | Variable | Description | Priority | | -------------------- | ---------------------------------- | -------- | | `TURSO_DATABASE_URL` | Turso database URL | 1st | | `TURSO_AUTH_TOKEN` | Turso authentication token | - | | `LIBSQL_URL` | libSQL database URL | 2nd | | `LIBSQL_AUTH_TOKEN` | libSQL authentication token | - | | `DATABASE_URL` | Generic database connection string | 3rd | ::note When no environment variables are set, NuxtHub falls back to a local SQLite file at `.data/db/sqlite.db` . :: ### D1 over HTTP These variables are required when using the `d1-http` driver to access Cloudflare D1 from non-Cloudflare hosts. | Variable | Description | | --------------------------------- | ---------------------------------------- | | `NUXT_HUB_CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID | | `NUXT_HUB_CLOUDFLARE_API_TOKEN` | Cloudflare API token with D1 permissions | | `NUXT_HUB_CLOUDFLARE_DATABASE_ID` | The ID of your D1 database | ## Blob Storage ### S3 / R2 | Variable | Description | Required | | ---------------------- | ------------------------------------- | -------- | | `S3_ACCESS_KEY_ID` | AWS or R2 access key ID | Yes | | `S3_SECRET_ACCESS_KEY` | AWS or R2 secret access key | Yes | | `S3_BUCKET` | Bucket name | Yes | | `S3_REGION` | AWS region (use `auto` for R2) | Yes | | `S3_ENDPOINT` | Custom endpoint URL (required for R2) | R2 only | ### Vercel Blob | Variable | Description | | ----------------------- | ---------------------------- | | `BLOB_READ_WRITE_TOKEN` | Vercel Blob read/write token | ## Key-Value Storage ### Upstash Redis | Variable | Description | | -------------------------- | ------------------------ | | `UPSTASH_REDIS_REST_URL` | Upstash Redis REST URL | | `UPSTASH_REDIS_REST_TOKEN` | Upstash Redis REST token | ### Redis | Variable | Description | | ----------- | -------------------- | | `REDIS_URL` | Redis connection URL | ## Cache DevTools These variables are required for cache management in Nuxt DevTools when self-hosting on Cloudflare. | Variable | Description | | ---------------------------------------- | ---------------------------------------- | | `NUXT_HUB_CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID | | `NUXT_HUB_CLOUDFLARE_API_TOKEN` | Cloudflare API token with KV permissions | | `NUXT_HUB_CLOUDFLARE_CACHE_NAMESPACE_ID` | KV namespace ID for cache storage | ::note These variables are only required for DevTools cache management. Normal cache operations work directly through KV bindings without additional configuration. :: ## Build & Deployment | Variable | Description | Used By | | ---------------------- | ---------------------------------------- | ------------- | | `CLOUDFLARE_ENV` | Wrangler environment to use during build | Cloudflare | | `NUXT_HUB_PROJECT_KEY` | NuxtHub project key (deprecated) | NuxtHub Admin | | `NUXT_HUB_USER_TOKEN` | NuxtHub user token (deprecated) | NuxtHub Admin | ## Detection Priority NuxtHub checks environment variables in a specific order and uses the first one found. If none are set, it falls back to local development defaults where available. ### PostgreSQL `POSTGRES_URL` → `POSTGRESQL_URL` → `DATABASE_URL` → PGlite (local) ### MySQL `MYSQL_URL` → `DATABASE_URL` → error (no local fallback available) ### SQLite `TURSO_DATABASE_URL` → `LIBSQL_URL` → `DATABASE_URL` → local file ### KV `UPSTASH_REDIS_REST_URL` → `REDIS_URL` → filesystem ### Blob S3 variables → `BLOB_READ_WRITE_TOKEN` → filesystem ::tip Configure environment variables in your hosting provider's dashboard or in a `.env` file for local development. Never commit `.env` files to version control. :: # NuxtHub Multi-Vendor is now available ::tip This feature is available in [`@nuxthub/core >= v0.10.0`](https://github.com/nuxt-hub/core/releases/tag/v0.10.0){rel=""nofollow""} . :: Since [Vercel's acquisition of NuxtLabs](https://nuxtlabs.com){rel=""nofollow""}, we've been working to let you build full-stack Nuxt applications across multiple hosting providers. Today, we're excited to announce that **NuxtHub is now multi-vendor**. ## Deploy Anywhere With **v0.10**, you can now deploy your NuxtHub projects to any hosting provider while keeping an almost zero-config experience. Whether you choose Cloudflare, Vercel, AWS, or any other provider, NuxtHub now adapts to your infrastructure. ### What's Supported NuxtHub v0.10 brings multi-cloud support for all core features: - **Database**: Use SQLite, PostgreSQL, or MySQL with [Drizzle ORM](https://hub.nuxt.com/docs/database) — NuxtHub auto-configures based on your provider - **Blob Storage**: Upload and serve files from [Cloudflare R2](https://developers.cloudflare.com/r2/){rel=""nofollow""}, [Vercel Blob](https://vercel.com/docs/storage/vercel-blob){rel=""nofollow""}, [AWS S3](https://aws.amazon.com/s3/){rel=""nofollow""}, and more - **KV Storage**: Key-value storage via [Cloudflare KV](https://developers.cloudflare.com/kv){rel=""nofollow""}, [Upstash Redis](https://upstash.com/docs/redis){rel=""nofollow""}, [Vercel KV](https://vercel.com/docs/storage/vercel-kv){rel=""nofollow""}, and others - **Cache Storage**: Efficient caching that works across all supported providers ```ts [nuxt.config.ts] export default defineNuxtConfig({ hub: { db: 'postgresql', // or 'sqlite', 'mysql' blob: true, kv: true, cache: true } }) ``` NuxtHub detects your deployment environment and configures the appropriate drivers automatically. It also uses PGLite locally if no PostgreSQL connection is provided. ::note{to="https://hub.nuxt.com/docs/getting-started/migration"} Read the migration guide to upgrade your existing project to v0.10. :: ## First-Class Drizzle Support NuxtHub v0.10 introduces a completely new database experience powered by [Drizzle ORM](https://orm.drizzle.team){rel=""nofollow""}. This isn't just a wrapper, it's a deep integration that makes working with a database in Nuxt as easy as using a `db` instance. ### Auto-Registered Schema Define your schema in `server/db/schema.ts` (or split across multiple files in `server/db/schema/`), and NuxtHub automatically registers everything: ```ts [server/db/schema.ts] import { pgTable, text, serial, timestamp } from 'drizzle-orm/pg-core' export const users = pgTable('users', { id: serial().primaryKey(), name: text().notNull(), email: text().notNull().unique(), createdAt: timestamp().notNull().defaultNow() }) ``` Your schema is then accessible via the `schema` object in the `hub:db` namespace: ```ts [server/api/users.get.ts] import { db, schema } from 'hub:db' export default eventHandler(async () => { return await db.select().from(schema.users) }) ``` ### Extendable by Modules & Layers One of the most powerful features is the ability for [Nuxt modules](https://nuxt.com/modules){rel=""nofollow""} and [layers](https://nuxt.com/docs/getting-started/layers){rel=""nofollow""} to extend your database schema. This opens up exciting possibilities for the ecosystem, an auth module could automatically add user tables, or a CMS module that brings its own content schemas. ```ts [modules/auth/index.ts] export default defineNuxtModule({ setup(options, nuxt) { nuxt.hook('hub:db:schema:extend', async ({ dialect, paths }) => { paths.push(await resolvePath(`./schema/users.${dialect}`)) }) } }) ``` ### The `nuxt db` CLI Managing your database is now as simple as running a few commands: ```bash [Terminal] # Generate migrations from schema changes npx nuxt db generate # Apply migrations npx nuxt db migrate # Run SQL queries directly npx nuxt db sql "SELECT * FROM users" # Mark migrations as applied npx nuxt db mark-as-migrated # Drop a table npx nuxt db drop
``` Migrations are automatically applied during development and at build time — no extra configuration needed. ::note{to="https://hub.nuxt.com/docs/database"} Read the full Database documentation to learn more. :: ::important We are in the process of building `@nuxt/db` based on the work of NuxtHub DB v0.10. :: ## Templates Update We are in the process of updating all our templates to support the new multi-vendor architecture. Stay tuned for updated starters that work out of the box with Vercel, Cloudflare, and more. ## NuxtHub Admin Transition As we move toward a fully self-hosted, multi-cloud future, we're making important changes to NuxtHub Admin. ### Migration Tool We've added a **guided migration tool** directly in [NuxtHub Admin](https://admin.hub.nuxt.com){rel=""nofollow""}. This tool helps you: - **Stay on Cloudflare**: Keep your project on Cloudflare with current bindings using `wrangler.jsonc` - **Move to Vercel**: Migrate your project and optionally migrate the database, KV, and blob storage to Vercel The migration tool walks you through each step, ensuring your data and configuration are preserved. ### Subscription Changes **During December 2025**, we will cancel all active subscriptions. Pro-rata refunds will be issued for any unused time beyond **December 31st, 2025**. During this period: - You **won't be able** to create new projects on NuxtHub Admin - You **will still be able**until February 2nd, 2026 to: - deploy existing projects - manage your projects on the dashboard ### CLI & Action Deprecation Starting **February 2nd, 2026**, new deployments using the `nuxthub` CLI and GitHub Action will no longer work. We recommend switching to your provider's native deployment method: - **Cloudflare**: Use [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/){rel=""nofollow""} - **Vercel**: Use the [Vercel CLI](https://vercel.com/docs/cli){rel=""nofollow""} or [Git integration](https://vercel.com/docs/deployments/git){rel=""nofollow""} - **Other providers**: Use their respective CLI or CI/CD pipelines ::callout You can visit [legacy.hub.nuxt.com](https://legacy.hub.nuxt.com){rel=""nofollow""} to access the legacy documentation for **v0.9** . :: ## Looking Forward This release represents a major step in NuxtHub's evolution. With v0.10, NuxtHub works wherever you deploy. Pick the cloud that makes sense for your project without sacrificing the developer experience that makes NuxtHub special. Thank you for being part of this journey. We can't wait to see what you build with NuxtHub v0.10. # Self-Hosting First & Cloud-Agnostic Future ::tip This feature is available in [`@nuxthub/core >= v0.9.1`](https://github.com/nuxt-hub/core/releases/tag/v0.9.1){rel=""nofollow""} . :: Following the [acquisition of NuxtLabs](https://nuxtlabs.com){rel=""nofollow""}, we are evolving NuxtHub to become a truly multi-cloud platform. This transition requires us to move away from features tightly coupled to a single cloud provider and adopt a more flexible, cloud-agnostic approach. ## What's Changing ### NuxtHub Admin Sunset **NuxtHub Admin will be sunset on December 31st, 2025.** This platform was designed specifically for Cloudflare deployments, which conflicts with our multi-cloud vision. We are now recommending all projects to adopt self-hosting practices. ::note --- to: https://hub.nuxt.com/docs/getting-started/deploy#self-hosted-recommended --- Read more about self-hosting. :: ### Enhanced Self-Hosting Support To ensure a smooth transition, we've significantly improved the self-hosting experience with direct Cloudflare API integration. You can now use more features without relying on NuxtHub Admin: #### **Simplified Remote Storage Setup** - New `hub.projectUrl` configuration option for cleaner setup - Environment-based project URL selection (production/preview) - Direct project-to-project authentication via `NUXT_HUB_PROJECT_SECRET_KEY` - No more CLI linking required for remote storage access ::note{to="https://hub.nuxt.com/docs/getting-started/remote-storage"} Read more about the remote storage setup. :: #### **Direct Cloudflare API Support** New environment variables enable direct API access for self-hosted projects: - `NUXT_HUB_CLOUDFLARE_ACCOUNT_ID` - Your Cloudflare account ID - `NUXT_HUB_CLOUDFLARE_API_TOKEN` - API token for service access - `NUXT_HUB_CLOUDFLARE_BUCKET_ID` - For R2 blob operations - `NUXT_HUB_CLOUDFLARE_CACHE_NAMESPACE_ID` - For KV cache operations These credentials allow you to: - Run AI and AutoRAG models during local development - Perform cache bulk deletion operations with the Nuxt DevTools - Generate presigned URLs for blob uploads at runtime ### Cloud-Specific Features Deprecated As part of our multi-cloud strategy, we are deprecating features that are specific to Cloudflare: - **AI & AutoRAG** - Use [AI SDK](https://ai-sdk.dev/){rel=""nofollow""} with the [Workers AI Provider](https://ai-sdk.dev/providers/community-providers/cloudflare-workers-ai){rel=""nofollow""} instead, or access `process.env.AI` directly - **Browser (Puppeteer)** - Cloudflare-specific browser automation - **Vectorize** - Cloudflare's vector database - **Additional Bindings** - Direct Cloudflare Workers bindings These features are now marked as deprecated in the documentation and will be removed in a future major version to maintain framework neutrality. ## Migration Guide ### For Current NuxtHub Admin Users 1. **Switch to self-hosting** by following the updated [remote storage documentation](https://hub.nuxt.com/docs/getting-started/remote-storage) 2. **Set up direct authentication** using `NUXT_HUB_PROJECT_SECRET_KEY` 3. **Configure Cloudflare credentials** if using AI, AutoRAG, or advanced blob/cache features ### For Self-Hosted Projects Update your environment configuration to use the new Cloudflare API credentials: ```bash [.env] # Required for remote storage NUXT_HUB_PROJECT_SECRET_KEY= # Optional: for direct Cloudflare API features (AI, AutoRAG, etc.) NUXT_HUB_CLOUDFLARE_ACCOUNT_ID= NUXT_HUB_CLOUDFLARE_API_TOKEN= NUXT_HUB_CLOUDFLARE_BUCKET_ID= NUXT_HUB_CLOUDFLARE_CACHE_NAMESPACE_ID= ``` ::note In the coming weeks, we will update the NuxtHub Admin with an easier migration path to help you either stay on Cloudflare or move to Vercel. :: # Build full-stack Nuxt applications. ::u-page-hero --- ui: container: lg:items-start orientation: horizontal --- :::tabs{.xl:-mt-10} ::::tabs-item{icon="i-lucide-database" label="Database"} ```ts import { eq, desc } from 'drizzle-orm' import { db, schema } from '@nuxthub/db' // Type-safe queries with Drizzle ORM const todos = await db.query.todos.findMany({ where: eq(schema.todos.completed, false), orderBy: [desc(schema.todos.createdAt)] }) // Insert with automatic type inference await db.insert(schema.todos).values({ title: 'Ship my app', completed: false, }) ``` :::: ::::tabs-item{icon="i-lucide-shapes" label="Blob"} ```ts import { blob } from 'hub:blob' // Ensure the blob is valid ensureBlob(imageData, { maxSize: '1MB', types: ['image'] }) // Upload files with ease const file = await blob.put('avatars/user-1.png', imageData, { access: 'public' }) // List avatars const avatars = await blob.list({ prefix: 'avatars/', limit: 10 }) // Serve the avatar with streaming return blob.serve(event, 'avatars/atinux.png') ``` :::: ::::tabs-item{icon="i-lucide-list" label="KV"} ```ts import { kv } from '@nuxthub/kv' // Store and retrieve any data await kv.set('user:1:session', { token, expiresAt }) const session = await kv.get('user:1:session') // With TTL support await kv.set('rate-limit:ip', count, { ttl: 60 }) ``` :::: ::::tabs-item{icon="i-lucide-zap" label="Cache"} ```ts // Cache API responses for 1 hour export default defineCachedEventHandler(async () => { const data = await $fetch('https://api.example.com') return data }, { maxAge: 60 * 60 }) // Or cache any function const getStats = defineCachedFunction(fetchStats, { maxAge: 60 * 5, }) ``` :::: ::: #headline :::u-button --- size: sm to: https://hub.nuxt.com/changelog/nuxthub-multi-vendor trailing-icon: i-lucide-arrow-right variant: outline --- NuxtHub multi-vendor is now available ::: #title Build [full-stack]{.text-primary} Nuxt apps. #description NuxtHub is a Nuxt module giving you all the features required to ship full-stack applications, with no vendor lock-in. #links :::u-button --- size: lg to: https://hub.nuxt.com/docs/getting-started/installation trailing-icon: i-lucide-arrow-right --- Get started ::: :u-input-copy{value="npx nuxt module add hub"} :: ::u-container :::u-page-grid{.pb-12.xl:pb-24} ::::landing-feature --- description: Deploy your application with confidence to your favorite cloud provider. icon: i-lucide-cloud title: Multi-Platform to: https://hub.nuxt.com/docs/getting-started/deploy --- :::: ::::landing-feature --- description: Query your database with a type-safe ORM and automated migrations. icon: i-lucide-database title: SQL Database to: https://hub.nuxt.com/docs/database --- :::: ::::landing-feature --- description: Upload, store and serve images, videos and any kind of file. icon: i-lucide-shapes title: Files Storage to: https://hub.nuxt.com/docs/blob --- :::: ::::landing-feature --- description: Leverage a Key-Value data store replicated globally for maximum performance. icon: i-lucide-list title: KV Storage to: https://hub.nuxt.com/docs/kv --- :::: ::::landing-feature --- description: Cache Nuxt pages, API routes and server functions on the Edge. icon: i-lucide-zap title: Caching to: https://hub.nuxt.com/docs/cache --- :::: ::::landing-feature --- description: Access your application's data and storage in the Nuxt DevTools. icon: i-lucide-monitor title: DevTools to: https://hub.nuxt.com/docs/getting-started#nuxt-devtools --- :::: ::: ::