Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export default defineBuildConfig({
input: 'src/cache/runtime/',
outDir: 'dist/cache/runtime',
builder: 'mkdist'
},
// Remote (cloudflare dev bindings)
{
input: 'src/remote/runtime/',
outDir: 'dist/remote/runtime',
builder: 'mkdist'
}
]
})
48 changes: 48 additions & 0 deletions docs/content/docs/1.getting-started/3.deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ navigation.title: Deploy
description: Learn how to host a full-stack Nuxt application with minimal configuration.
---

NuxtHub supports multiple cloud providers including Vercel, Cloudflare Workers, and any Node.js hosting platform. Each provider offers different storage options that NuxtHub automatically configures.

## Vercel

::tip
Expand Down Expand Up @@ -56,6 +58,12 @@ export default defineNuxtConfig({
})
```

::important{title="Remote Bindings in Development"}
When you configure a Cloudflare binding ID (`databaseId`, `namespaceId`, `bucketName`, or `hyperdriveId`), NuxtHub automatically connects to your remote Cloudflare resources during local development. This uses Wrangler's [`getPlatformProxy`](https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy) to provide real bindings in dev mode.

Ensure `wrangler` is installed as a dev dependency: `npx nypm add -D wrangler`
::

::tip
See a working example at [onmax/repros/nuxthub-716](https://github.com/onmax/repros/tree/main/nuxthub-716) — deployed without a `wrangler.toml` file.
::
Expand Down Expand Up @@ -84,6 +92,46 @@ During the build process, NuxtHub resolves the target environment using the foll
3. Generates `.output/server/wrangler.json` with the resolved bindings
4. Wrangler uses this configuration during deployment

### Local-Only Development

If you prefer to use local storage during development and only connect to Cloudflare resources in production, use Nuxt's `$production` environment override. This pattern ensures binding IDs are only applied when building for production:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
hub: {
db: 'sqlite',
kv: true,
blob: true
},
// Only use remote bindings in production
$production: {
hub: {
db: {
dialect: 'sqlite',
driver: 'd1',
connection: { databaseId: '<database-id>' }
},
kv: {
driver: 'cloudflare-kv-binding',
namespaceId: '<kv-namespace-id>'
},
blob: {
driver: 'cloudflare-r2',
bucketName: '<bucket-name>'
}
}
}
})
```

With this configuration:
- **Development**: Uses local storage (SQLite file, filesystem) in `.data/` directory
- **Production**: Connects to your Cloudflare D1, KV, and R2 resources

::note
The `$production` override merges with your base config. You only need to specify the properties that differ in production.
::

### Deploy

Create a [Cloudflare Workers project](https://dash.cloudflare.com/?to=/:account/workers-and-pages/create) and link your GitHub or GitLab repository. NuxtHub auto-configures bindings from your `nuxt.config.ts` during build.
Expand Down
36 changes: 36 additions & 0 deletions docs/content/docs/1.getting-started/4.migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ You can visit [legacy.hub.nuxt.com](https://legacy.hub.nuxt.com) to read the doc
| 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)) |
| Remote storage | `hub.projectUrl` + `hub.projectSecretKey` | Auto-enabled when binding IDs configured |
| NuxtHub Admin | Supported | Deprecated (sunset Dec 31, 2025) |
| `nuxthub deploy` | Supported | Deprecated (sunset Jan 31, 2026) |

Expand Down Expand Up @@ -93,6 +94,40 @@ export default defineNuxtConfig({
})
```

### Remote Development

The `projectUrl` and `projectSecretKey` options have been removed. NuxtHub now auto-detects when you want to connect to remote Cloudflare bindings during local development.

When binding IDs are present in your config (e.g., `databaseId`, `namespaceId`, `bucketName`), NuxtHub will:
1. Detect the remote binding configuration
2. Show a warning about connecting to production resources
3. Generate a temporary `wrangler.toml` with `remote = true` for all bindings

```ts [nuxt.config.ts]
export default defineNuxtConfig({
hub: {
db: {
dialect: 'sqlite',
driver: 'd1',
connection: { databaseId: '<database-id>' } // Triggers remote binding
},
kv: { driver: 'cloudflare-kv-binding', namespaceId: '<kv-id>' },
blob: { driver: 'cloudflare-r2', bucketName: '<bucket-name>' }
}
})
```

::warning
Remote bindings connect to **production resources**. Seeds and migrations will run against production data. Confirm only when intentional.
::

::tip
Use `$production` pattern to only enable binding IDs in production, keeping local development isolated:
```ts
connection: { databaseId: { $production: '<database-id>' } }
```
::

## Code Migration

### Database Access
Expand Down Expand Up @@ -384,6 +419,7 @@ Replace `npx nuxthub deploy` with your provider's deployment method:
- [ ] 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
- [ ] Remove `hub.projectUrl` and `hub.projectSecretKey` (remote bindings auto-enable when IDs configured)
- [ ] 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.)
Expand Down
12 changes: 12 additions & 0 deletions docs/content/docs/2.database/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Install Drizzle ORM, Drizzle Kit, and the appropriate driver(s) for the database
- 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) serverless PostgreSQL.
::
::note
Setting a `DATABASE_URL` locally connects to your remote PostgreSQL database during development.
::
:::
:::tabs-item{label="MySQL" icon="i-simple-icons-mysql"}
:pm-install{name="drizzle-orm drizzle-kit mysql2"}
Expand All @@ -30,6 +33,9 @@ Install Drizzle ORM, Drizzle Kit, and the appropriate driver(s) for the database
- Uses `mysql2` driver if you set `DATABASE_URL` or `MYSQL_URL` environment variable.
- Requires environment variable (no local fallback).
::
::note
Setting a `DATABASE_URL` or `MYSQL_URL` locally connects to your remote MySQL database during development.
::
:::
:::tabs-item{label="SQLite" icon="i-simple-icons-sqlite"}
:pm-install{name="drizzle-orm drizzle-kit @libsql/client"}
Expand All @@ -38,9 +44,15 @@ Install Drizzle ORM, Drizzle Kit, and the appropriate driver(s) for the database
- Uses `libsql` driver for [Turso](https://turso.tech) 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.
::
::note
Setting `TURSO_DATABASE_URL` locally connects to your remote Turso database during development.
::
::tip{to="/docs/getting-started/deploy#cloudflare"}
For Cloudflare D1, configure the database ID in your `nuxt.config.ts` and NuxtHub auto-generates the wrangler bindings.
::
::warning
Configuring a `databaseId` connects to your remote Cloudflare D1 database during development. Use the [`$production` pattern](/docs/getting-started/deploy#local-only-development) to keep development local-only.
::
:::
::

Expand Down
12 changes: 12 additions & 0 deletions docs/content/docs/3.blob/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ By default, if NuxtHub cannot detect a driver, files are stored locally in the `
S3_REGION=your-region
S3_ENDPOINT=your-endpoint # (optional)
```

::note
Setting these credentials locally connects to your remote S3 bucket during development.
::
:::
:::tabs-item{label="Vercel Blob" icon="i-simple-icons-vercel"}

Expand All @@ -86,6 +90,10 @@ By default, if NuxtHub cannot detect a driver, files are stored locally in the `
```bash [.env]
BLOB_READ_WRITE_TOKEN=your-token
```

::note
Setting this token locally connects to your remote Vercel Blob store during development.
::
:::

:::tabs-item{label="Cloudflare R2" icon="i-simple-icons-cloudflare"}
Expand All @@ -107,6 +115,10 @@ By default, if NuxtHub cannot detect a driver, files are stored locally in the `
Learn more about R2 bindings on Cloudflare's documentation.
::

::warning
Configuring a `bucketName` connects to your remote Cloudflare R2 bucket during development. Use the [`$production` pattern](/docs/getting-started/deploy#local-only-development) to keep development local-only.
::

::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/).
::
Expand Down
8 changes: 8 additions & 0 deletions docs/content/docs/4.kv/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ When building the Nuxt app, NuxtHub automatically configures the key-value stora
UPSTASH_REDIS_REST_TOKEN=...
```

::note
Setting these credentials locally connects to your remote Upstash Redis during development.
::

::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.
::
Expand Down Expand Up @@ -73,6 +77,10 @@ When building the Nuxt app, NuxtHub automatically configures the key-value stora
Learn more about KV bindings on Cloudflare's documentation.
::

::warning
Configuring a `namespaceId` connects to your remote Cloudflare KV namespace during development. Use the [`$production` pattern](/docs/getting-started/deploy#local-only-development) to keep development local-only.
::

:::

:::tabs-item{label="Deno KV" icon="i-simple-icons-deno"}
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/5.cache/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ NuxtHub automatically configures the cache storage driver based on your hosting
::callout{to="https://developers.cloudflare.com/kv/concepts/kv-bindings/"}
Learn more about KV bindings on Cloudflare's documentation.
::

::warning
Configuring a `namespaceId` connects to your remote Cloudflare KV namespace during development. Use the [`$production` pattern](/docs/getting-started/deploy#local-only-development) to keep development local-only.
::
:::

:::tabs-item{label="Other" icon="i-simple-icons-nodedotjs" class="p-4"}
Expand Down
5 changes: 3 additions & 2 deletions src/blob/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ export function resolveBlobConfig(hub: HubConfig, deps: Record<string, string>):
}) as ResolvedBlobConfig
}

// Cloudflare R2
if (hub.hosting.includes('cloudflare')) {
// Cloudflare R2 (production or dev with bucketName)
const blobConfig = typeof hub.blob === 'object' ? hub.blob : {}
if (hub.hosting.includes('cloudflare') || ('bucketName' in blobConfig && blobConfig.bucketName)) {
return defu(hub.blob, {
driver: 'cloudflare-r2',
binding: 'BLOB'
Expand Down
4 changes: 2 additions & 2 deletions src/cache/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export function resolveCacheConfig(hub: HubConfig): ResolvedCacheConfig | false
return userConfig as ResolvedCacheConfig
}

// Cloudflare KV cache binding
if (hub.hosting.includes('cloudflare')) {
// Cloudflare KV cache binding (production or dev with namespaceId)
if (hub.hosting.includes('cloudflare') || userConfig.namespaceId) {
return defu(userConfig, {
driver: 'cloudflare-kv-binding',
binding: 'CACHE'
Expand Down
20 changes: 8 additions & 12 deletions src/db/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ export async function resolveDatabaseConfig(nuxt: Nuxt, hub: HubConfig): Promise
if (config.driver === 'd1') {
break
}
// Cloudflare D1 (production only - dev uses local libsql by default)
if (hub.hosting.includes('cloudflare') && !nuxt.options.dev) {
// Cloudflare D1 (production, or dev with explicit databaseId)
if ((hub.hosting.includes('cloudflare') && !nuxt.options.dev && !nuxt.options._prepare) || config.connection?.databaseId) {
config.driver = 'd1'
break
}
Expand All @@ -90,19 +90,14 @@ export async function resolveDatabaseConfig(nuxt: Nuxt, hub: HubConfig): Promise
config.connection = defu(config.connection, { url: '' })
break
}
// Cloudflare D1 (production only - dev/prepare uses local libsql)
if (hub.hosting.includes('cloudflare') && !nuxt.options.dev && !nuxt.options._prepare) {
config.driver = 'd1'
break
}
config.driver ||= 'libsql'
config.connection = defu(config.connection, { url: `file:${join(hub.dir!, 'db/sqlite.db')}` })
await mkdir(join(hub.dir, 'db'), { recursive: true })
break
}
case 'postgresql': {
// Cloudflare Hyperdrive with explicit hyperdriveId
if (hub.hosting.includes('cloudflare') && config.connection?.hyperdriveId && !config.driver) {
// Cloudflare Hyperdrive with explicit hyperdriveId (production or dev)
if (config.connection?.hyperdriveId && !config.driver) {
config.driver = 'postgres-js'
break
}
Expand All @@ -121,8 +116,8 @@ export async function resolveDatabaseConfig(nuxt: Nuxt, hub: HubConfig): Promise
break
}
case 'mysql': {
// Cloudflare Hyperdrive with explicit hyperdriveId
if (hub.hosting.includes('cloudflare') && config.connection?.hyperdriveId && !config.driver) {
// Cloudflare Hyperdrive with explicit hyperdriveId (production or dev)
if (config.connection?.hyperdriveId && !config.driver) {
config.driver = 'mysql2'
break
}
Expand Down Expand Up @@ -518,7 +513,8 @@ const db = drizzle(d1HttpDriver, { schema${casingOption} })
export { db, schema }
`
}
if (['postgres-js', 'mysql2'].includes(driver) && hub.hosting.includes('cloudflare') && connection?.hyperdriveId) {
// Hyperdrive requires lazy binding access - bindings only available in request context on CF Workers
if (['postgres-js', 'mysql2'].includes(driver) && (hub.hosting.includes('cloudflare') || connection?.hyperdriveId)) {
const bindingName = driver === 'postgres-js' ? 'POSTGRES' : 'MYSQL'
drizzleOrmContent = generateLazyDbTemplate(
`import { drizzle } from 'drizzle-orm/${driver}'`,
Expand Down
5 changes: 3 additions & 2 deletions src/kv/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ export function resolveKVConfig(hub: HubConfig): ResolvedKVConfig | false {
}) as ResolvedKVConfig
}

// Cloudflare KV
if (hub.hosting.includes('cloudflare')) {
// Cloudflare KV (production or dev with namespaceId)
const kvConfig = typeof hub.kv === 'object' ? hub.kv : {}
if (hub.hosting.includes('cloudflare') || kvConfig.namespaceId) {
return defu(hub.kv, {
driver: 'cloudflare-kv-binding',
binding: 'KV'
Expand Down
Loading
Loading